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);)
- There are some static code blocks in the defaultjsonparser class. When instantiating it, the static code block is called first
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
- Next, call the defaultjsonparser's constructor
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();
}
}
- Enter defaultjsonparser initialization
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
- First, build a jsonobject object
- Enter into parseobject
- Because the current token is 12, skip the if condition all the way to the else block
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
- Resolve the key name first in while
- The next value begins with '{‘
- Because it starts with '{', which means it's a nested JSON
- After that, call parseobject again at line 537 and pass the key name into the parameter to get the value of the corresponding key
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
- Next, the nested JSON is interesting
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