Java安全的重要拼图
前置知识
Apache Commons Collections是一个扩展了Java标准库里的Collection结构的第三方基础库,它提供了很多强大的数据结构类型和实现了各种集合工具类。作为Apache开放项目的重要组件,Commons Collections被广泛的各种Java应用的开发,⽽正是因为在⼤量web应⽤程序中这些类的实现以及⽅法的调⽤,导致了反序列化⽤漏洞的普遍性和严重性。
commons-collections组件反序列化漏洞的反射链也称为CC链,自从apache commons-collections组件爆出第一个java反序列化漏洞后,就像打开了java安全的新世界大门一样,之后很多java中间件相继都爆出反序列化漏洞。本文分析java反序列化CC1链,前置知识是java安全基础中的反射。
Java机制:反射
参考文章
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
在Java中,Class类与java.lang.reflect类库一起对反射技术进行了全力的支持。在反射包中,我们常用的类主要有Constructor类表示的是Class 对象所表示的类的构造方法,利用它可以在运行时动态创建对象、Field表示Class对象所表示的类的成员变量,通过它可以在运行时动态修改成员变量的属性值(包含private)、Method表示Class对象所表示的类的成员方法,通过它可以动态调用对象的方法(包含private)。
总之简单来说,就是我们可以在Java程序运行时去操作类。调度反射方法,最终是由jvm执行invoke()执行。
分析准备
环境配置:
- CommonsCollections <= 3.2.1
- java < 8u71
Maven依赖:
1 | <dependency> |
利用链:
1 | AnnotationInvocationHandler.readObject()--> |
分析
分析应当是自底向上的,从脆弱点开始,经过一步一步封装成为一条完整的利用链。
分析的时候建议在Maven中把依赖的源码下载下来。
脆弱点分析
脆弱点:org.apache.commons.collections.Transformer,transform方法有21种实现
1 | package org.apache.commons.collections; |
脆弱类:InvokerTransformer,我们重点看它的transform方法,发现可以执行传入的对象中的方法,显然这里可以执行任意代码。

