IMCAFS

Home

analysis of the latest fastjson deserialization vulnerability

Posted by punzalan at 2020-04-04
all

Preface

It's a little too much. It may be a bit verbose for the masters, but I'll understand after I finish it

POC

NewPoc.java

rmiServer.java

Vulnerability triggering process

At the json.parse breakpoint, step into the JSON class and start the parsing process of the fastjson library

After several parse functions (in Java, a function is uniquely determined by the function name and the function parameter, that is, the function signature in Java), although the same function is parse, the function parameters are different, so it enters into different functions

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

In the last parse, we really enter the process of JSON parsing

0x01 overview

We don't have to hurry to follow, because Java's call chain is generally deep, so let's first look at the overall process, to determine what these functions are used for, first look at the macro, and then follow it step by step to see what it is doing

DefaultJSONParser parser = new DefaultJSONParser(text, config, features);

Initializing a default JSON parser

Object value = parser.parse();

Call the parse function of the JSON parser to parse the JSON

parser.handleResovleTask(value);

This function doesn't know what to do for the time being. Put it first

parser.close();

Turn the parser off

return value;

Returns the result of JSON parsing

It can be seen that what we really need to pay attention to is actually the generation of defaultjsonparser and the processing of our input JSON data by defaultjsonparser

0x02 initialize JSON parser (defaultjsonparser = new defaultjsonparser (text, config, features);)

static { Class?[] classes = new Class[]{Boolean.TYPE, Byte.TYPE, Short.TYPE, Integer.TYPE, Long.TYPE, Float.TYPE, Double.TYPE, Boolean.class, Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class, BigInteger.class, BigDecimal.class, String.class}; Class[] var1 = classes; int var2 = classes.length; for(int var3 = 0; var3 var2; ++var3) { Class? clazz = var1[var3]; primitiveClasses.add(clazz); } }

This is to add many native classes to the primitiveclasses collection for later use

public DefaultJSONParser(String input, ParserConfig config, int features) { this(input, new JSONScanner(input, features), config); }

Here we initialize jsonscaner to register our JSON data

public JSONScanner(String input, int features) { super(features); this.text = input; this.len = this.text.length(); this.bp = -1; this.next(); if (this.ch == 'ufeff') { this.next(); } } public DefaultJSONParser(Object input, JSONLexer lexer, ParserConfig config) { this.dateFormatPattern = JSON.DEFFAULT_DATE_FORMAT; this.contextArrayIndex = 0; this.resolveStatus = 0; this.extraTypeProviders = null; this.extraProcessors = null; this.fieldTypeResolver = null; this.autoTypeAccept = null; this.lexer = lexer; this.input = input; this.config = config; this.symbolTable = config.symbolTable; int ch = lexer.getCurrent(); if (ch == '{') { lexer.next(); ((JSONLexerBase)lexer).token = 12; } else if (ch == '[') { lexer.next(); ((JSONLexerBase)lexer).token = 14; } else { lexer.nextToken(); } }

Some member variables are registered here, and our JSON data starts with '{', so the token is set to 12

At this point, the initialization of defaultjsonparser is over. You can see that in this function, the main task is to initialize the whole parsing context, register the JSON data and the classes needed later, so as to use them later

0x03 start the JSON parsing process (object value = parser. Parse();)

Enter the parse method of defaultjsonparser, and the real JSON parsing of fastjson starts

public Object parse() { return this.parse((Object)null); } public Object parse(Object fieldName){ JSONLexer lexer = this.lexer; switch(lexer.token()) { ...... case 12: JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField)); return this.parseObject((Map)object, fieldName); ...... }

The code is relatively long. In the initialization of defaultjsonparser above, set our token value to 12, so enter the case 12 condition

ParseContext context = this.context; try { Map map = object instanceof JSONObject ? ((JSONObject)object).getInnerMap() : object; boolean setContextFlag = false; while(true) { lexer.skipWhitespace(); char ch = lexer.getCurrent(); if (lexer.isEnabled(Feature.AllowArbitraryCommas)) { while(ch == ',') { lexer.next(); lexer.skipWhitespace(); ch = lexer.getCurrent(); } } ...... } }

This is a very large while loop, where you can analyze the JSON String Syntax and make various judgments

