Java原生反序列化 JDK7u21
影响范围:
这条链子是java的原生反序列化链子,Java的版本是多个分支同时开发的,并不意味着JDK7的所有东西都一定比JDK6新,所以JDK6并非所有版本都受影响
概是6u51的时候修复了这个漏洞,但是这个结论不能肯定,因为免费用户下载不到这个版本。
JDK8在发布时,JDK7已经修复了这个问题,所以JDK8全版本都不受影响。
Gadget
LinkedHashSet.readObject()
LinkedHashSet.add()
...
TemplatesImpl.hashCode() (X)
LinkedHashSet.add()
...
Proxy(Templates).hashCode() (X)
AnnotationInvocationHandler.invoke() (X)
AnnotationInvocationHandler.hashCodeImpl() (X)
String.hashCode() (0)
AnnotationInvocationHandler.memberValueHashCode() (X)
TemplatesImpl.hashCode() (X)
Proxy(Templates).equals()
AnnotationInvocationHandler.invoke()
AnnotationInvocationHandler.equalsImpl()
Method.invoke()
...
TemplatesImpl.getOutputProperties()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
ClassLoader.defineClass()
Class.newInstance()
...
MaliciousClass.<clinit>()
...
Runtime.exec()
equalsImpl
JDK7u21的核心点就是sun.reflect.annotation.AnnotationInvocationHandler
,没错就是之前CC1用到的那个入口。但当时只用到了这个类会触发Map#put 、Map#get 的特点。
构造方法
又贴了一次,记住这俩参数,一个是Class<? extends Annotation>一个是Map
private final Class<? extends Annotation> type;
private final Map<String, Object> memberValues;
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
Class<?>[] superInterfaces = type.getInterfaces();
if (!type.isAnnotation() ||
superInterfaces.length != 1 ||
superInterfaces[0] != java.lang.annotation.Annotation.class)
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
this.type = type;
this.memberValues = memberValues;
}
这个equalsImpl是7u21的核心,就像上一次学的PQ一样
下面看看源码
private Boolean equalsImpl(Object o) {
if (o == this)
return true;
if (!type.isInstance(o))
return false;
for (Method memberMethod : getMemberMethods()) {
String member = memberMethod.getName();
Object ourValue = memberValues.get(member);
Object hisValue = null;
AnnotationInvocationHandler hisHandler = asOneOfUs(o);
if (hisHandler != null) {
hisValue = hisHandler.memberValues.get(member);
} else {
try {
hisValue = memberMethod.invoke(o);
} catch (InvocationTargetException e) {
return false;
} catch (IllegalAccessException e) {
throw new AssertionError(e);
}
}
if (!memberValueEquals(ourValue, hisValue))
return false;
}
return true;
}
private Method[] getMemberMethods() {
if (memberMethods == null) {
memberMethods = AccessController.doPrivileged(
new PrivilegedAction<Method[]>() {
public Method[] run() {
final Method[] mm = type.getDeclaredMethods();
validateAnnotationMethods(mm);
AccessibleObject.setAccessible(mm, true);
return mm;
}
});
}
return memberMethods;
}
private transient volatile Method[] memberMethods = null;
getMemberMethods函数是分开写的,作用就是反射获得this.type的所有方法,调用点是 for (Method memberMethod : getMemberMethods())
AnnotationInvocationHandler hisHandler = asOneOfUs(o);
尝试将传入对象 o
转换为 AnnotationInvocationHandler
类型,以便直接获取其 memberValues
。
如果转换失败,通过反射调用该方法来获取 hisValue
,这里的hisValue = memberMethod.invoke(o);
就是我们的调用点
假设this.type 是Templates类,则势必会调用到其中的newTransformer() 或getOutputProperties()方法,进而触发任意代码执行。
所以这里构造的AnnotationInvocationHandler的this.type 是Templates类,还缺一个this.memberValues
如何调用equalsImpl
equalsImpl 是一个私有方法,在AnnotationInvocationHandler#invoke 中被调用,我们之前也利用过这个方法,在CC1 lazymap那个链里用动态代理。
在使用java.reflect.Proxy 动态绑定一个接口时,如果调用该接口中任意一个方法,会执行到InvocationHandler#invoke 。执行invoke时,被传入的第一个参数是这个proxy对象,第二个参数是 被执行的方法名,第三个参数是执行时的参数列表。
我们看看它的invoke方法:
public Object invoke(Object proxy, Method method, Object[] args) throws RuntimeException {
String member = method.getName();
Class<?>[] paramTypes = method.getParameterTypes();
// Handle Object and Annotation methods
if (member.equals("equals") && paramTypes.length == 1 &&
paramTypes[0] == Object.class)
return equalsImpl(args[0]);
//......
可见,当方法名等于“equals”,且仅有一个Object类型参数时,会调用到equalImpl 方法。
所以,现在的问题变成,我们需要找到一个方法,在反序列化时对proxy调用equals方法。
找到equals方法调用链
比较Java对象时,我们常用到两个方法:
- equals
- compareTo
任意Java对象都拥有equals 方法,它通常用于比较两个对象是否是同一个引用;
一个常见的会调用equals的场景就是集合set。set中储存的对象不允许重复,所以在添加对象的时候,势必会涉及到比较操作。
我们查看HashSet的readObject方法:
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in any hidden serialization magic
s.defaultReadObject();
// Read in HashMap capacity and load factor and create backing HashMap
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));
// Read in size
int size = s.readInt();
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
E e = (E) s.readObject();
map.put(e, PRESENT);
}
}
这里使用了一个HashMap,将对象保存在HashMap的key处来做去重,这里是调用equal的关键。
为了触发比较操作,我们需要让比较与被比较的两个对象的哈希相同,这样才能被连接到同一条链表上,才会进行比较。
跟进下HashMap的put方法:
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) ,触发前面说过的代码执行。
也就是说:
我们需要传入不小于两个对象(两个就行),并且要有对象和先前传入的对象计算出的hash相同,这样才会调用key.equals(k)
那么这两个对象咋搞呢?
我们需要让proxy调用equals,并且proxy里的东西基本确定好了this.type是tempslate,还剩下this.memberValues,是一个Map
那么问题就变成,==我们控制proxy的这个Map使第二个对象(就是proxy)的hash和传入的第一个东西的hash相等==
这里要具体进去看hash是咋算的
巧妙的Magic Number
计算“哈希”的主要是下面这两行代码:
int hash = hash(key);
int i = indexFor(hash, table.length);
将其中的关键逻辑提权出来,可以得到下面这个函数:
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 ,进而调用到AnnotationInvocationHandler#hashCodeImpl ,我们看看这个方法:
private int hashCodeImpl() {
int result = 0;
for (Map.Entry<String, Object> e : memberValues.entrySet()) {
result += (127 * e.getKey().hashCode()) ^
memberValueHashCode(e.getValue());
}
return result;
}
- 当memberValues 中只有一个key和一个value时,该哈希简化成(127 * key.hashCode()) ^value.hashCode()
- 当key.hashCode() 等于0时,任何数异或0的结果仍是他本身,所以该哈希简化成value.hashCode() 。
- 当value 就是TemplateImpl对象时,这两个哈希就变成完全相等
最终Map里放置提前构造好的key:map.put(zeroHashCodeStr, templates)
,对于 0 ^ x
(x
是任意一个数)的异或运算,结果是 x
(也就是这里的templates)。
那么计算proxy对象的hash就是计算templates的hash,那我第一个对象也传这个templates,他俩就相等了。
所以,整个利用的过程就清晰了,按照如下步骤来构造:
首先生成恶意TemplateImpl 对象
实例化AnnotationInvocationHandler 对象
- 它的type属性是一个TemplateImpl类
- 它的memberValues属性是一个Map,Map只有一个key和value,key是字符串f5a5a608 , value是前面生成的恶意TemplateImpl对象
对这个AnnotationInvocationHandler 对象做一层代理,生成proxy对象
实例化一个HashSet,这个HashSet有两个元素,分别是:
-
TemplateImpl对象
-
proxy对象
将HashSet对象进行序列化
调试
配置idea
首先配置idea jdk7u21的环境,下载jdk就不说了,项目结构里导入新的sdk,设置好语言级别
设置-java编译器里设置好字节码版本
接下来就可以调试了
调试开始
这样,反序列化触发代码执行的流程如下:
触发HashSet的readObject方法,这个hashset我们丢进去两个对象,一个templates和一个proxy
这里进行遍历,先把templates put进去
hash()函数计算hash,但是tempslate的hashCode是native的不能左右
这里也不会进入那个for循环因为table里还没有东西,也就是table是null
第二次对我们传入的proxy进行put,计算hash
h ^= k.hashCode();
里k是proxy,所以调用它的hashcode方法依然会进入这个代理类的invoke
调用它的hashCodeImpl函数
这里是我们提前构造好的key:map.put(zeroHashCodeStr, templates)
,对于 0 ^ x
(x
是任意一个数)的异或运算,结果是 x
。
所以这里我们传入的key的字符串算出来是0,异或得到的是tempslate,这就和第一步put进去的那个tempslate是一样的了
去重时计算HashSet中的两个元素的hashCode() ,因为我们的静心构造二者相等,进而触发equals() 方法
调用到AnnotationInvocationHandler#invoke
调用AnnotationInvocationHandler#equalsImpl 方法
equalsImpl 中遍历this.type 的每个方法并调用
因为this.type 是TemplatesImpl类,所以触发了newTransform() 或getOutputProperties()方法
到这里就结束了
修复
官方是这样修复的
https://github.com/openjdk/jdk7u/commit/b3dd6104b67d2a03b94a4a061f7a473bb0d2dc4e
在sun.reflect.annotation.AnnotationInvocationHandler 类的readObject函数中,原本有一个对this.type 的检查(我们这篇文档的type是tempslate),在其不是AnnotationType的情况下,会抛出一个异常。但是,捕获到异常后没有做任何事情,只是将这个函数返回了,这样并不影响整个反序列化的执行过程。
新版中,将return; 修改成throw new java.io.InvalidObjectException(“Non-annotation typein annotation serial stream”); ,这样,反序列化时会出现一个异常,导致整个过程停止
这个修复方式看起来击中要害,实际上仍然存在问题,这也导致后面的另一条原生利用链JDK8u20。