作者:天融信阿尔法实验室
原文链接:https://mp.weixin.qq.com/s/qlg3IzyIc79GABSSUyt-OQ

1. Jdk7U21漏洞简介

谈到java的反序列化,就绕不开一个经典的漏洞,在ysoserial 的payloads目录下 有一个jdk7u21,以往的反序列化Gadget都是需要借助第三方库才可以成功执行,但是jdk7u21的Gadget执行过程中所用到的所有类都存在在JDK中,JRE版本<=7u21都会存在此漏洞

2. Jdk7u21漏洞原理深入讲解

2.1 漏洞执行流程

整体的恶意对象的封装整理成了脑图,如下图所示

这里用到了TemplatesImpl对象来封装我们的恶意代码,其封装和代码执行的流程在《Java 反序列化系列 ysoserial Hibernate1》中针对这种利用已经进行了详细的讲解,基本原理是通过动态字节码生成一个类,该类的静态代码块中存储有我们所要执行的恶意代码,最终通过TemplatesImpl.newTransformer()实例化该恶意类从而触发其静态代码块中的恶意代码,关于TemplatesImpl的详细分析可以去查看java 反序列化系列 Hibernate1中去学习了解。

首先最外层的是LinkedHashSet 类,看过该类源码的同学应该都清楚,该类其实是基于HashMap实现的。我们首先来看LinkedHashSet的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);
        }
    }
}

该方法最后可以看到有一个for循环,将LinkedHashSet对象在序列化时一个一个被序列化的元素在反序列化回来。该循环体中有一行代码 map.put(e,PRESENT) 这里的map变量指向的是一个LinkedHashMap对象,PRESENT常量的值是一个空的Object对象由下图可知

此时的变量e指向的是我们实现封装进LinkedHashSet里的TemplatesImpl对象,里面存有我们的恶意代码

接下来我们来看LinkedHashMap.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;
}

大概流程就是判断其key值的hash是否一致 如果不一致则证明是一个新的元素从而加入到当前的HashMap对象中,如果hash一致则进行判断该元素是否存在于当前的HashMap中如果存在则返回oldValue,如果不存在则加入当前HashMap对象中。

这里核心关键点就是如何让程序执行到key.equals,此时的key指向的是我们通过动态代理生成的Proxy对象,我们知道调用Proxy对象的任何方法,本质上都是在调用,InvokcationHandler 对象中被重写的invoke方法。因为生成Proxy对象时传入的参数是InvokcationHandler的子类AnnotationInvocationHandler,所以自然要调用AnnotationInvocationHandler.invoke()方法。

我们来看该方法的具体实现

通过观察代码我们可以看到接下来会调用equalsImpl()方法,传入的var3参数是封装了我们恶意代码的TemplatesImpl对象

private Boolean equalsImpl(Object var1) {
    if (var1 == this) {
        .....
    } 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);
       ......

在这里我们可以看到有这么一行代码var8 = var5.invoke(var1);这里就会调用TemplatesImpl.newTransformer()从而实例化恶意类,这里的var1我们清楚是我们传递进来的TemplatesImpl对象,但是var5的结果是怎么来的还需要分析一下。

从代码中可以看到Method var5 = var2[var4]; var4=0 而var2= this.getMemberMethods();

跟入getMemberMethods()方法

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

该方法会循环获取AnnotationInvocationHandler.type中的方法,我们可以看到type对象指向了一个Templates.class对象

Templates是一个接口,该接口中只有两个抽象方法

所以getMemberMethods()方法返回的结果就是两个Method对象,一个是newTransformer的Method对象,一个是getOutputProperties的Method对象,这样我们是如何通过反射调用的TemplatesImpl.newTransformer()方法的逻辑就清晰了

2.2 如何构造满足条件的hash值

但是有一个问题还没有解决,那就是刚才所讲的所有代码逻辑,都要在key.equals(k)可以执行的前提下才可以,那么究竟怎样才能执行key.equals(k)呢,我们来重新看一遍LinkedHashMap.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))) {
        ......
        }

可以看到 需要满足一些条件 才可以执行到key.equals(k)接下来就详细讲一讲如何才能满足以上这些条件,这是笔者个人觉得整个漏洞利用中最难也是最让人拍案叫绝的思路。

首先第一次调用map.put()时传入的参数e是我们封装了恶意代码的TemplatesImpl对象,另一个参数就是一个空的Object对象

由下图代码可知,我们需要计算出key 也就是恶意TemplatesImpl对象的hash值

深入看hash方法的实现

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

这里调用TemplatesImpl.hashCode()方法来得出hash值然后进行固定的异或操作,得出的最终结果进行返回,下面的截图中就是此次运算得出的hash值

接下来通过indexFor()函数 得到其hash索引 这里返回的索引值是12,并将值符給变量i 这里传入的table.legth,table是一个Entry数组,用来存放我们通过map.put()传入的键值对,并作为后续判断新传入的键值对和旧键值对是否重复的依据

/**
 * Returns index for hash code h.
 */
static int indexFor(int h, int length) {
    return h & (length-1);
}

接着就开始了第一次判断,首先当前table变量指向的Entry对象是空的,所以自然e 为null 在这里就不符合了,所以循环体内的代码不会执行

