WebLogic系列漏洞学习之T3:CVE-2015-4852
本文已投稿雷神众测公众号:https://mp.weixin.qq.com/s/iyhfeWofVoq0VBQ35vGUCQ
0x00 前言
15年由FoxGlove团队爆出的Java反序列化漏洞,让weblogic、Websphere、Jenkins等都纷纷中招,本文以WebLogic漏洞来进行探索,也算是老生常谈。虽然网上有一些分析CVE-2015-4852的文章,但是有很多地方对于我这样的小白不太能理解,还有很多直接对CommonsCollections分析的,不能解答我的困惑。在开始分析前,先记录下,有两个问题需要我探索。
- 为什么CommonsCollections能在WebLogic中经过T3自动引发?在收到数据后Weblogic到底怎么操作然后调用CommonsCollections的?
- 为什么CommonsCollections能够引起命令执行,需要什么条件?有哪些类是类似的?
文笔粗糙,如有不当,请各位师傅批评指正。
0x01 复现
环境搭建
环境:centos7.3; docker + docker-compose
- vim Dockerfile
1 | FROM vulhub/weblogic |
- vim docker-compose.yml
1 | version: '2' |
- 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 | import binascii |
进入容器,验证是否执行成功
1 | docker exec -it 363 bash |
成功创建success文件
0x02 深入分析
远程调试
搭建环境复现的时候我们已经对容器开去了远程调试服务,下面只需要对本地IDEA环境进行部署。
首先从容器拷贝root目录,然后单独将相关的jar包拷贝出来
1 | docker cp 363:/root . |
将上述jar_lib和root放到本地,然后用IDEA打开root/Oracle/Middleware/wlserver_10.3,将jar_lib加入libraries
选择weblogic自带的jdk root/jdk/jdk1.6.0_45
添加远程JVM
然后debug,出现下图说明连接上了远程JVM
Weblogic源码分析
ok,到这里我们万事俱备,只差Debug的入口,从哪里入口是一个难题。
首先我考虑的是T3相关的jar,那肯定T3相关的处理能拦截到,但是在weblogic.jar下面找了几个T3的试了下都不成功
1 | weblogic.common.T3Connection #不能拦截 |
然后在我有点绝望的时候,在Weblogic漏洞Java反序列化CVE-2015-4852解析博客中发现有说明漏洞的补丁情况
所以就想我为什么不能在这些地方打断点分析呢?通过快捷键(双击shift)查找,找到了这些类的位置如下
1 | wlthint3client.jar:weblogic.rjvm.InboundMsgAbbrev |
在wlthint3client.jar:weblogic.rjvm.InboundMsgAbbrev:23行这里打断点试试,然后执行脚本,成功拦截
参数var1.head可以看到是我们发送的包含了反序列化数据的包,InboundMsgAbbrev#read()应该是处理从客户端接收到的数据进行读取
往下执行到readObject,我们F7进入该方法进行分析
执行var1.read,继续F7深入
进入到readObject(),往下执行var2=0进入case 0,return (new InboundMsgAbbrev.ServerChannelInputStream(var1)).readObject();
我们先分析new InboundMsgAbbrev.ServerChannelInputStream(var1)
进入后,继续深入getServerChannel()
然后进入到weblogic.rjvm.MsgAbbrevInputStream#getServerChannel(),这个类就是我们刚刚看到的打补丁的第二个地方
继续深入getChannel看看,这次不是在同一个jar了,进入到了 weblogic.jar:weblogic.rjvm.t3.MuxableSocketT3.T3MsgAbbrevJVMConnection#getChannel(),这里是刚开始找debug入口时看到的类,处理T3协议的socket
继续深入进入weblogic.socket.BaseAbstractMuxableSocket(BaseAbstractMuxableSocket implements MuxableSocket, SocketRuntime, ContextHandler, Serializable)
返回了channel,这个时候看到了(“AS:” + MsgAbbrevJVMConnection.ABBREV_TABLE_SIZE + “\n” + “HL” + “:” + 19 + “\n\n”)跟之前学习T3的包联系上了
继续执行,一步步返回到了最初的位置,这个时候完成了ServerChannelInputStream的实例,其实到这里我们可以看出经过这几个步骤,能够对传入的socket进行T3的处理,获取到信息流。接下来调用readObject,我们需要认真分析下这个
进入ChunkedObjectInputStream#read(),curEndEnvelop=-1,调用super.read(),F7进入read()
进入到了ChunkedInputStream#read(),该方法主要是读取head数据也就是我们发送的包
继续返回ChunkedObjectInputStream#read(),会继续调用ChunkedObjectInputStream#read(var1,var2,var3)以及ChunkedInputStream#read(var1,var2,var3),往后执行,发现这几个方法是对数据流进行分块处理,按照7870
即xp将序列化部分分块,依次解析每块的类,然后去执行
以下为分析过程中记录的每个分块的类的通过resolveClass()方法进行的解析
chunkedpos从109-239,sun.reflect.annotation.AnnotationInvocationHandler
chunkedpos从239-393,org.apache.commons.collections.map.TransformedMap
chunkedpos从239-533,org.apache.commons.collections.functors.ChainedTransformer
chunkedpos从533-595,[Lorg.apache.commons.collections.Transformer;
chunkedpos从595-708,org.apache.commons.collections.functors.ConstantTransformer
chunkedpos从708-742,java.lang.Runtime
chunkedpos从742-917,org.apache.commons.collections.functors.InvokerTransformer
chunkedpos从917-953,[Ljava.lang.Object;
chunkedpos从953-1018,[Ljava.lang.Class;
chunkedpos从1018-1055,java.lang.String
chunkedpos从1055-1131,java.lang.Object
chunkedpos从1131-1256,java.util.HashMap
chunkedpos从1256-1338,java.lang.annotation.Retention
根据上面的解析对应到包其实就是下面的图,也就是在反序列化过程中我们调用的类
到这里我们解答了一开始提出的第一个问题:为什么CommonsCollections能在WebLogic中经过T3自动引发?在收到数据后Weblogic到底怎么操作然后调用CommonsCollections的?也就是刚刚说的wlthint3client.jar:weblogic.rjvm.InboundMsgAbbrev将T3数据流的序列化部分依次分块通过resolveClass()来解析类,因为没有在对序列化数据解析时判断他是否安全,没有任何过滤,所以可以直接调用CommonsCollections1中涉及到的所有类。
CommonsCollections1分析
第一个调用CommonsCollections的问题我们清楚了,现在需要探讨第二个问题为什么CommonsCollections能够引起命令执行?
网上模仿ysoserial的CommonsCollections1的代码,这里用的是LazyMap利用链
思路:执行代码生成序列化数据,在这个过程中深入看看到底是怎么执行的命令。这里有个坑,我刚开始执行的时候没有弹出计算器,用的jdk1.8_111,后来换了jdk1.6_45可以了。
1 | package Pocs.WebLogic; |
需要导入CommonsCollections,因为在之前的weblogic包里面没有找到,就去容器里面找到了,导入到项目里面即可。
在Transformer[]数组这里打上断点,开始一步一步分析这段代码
为了方便梳理,我们分为三部分进行分析:
分析第一部分
先生成Transformer数组,值分别是1个ConstantTransformer对象和3个InvokerTransformer对象
先看下Transformer,CTRL+H可以看到Transformer的继承实现关系,ConstantTransformer和InvokerTransformer都实现了Transformer,这里提一下ChainedTransformer也实现了Transformer,后面会用到。
Transformer官方文档说明了这个接口主要是用来将一个对象转换为另一个对象
回到main,步入分析下每个对象的生成
ConstantTransformer(Object constantToReturn),构造一个ConstantTransformer对象,并且设置对象的iConstant值为传入的参数值;重写transformer,返回构造函数传入的值,该值是class类型
InvokerTransformer(String methodName, Class[] paramTypes, Object[] args):构造函数获取方法名、参数类型、参数将其赋值给对象;transform(Object input) 获取类的方法名、参数类型、参数 采用反射机制调用该方法
new Transformer[]完成,此时我们生成了一个transformer数组如下图
接着new一个ChainedTransformer,F7步入分析
ChainedTransformer(Transformer[] transformers):将参数Transformer数组 赋值给对象;transform(Object object)将TransFormer数组的值 依次执行数组每个对象的transform()方法,也就是按照数组顺序去调用transform(),这里有一点容易遗漏,每次调用完transform()会赋值给Object作为下一次调用transform的参数。
以上我们在分析transformer接口的实现类时都分析了它的实现方法transform(),这个是后面要用到的
到这里new ChainedTransformer()完成
分析第二部分
我们继续往下,生成一个HashMap的实例innerMap,接着调用LazyMap.decorate(innerMap, chain)
这里我们看下HashMap和LazyMap,以一段代码来分析下。我们测试了两个实例,一个时LazyMap的Factory factory,一个是LazyMap的Transformer factory。
1 | package Pocs.WebLogic; |
从以上代码我们可以看出,在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()。
到这里我们基本已经清晰了一半
- 创建了一个Transform数组,这个数组的值是一个ContranTransformer和三个InvokerTransformer对象,ContranTransformer.transformer()可以返回class【这里是Runtime】,而InvokerTransformer可以通过反射机制去执行我们想要执行的方法,也就是getRuntime()、invoke、exec
- new ChainedTransformer(transformers)创建一个ChainedTransformer实例,将Transform数组赋值给对象,ChainedTransformer.transform()将TransFormer数组的每个对象按照顺序依次执行数组每个对象的transform()方法
- LazyMap.decorate(innerMap, chain)定义了innerMap的无key处理方法:去自动调用chain.transform()
- 完成以上三步,当我们获取LazyMap的key不存在时,就会去调用chain.transform(),从而依次4个对象的transform(),也就是下图 1 先获取类Runtime,2 getMethod(getRuntime),3 invoke(),4 exec(“calc”),因为前一个执行的结果会作为参数给下一个调用,所以最后会执行Runtime.getRuntime().exec(“calc”)
分析第三部分
继续往下分析,Class.forName获取AnnotationInvocationHandler类,接着 clazz.getDeclaredConstructor获取构造方法sun.reflect.annotation.AnnotationInvocationHandler(java.lang.Class, java.util.Map),设置可调用
下面就是动态代理部分,结合之前学习的动态代理步骤来理解下
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 | // var2是Map接口的某个方法 |
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 | import java.io.*; |
而AnnotationInvocationHandler.readObject()里面调用了我们刚刚分析的触发点Map接口的方法也就是Map.entrySet()。
梳理下:
- 首先利用ChainedTransformer的transform()的循环调用特性创建了exec执行链chain
- 利用LazyMap的decorate()定义了get key不存在时的调用Transformer工厂chain.transform()
- 找到条件去get(不存在的key)从而调用工厂,这个条件就是动态代理AnnotationInvocationHandler.invoke()
- 创建动态代理来代理Map的所有接口,只要调用Map的接口就会去调用代理方法AnnotationInvocationHandler.invoke()
- 找到触发点触发代理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 反序列化漏洞