0x01 Introduction
Recently, I have learned about the Fastjson deserialization vulnerability. After watching videos and reading articles, I have just barely understood the principle. Here, I will briefly summarize the content I have learned, hoping it will be helpful for everyone to understand the Fastjson deserialization vulnerability. If there are any mistakes in the article, please criticize and correct them, and I will make the necessary corrections as soon as possible.
0x02 Prior knowledge
What is a JSON string?
A string like the following is called a JSON string, which is in the form of a dictionary.

{
"name":"BossFrank",
"age":23
{}
What is Fastjson?
An open-source Java utility library used to serialize Java objects into JSON strings or deserialize JSON strings into Java objects
Dynamic assignment of object properties
1. Call the getter/setter methods of the object
2. Reflection assignment
Security issues
FastJson introduces a feature that allows the object to be deserialized to be specified through the @type field, which is the fully qualified name of the object class. Due to this @type, during Fastjson deserialization, any deserialization class can be specified. If the user passes a malicious JSON string, security issues may arise.
FastJson serialization and deserialization functions
Serialization function: JSON.toJSONString(Object object)
Deserialization function: Fastjson deserialization uses the JSON.parseObject() and JSON.parse() methods.
parseObject(): Return JSON object
The object returned by the first parse is our ordinary Java object, which will then be logically judged and finally converted into a JSON object to be returned, so, whether @type is used to specify the object or not, the object returned will always be a JSON object.
There are two ways to obtain a regular object by calling parseObject, one is to pass the second parameter to parseObject, which is the class of the object to be returned.
In fact, the function called here is not the parseObject mentioned earlier, but a function with the same name.
The second method is to call the toJavaObject function after obtaining the JSON object.
Parse():Return the class object itself.
As can be seen, after specifying the type with @type, the returned object is a person object.
At the same time, we notice that when calling parse and parseObject, the getter and setter methods of the person class are automatically called. The reason for this will be known in the later process analysis. It was also found that there are differences in the getter/setter methods called by the two, that is, parseObject will call the getter and setter methods, while parse will only call the setter method.
The reason for the difference can be superficially understood as: parseObject will first create a common java class object, here is the person object, as we know before, to assign values to object properties, either reflection or set method, so we need to call the set method to assign properties to the person object, and because we need to convert it to a JSON object {"name":"mike","age":17} when returning, so we need to call the get method to get the value of each property}
0x03 Process Analysis
Set a breakpoint at the parseObject, follow in, and find that the parse function that only accepts a string is called
Continue to follow in, and then call parse(String text, int features), which means that both the parseObject function that only accepts a string and the parse function will finally call here
Here, a default JSON parser is instantiated, and then a parse function without parameters is called, continue to follow in, and finally we come to a parse function that accepts an Object type parameter, the logic in this function is to get the token, parse according to the type of the token, such as left curly brace, JSON array, left bracket, etc.
Since we are a JSON string, that is, the left curly brace, it will go to this part
Create a new JSON object and pass it to the parseObject(final Map object, Object fieldName) function in DefaultJSONParser, continue to follow in, and there are some boundary value checks at the beginning, and the core logic is here in the try block
Continue to go down, define a key, and then there are some if-else statements to get the value of the key
Then we will come to this part, where the key value is judged; in fact, the key value is JSON.DEFAULT_TYPE_KEY, which means that it will parse the Java object below, so it will go into this if statement
First, the class is loaded in the if statement, and then there are some if-else judgments, but the most important part is still here
We get the deserializer for the person class, and then use this deserializer to perform deserialization; here, the returned object is our person object, and we follow into the getDeserializer function
In this function, we first try to get an existing deserializer from the cache based on our type, but there is none here, so it will not return directly, and then we call a function with the same name getDeserializer((Class<?>) type, type) in ParserConfig, and we follow into it
Here is a blacklist; continue down after passing through some big and small if conditions, and finally come to here, creating a JavaBean deserializer, and follow in
First, we see there is a button asmEnable, which is set to true by default
Then there are some if conditions that can change the value of asmEnable, and then here, a new JavaBeanInfo is created. It is important to know that when creating the deserializer of the class, you need to understand some content inside the class first, such as fields, constructors, setter/getter functions, and then put these contents into JavaBeanInfo, and we follow in
Here we get all the fields and methods of the person class
Then continue down to here, briefly discuss the functions of these three loops, which are to obtain all the setter methods, fields, and getter methods that meet the conditions of this class
Let's briefly talk about the logic of the first loop, traversing all methods, with some if conditions at the beginning to find methods that meet the following conditions:
- The length of the method name is greater than 4
- Not a static function
- The return value is void
- The function accepts one parameter
- Starting with set
Then, cut off the field name after setxxx, convert it to lowercase, and then put the obtained information into the new FieldInfo and add it to the fieldList
The logic of the second loop is to add some fields such as public, private, and not existing in the list to the fieldList
The third loop is similar to the first, finding the method that meets the conditions and adding it to the fieldList, which is actually the getter method; the conditions to be met here are probably:
- The length of the method name is greater than 4
- Not a static function
- Starting with get
- The return value must be one of Collection, Map, AtomicBoolean, AtomicInteger, AtomicLong
- The function accepts a parameter of 0
- And the fields that have not been added to the FileList before, that is to say, there is no corresponding setter method
Then it will process the set as above,截取getxxx after the field name, convert it to lowercase, and add it to the fieldList
After processing these three loops, encapsulate this information into JavaBeanInfo and return it out, and then create and return the JavaBean deserializer based on JavaBeanInfo
We return all the way back to here
We follow the deserialze function to see how deserialization is performed, and find that only the value changes; the specific code execution cannot be debugged because the asmEnable button is turned on, and we return the temporary deserializer created by asm
If we want to debug, we need to return JavaBeanDeserializer, that is, we need to change the value of asmEnable to false. We can modify the configuration before deserialization to change the value of asmEnable
After debugging, we follow up on the deserialze function, bypass some if conditions, and here we get the fields added to the FieldList before
Then, here is the construction of the object
Continue to call the assignment function
Then, it will call the corresponding setter function and the corresponding functions with only getter, so the call to parseObject function at the beginning will call the setter function, and at the same time, we also know that the functions with only getter are also called in the process of returning the person object
For getter functions that correspond to setter functions, they are called after the person object is returned by calling the toJSON function, that is, at this place, so the setter function will not be automatically called when parse() is called
Let's write a malicious class to try
We can see that the calculator has popped up, and at the same time, we notice that it is also possible to have fields in the JSON string that do not correspond to setter/getter functions in the class or do not exist, because as we know, it will intercept the part behind the setter/getter function as the field in the processing of setter/getter function. Therefore, the variable name of the input JSON string uses the part behind the setter/getter function, and the value will be passed into the corresponding setter/getter function. In our actual use, it is unlikely that we can find malicious code directly in the setter function, usually starting from the setter/getter function as the entry point and looking for the deserialization chain.

评论已关闭