for (Entry<K,V> e = table[i]; e != null; e = e.next

跳过for循环体,然后计数器自增,并将此TemplatesImpl对象本身,还有其Hash值和索引放入到之前说到的table变量中。

接下来就开始第二次循环了,第二次传入的key就是触发TemplatesImpl.newTransformer()的媒介 Proxy对象了这个对象里有我们特意封装进去的AnnotationInvocationHandler对象。

接下来问题就来了首先for循环中要满足e不为空,这就要求这次循环并计算Proxy对象从而得出的Hash值和Hash索引必须和上一次循环中的TemplatesImpl对象相同,这样才能在Entry<K,V> e = table[i]这一步中,从table中取到对应索引的对象赋值給e,从而满足e != null

for (Entry<K,V> e = table[i]; e != null; e = e.next)

那怎么才能让两个连类型都不相同的对象通过运算却能得出一样的hash值呢?接下载关键点就来了,也就是我们为什么生成Proxy对像时要传入AnnotationInvocationHandler对象。

在计算Proxy对象的hash值的时候 我们看到最终是通过调用Proxy.hashCode()来计算hash值

Proxy是一个动态代理对象,所以经过对调用方法名称的判断,最终调用AnnotationInvocationHandler.hashCodeImpl()方法

以下是hashCodeImpl方法的实现,此时的var2是一个Iterator对象,用来遍历memberValues对象中存储的键值对

private int hashCodeImpl() {
    int var1 = 0;

    Entry var3;
    for(Iterator var2 = this.memberValues.entrySet().iterator(); 
        var2.hasNext(); 
        var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())) {
        var3 = (Entry)var2.next();
    }

    return var1;
}

可以看到memberValues中只有一个键值对就是,就是我们在初期通过反射生成AnnotationInvocationHandler对象时传入的HashMap对象中的那个键值对 key是一个字符串"f5a5a608" Value值适合第一次循环时用来计算hash值的同一个TemplatesImpl对象

我们在看一看var3此时的值。

AnnotationInvocationHandler计算hash最关键的是这一段代码。简单来说就是127乘var3 key的hash值,然后和var3的value值的hash值进行异或操作

var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())

下面贴出memberValueHashCode方法的关键代码,返回var3的value值也就是TemplatesImpl对象的Hash值。

private static int memberValueHashCode(Object var0) {
    Class var1 = var0.getClass();
    if (!var1.isArray()) {
        return var0.hashCode();

至此所得到的结果都是和第一次循环时得到的Hash值相同,但接下来就要解决如果在经过与127 * ((String)var3.getKey()).hashCode()进行异或操作后,保持结果不变。

我们知道0和任何数字进行异或,得到的结果都是被异或数本身。所以我们要让127 * ((String)var3.getKey()).hashCode()的结果等于0 也就是(String)var3.getKey()).hashCode()的值要为零

还记得我们var3的 key是什么么?是一个字符串 值为"f5a5a608" 这个字符串非常有意思我们看一下这个字符串的hash值是多少

结果是0,完全符合我们的要求,这样127乘以0自然结果是0,0在同TemplatesImpl对象的hash值进行异或,得到的结果自然也是TemplatesImpl对象的hash值本身。这样就符合我们的要求。通过了LinkedHashMap.put方法中的for循环的判断,由于hash值相同,所以计算出的索引相同,e的值就为之前的TemplatesImpl对象,所以e不为null 结果为true

for (Entry<K,V> e = table[i]; e != null; e = e.next) 

接下来好要通过if 判断中的前两个条件,因为&& 和|| 有短路效果,所以这三个条件我们要符合e.hash == hash为true (k = e.key) == key为flase

if (e.hash == hash && ((k = e.key) == key || key.equals(k)))

首先e.hash == hash是将第一次循环时的TemplatesImpl对象的hash取出同第二次循环时TemplatesImpl对象的hash进行对比,本来就都是同一个对象,所以自然时相同的,所以结果为true

(k = e.key) == key 将第一次循环时的key取出和第二次循环时的key做比对看是否相同,第一次循环的key是TemplatesImpl对象,而第二次循环时key时Proxy对象,所以结果为flase

如此这般,我们就通过了前两个判断条件,接下来自然就会执行key.equals(k)从而调用TemplatesImpl.newTransformer()方法并最终触发我们的恶意代码

至此jdk7u21漏洞原理分析完毕

3. 总结

此次Jdk7u21 payload中作用到的所有类,均存在于JDK自身的代码中,无需再调用任何第三方jar包,所以当时爆出漏洞时影响极大。只要目标系统中使用的jdk版本并存在反序列化数据交互点就会存在远程代码执行漏洞。漏洞的触发点在LinkedHashSet,其实我们看代码的时候可以看到LinkedHashSet里面的方法都是调用了其父类HashSet中的方法,但是之所以不直接用HashSet的原因是LinkedHashSet里数据的下标和我们插入时的顺序一样,而HashSet顺序就不一样了。通过Hash值的匹配,然后执行到key.equals(k)最终执行到TemplatesImpl.newTransformer()方法


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1224/