0x04 huge while

The parsing process of JSON is character by character, which is similar to the judging mechanism of JS

You can think of parseobject as a magic box. It will parse the JSON you passed in, such as {'xxx': 'xxx'}, and store it in a map according to the key value. If you pass in {'xxx': In the form of {'xxx': 'xxx'}}, since the value is also the form of a key value pair, parseobject will be called again, and the final result will become the structure of nesting a map in a map

JSON in Java is not as simple as JSON in PHP. The most important thing about JSON in Java is that it is a very important means for Java to realize deserialization and serialization. Therefore, in fastjson, some keys are defined. If these keys appear, the corresponding value of the key will not be simply considered as a string

We extract this value:

Two conditions are judged here:

1 key = = json.default'type'key determines whether it is a default special key name

2! Whether lexer.isenabled (feature. Disablespecialkeydetect) turns off detection of special key names (on by default)

This is what the patch added after the first occurrence of fastjson deserialization. It limits the white list and blacklist of the deserialized classes. This time, including the previous fastjson deserialization vulnerability, it is caused by the problem of checkautotype

0x05 checkAutoType

Here, we check whether the deserialized class is in the blacklist, which is the following paragraph:

Here, Ali plays a trick. In order to prevent attackers from getting the black-and-white list of forbidden classes, Ali does not directly compare the class name, but the hash of a string of classes, so it is not easy to know what the black-and-white list of Ali is (of course, it has been made by some big guys)

We can see that if the class we passed in conforms to the hash defined by this.denyhashcodes, the class will not be deserialized. Of course, the class com.sun.rowset.jdbcrowsetimpl must be prohibited

Go back to our previous process, because there is java.lang.class in identityhashmap, that is to say, this class is considered safe, so in clazz = this.deserializers.findclass (typename); this place directly obtains the java.lang.class object

And then

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

It directly returns the java.lang.class object

0x06 back to defaultjsonparser

After checkautotype detection, our clazz variable becomes a java.lang.class object

Call objectdeserializer deserializer = this.config.getdeserializer (clazz)

ObjectDeserializer deserializer = this.config.getDeserializer(clazz) ObjectDeserializer deserializer = this.config.getDeserializer(clazz)

ObjectDeserializer derializer = (ObjectDeserializer)this.deserializers.get(type);

ObjectDeserializer derializer = (ObjectDeserializer)this.deserializers.get(type); ObjectDeserializer derializer = (ObjectDeserializer)this.deserializers.get(type);

Here we get the corresponding deserilizer: misccodec object

After that, the deserilize method of the MiscCodec object was called.

Enter this method:

Then call parser.parse ().

Come here

Here through string stringliteral = lexer. Stringval(); get the value corresponding to Val: com.sun.rowset.jdbcrowsetimpl, and finally return the string

Later, make a series of judgments on our java.lang.class, and enter

Follow up and enter the loadclass function all the way

In short, this function determines whether there is the classname passed in the map of the predefined class. If not, add it in

This map is the map in checkautotype, which is the trigger point of the vulnerability. By loading the class com.sun.rowset.jdbcrowsetimpl into the map, the verification of checkautotype is bypassed and deserialization is caused

0x07 new round of JSON parsing

As mentioned earlier, the large while is used to parse JSON. After the first key value pair is parsed, continue to parse the second key value pair, that is:

There is nothing to say. The most important thing is this step:

We follow up and come to this function

public static Class? getClassFromMapping(String className) { return (Class)mappings.get(className); }

The values in maps are:

In the first part of JSON parsing, we successfully added com.sun.rowset.jdbcrowsetimpl (see line 63), so we can directly return the class of jdbcrowsetimpl

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

Because the value of clazz is returned in the map obtained above, the com.sun.rowset.jdbcrowsetimpl class is directly returned here. There is no black-and-white list detection below, and the checkautotype is successfully bypassed

0x08 JdbcRowSetImpl deserialization

Finally, the deserializer is obtained at this place, and the jdbcrowsetimpl class is deserialized through the deserializer. Finally, the lookup is successfully called for JNDI injection

Because this place involves Java ASM generating bytecode directly. I can't understand it, so I have to skip it and learn later, TCL

Finally, give a call chain after ASM