下面是InvokerTransformer的一个构造函数:
1 | public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { |
首先先尝试直接利用InvokerTransformer来执行命令,我们只需要按照上面的构造函数去生成一个对象,然后调用它的transform方法。要执行系统命令,methodName为exec,参数类型为String,参数为calc。在transform中我们只需要传入Runtime类的实例,使用Runtime.getRuntime()
1 | import org.apache.commons.collections.Transformer; |
直接运行,执行弹出计算器命令:

构造序列化链
当然在实际的漏洞中,不可能直接就这么触发,一定是经过了多层调用,当恶意payload执行到这里才会造成RCE。我们就这么盲目地去找吗,当然不是,我们这里是反序列化漏洞,所以最顶层的调用一定是某个readObject()方法。
特地提到这个方法是因为在反序列化漏洞中它起到了关键作用,readObject()方法被重写的的话,反序列化该类时调用便是重写后的readObject()方法,如果该方法书写不当的话就有可能引发恶意代码的执行。
那么接下来我们继续分析,按住ctrl单击transform函数查看调用它的函数,我们跳转到TransformedMap类的transform函数中

可以发现是一个叫**checkSetValue**的函数,就一行很简单直接调用了transform,我们接着查看对它的调用
1 | protected Object checkSetValue(Object value) { |
发现对它的调用只有一处,直接跳转了,在**AbstractInputCheckedMapDecorator**中的setValue函数
1 | public Object setValue(Object value) { |
通过上面的分析,我们可以进一步的构造我们的代码:
要调用setValue这个函数很简单,因为它是一个共有函数,那么下一步是解决如何构造**TransformedMap**的一个实例,然后去作为setValue函数的参数,发现它的构造函数是一个受保护的,所以去找哪里可以调用函数构造它。
1 | protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) { |
查看对构造函数的调用,发现它有一个叫decorate的函数可以构造:
1 | public static Map decorate(Map map, Transformer keyTransformer, |
那么到这里我们就可以写出当前的利用链了:
1 | public static void main(String[] args) { |
可以成功弹出计算器:

此时我们右键setValue函数查看调用,发现在AnnotationInvocationHandler类中一处readObject函数对它进行了调用。

注意:如果你的调用中没有出现这个类,你应当下载源码,jdk下载网页,下载后解压其中/src/share/classes/sun到自己jdk的src文件夹中

找到自己的文件夹,解压进去,

然后在IDEA中项目属性里添加源码路径,CTRL+shift+alt+s打开

ok,现在你已经可以找到我们的最后的入口函数了,可以发现readObject正好符合我们的调用方式,用构造的恶意序列化流进入这处readObject就可以完成CC1链了,但是有一个问题,AnnotationInvocationHandler类不是公开的,仅可在同一个包下可访问也就是在外面无法通过名字来调用,因此只可以用反射获取这个类。
这个类的构造方法为,也就是说需要一个注解类和一个Map类,这个注解类的选取是有讲究的,后面再探,先设置为Override注解
1 | AnnotationInvocationHandler(Class<? extends Annotation> type, |
然后我们通过反射调用readObject方法,
1 | Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); |
于是乎,我们现在的代码应该写成下面这样:记得抛出异常和写好序列化和反序列化函数。
1 | public static void main(String[] args) throws Exception{ |
链子中的小巧思
可以发现写完这段运行之后并没有弹出计算器,其实一条可用的序列化链的构造是非常精心的,里面有许多弯弯绕绕,并不是应该这么写所以就这么写,每一个环节都有其出现的必要性。这里我们正向调试一番,看看问题到底出现在哪里:
首先在序列化函数中的readObject方法处打上断点,

你可以选择一步一步调试去分析整个反序列化的过程,这里我就不单步调试了,我们直到obj的最后一层是一个AnnotationInvocationHandler的实例,那么我们就直接在它的readObject函数打上断点,直接恢复项目运行,可以发现到了我们的断点处,然后可以单步调试了

发现在下面判断memberType时,memberType==null,直接就跳转到了函数末尾,所以根本就没有执行之后的序列化过程。

ok,这里出问题,那么怎么解决呢,找到memberTypes从哪里来的,在上面几行代码中,
1 | annotationType = AnnotationType.getInstance(type); |
这里就和我说的在构造该对象时,注解类的问题了,Override根本就没有成员,所以为null,所以构造的时候注解的选取不是随意的。
1 |
|
所以我们将源代码的map的key改为Target注解中的成员变量value,构造时传入Target类
1 | // 第二层 |
重新调试到此处,发现已经成功绕过第一层if了

此时我们运行整个代码发现报错了,这是为什么呢,其实是因为Runtime类没有继承serialize接口,所以runtime对象不能被序列化,自然也不能被反序列化。这里解决方法,非常巧妙,虽然Runtime对象不可以被序列化,但是Class对象可以被序列化。那么结合InvokerTransformer和反射可以写出下面的代码:
其实非常好理解,比如第一个,就是执行getDeclaredMethod找到getRuntime函数。
实现逻辑,构造对象的参数是getDeclaredMethod和getRuntime的对象,执行它的transform方法,在这个方法中,Runtime.class的作用是通过getMethod找到它的getDeclaredMethod函数,然后将getRuntime作为参数。
这整个语句的返回值就是getRuntime函数,即getDeclaredMethod的返回值。
1 | //Class clazz = Runtime.class; |
好了,解决了不能反序列化的问题,但是这样构造破坏了我们的序列化链,之前只有一个InvokerTransformer对象,现在变成了3个,那么如何解决这个问题?
这时需要一个新的函数,聚焦到org.apache.commons.collections.functors包下面的ChainedTransformer类。
这个类存在transform方法可以帮我们遍历InvokerTransformer,并且循环调用遍历的InvokerTransformer的transform方法
1 | public ChainedTransformer(Transformer[] transformers) { |
所以只需要把上面的代码构造成一个Transformer数组即可,
1 | Transformer[] transformers = new Transformer[]{ |
到这里,Runtime类不能反序列化的问题就解决了。
但是,有一个致命问题,在这样使用ChainTransformer时,我们的Runtime.class也就是参数该如何传入?
会不会有一个Transformer的transform函数就是返回参数本身呢,答案是有的😋
刚刚好到org.apache.commons.collections.functors包下的ConstantTransformer类。它里面的transform就是返回我们传入的对象,如果我们传入Runtime.class,那返回的也即是Runtime.class。我们可以利用ConstantTransformer类解决问题了。
ok,到此为止,我们可以编写完整的payload了。
完整Exp
完整项目可以直接从我的测试代码仓库拉取
1 | public static void main(String[] args) throws Exception{ |
漏洞触发场景
在java编写的web应用与web服务器间java通常会发送大量的序列化对象例如以下场景:
HTTP请求中的参数,cookies以及Parameters。
RMI协议,被广泛使用的RMI协议完全基于序列化
JMX 同样用于处理序列化对象
自定义协议 用来接收与发送原始的java对象
漏洞挖掘
确定反序列化输入点
首先应找出readObject方法调用,在找到之后进行下一步的注入操作。一般可以通过以下方法进行查找:
源码审计:寻找可以利用的“靶点”,即确定调用反序列化函数readObject的调用地点。
对该应用进行网络行为抓包,寻找序列化数据,如wireshark,tcpdump等
注: java序列化的数据一般会以标记(ac ed 00 05)开头,base64编码后的特征为rO0AB。
再考察应用的Class Path中是否包含Apache Commons Collections库
生成反序列化的payload
提交我们的payload数据
修复
- 禁止JVM执行外部命令
- TransformedMap 类去掉了 Map.Entry 的 setValue 方法