A fast JSON parser/generator for Java.一个Java语言实现的JSON快速解析/生成器。
官方描述:
Fastjson is a Java library that can be used to convert Java Objects into their JSON representation.
It can also be used to convert a JSON string to an equivalent Java object.
Fastjson can work with arbitrary Java objects including pre-existing objects that you do not have source-code of.
Fastjson Goals:
- Provide best performance in server side and android client
- Provide simple toJSONString() and parseObject() methods to convert Java objects to JSON and vice-versa
- Allow pre-existing unmodifiable objects to be converted to and from JSON
- Extensive support of Java Generics
- Allow custom representations for objects
- Support arbitrarily complex objects (with deep inheritance hierarchies and extensive use of generic types)
Fastjson是阿里巴巴开源的Apache顶级项目,在国内开发圈子中被使用广泛,由于它假定有序的解析特性,其相对于Jackson,性能会有一定的优势,不过个人觉得,相对于磁盘、网络IO等时间损耗,这样的提升对于大部分企业来讲,意义并不大。
因为Fastjson在国内被广泛使用,也就是说受众广,影响范围大,那么一但出现安全漏洞,被不法分子利用,将会对企业、用户造成极大损失。对于我们研究安全的人员来讲,研究分析Fastjson的源码,跟踪Fastjson安全漏洞,可以更好的挖掘出潜在的安全隐患,提前消灭它。
我曾经从网络上看到过很多对Fastjson分析的文章,但大部分都是对于新漏洞gadget chain触发的源码debug跟踪,缺少对于一些关键点代码的分析描述,也就是说,我看完之后,该不懂还是不懂,最后时间花出去了,得到的只是一个证明可用的exp...因此,我这篇文章,将针对Fastjson反序列化部分涉及到的关键点代码进行详细的讲解,其中一共四个关键点“词法解析、构造方法选择、缓存绕过、反射调用”,希望大家看完之后,将能完全搞懂Fastjson漏洞触发的一些条件以及原理。
词法解析是Fastjson反序列化中比较重要的一环,一个json的格式、内容是否能被Fastjson理解,它充当了最重要的角色。
在调用JSON.parse(text)对json文本进行解析时,将使用缺省的默认配置
public static Object parse(String text) {
return parse(text, DEFAULT_PARSER_FEATURE);
}
DEFAULT_PARSER_FEATURE是一个缺省默认的feature配置,具体每个feature的作用,我这边就不做讲解了,跟这一小节中的词法解析关联不大
public static int DEFAULT_PARSER_FEATURE;
static {
int features = 0;
features |= Feature.AutoCloseSource.getMask();
features |= Feature.InternFieldNames.getMask();
features |= Feature.UseBigDecimal.getMask();
features |= Feature.AllowUnQuotedFieldNames.getMask();
features |= Feature.AllowSingleQuotes.getMask();
features |= Feature.AllowArbitraryCommas.getMask();
features |= Feature.SortFeidFastMatch.getMask();
features |= Feature.IgnoreNotMatch.getMask();
DEFAULT_PARSER_FEATURE = features;
}
而如果想使用自定义的feature的话,可以自己或运算配置feature
public static Object parse(String text, int features) {
return parse(text, ParserConfig.getGlobalInstance(), features);
}
而这里,我们也能看到,传入了一个解析配置ParserConfig.getGlobalInstance(),它是一个默认的全局配置,因此,如果我们想要不使用全局解析配置的话,也可以自己构建一个局部的解析配置进行传入,这一系列的重载方法都给我们的使用提供了很大的自由度。
接着,我们可以看到,最终其实都走到这一步,创建DefaultJSONParser类实例,接着对json进行解析
public static Object parse(String text, ParserConfig config, int features) {
if (text == null) {
return null;
}
DefaultJSONParser parser = new DefaultJSONParser(text, config, features);
Object value = parser.parse();
parser.handleResovleTask(value);
parser.close();
return value;
}
然后我们跟进DefaultJSONParser构造方法中,可以看到其中调用了另一个重载的构造方法,我们可以重点关注第二个参数,也就是JSONScanner,它就是词法解析的具体实现类了
public DefaultJSONParser(final String input, final ParserConfig config, int features){
this(input, new JSONScanner(input, features), config);
}
看类注释,可以知道,这个类为了词法解析中的性能提升,做了很多特别的处理
//这个类,为了性能优化做了很多特别处理,一切都是为了性能!!!
/**
* @author wenshao[[email protected]]
*/
public final class JSONScanner extends JSONLexerBase {
...
}
在分析该词法解析类之前,我这里列出一些该类以及父类中变量的含义,有助于后续的代码分析:
text:json文本数据
len:json文本数据长度
token:代表解析到的这一段数据的类型
ch:当前读取到的字符
bp:当前字符索引
sbuf:正在解析段的数据,char数组
sp:sbuf最后一个数据的索引
hasSpecial=false:需要初始化或者扩容sbuf
可以从JSONScanner构造方法看到,text、len、bp、ch的大概意义,并且对utf-8 bom进行跳过
public JSONScanner(String input, int features){
super(features);
text = input;//json文本数据
len = text.length();//json文本数据长度
bp = -1;//当前字符索引
next();
if (ch == 65279) { // utf-8 bom
next();
}
}
接着在构造方法中,会调用next进行对text中一个一个字符的获取,可以看到bp值初始值为-1,在第一次调用时执行++bp变为0,即开始读取第一个字符的索引
public final char next() {
int index = ++bp;
return ch = (index >= this.len ? //
EOI //
: text.charAt(index));
}
再跟进DefaultJSONParser主要构造方法,其中lexer是词法解析器,这里我们跟踪得到其实现为JSONScanner,也就是我们前面所讲的那个,而input就是需要解析的json字符串,config为解析配置,最重要的就是symbolTable,我称之为符号表,它可以根据传入的字符,进而解析知道你想要读取的一段字符串
public DefaultJSONParser(final Object input, final JSONLexer lexer, final ParserConfig config){
this.lexer = lexer;//JSONScanner
this.input = input;//需要解析的json字符串
this.config = config;//解析配置
this.symbolTable = config.symbolTable;
//获取当前解析到的字符
int ch = lexer.getCurrent();
if (ch == '{') {
lexer.next();
((JSONLexerBase) lexer).token = JSONToken.LBRACE;
} else if (ch == '[') {
lexer.next();
((JSONLexerBase) lexer).token = JSONToken.LBRACKET;
} else {
lexer.nextToken(); // prime the pump
}
}
从上面的if、else流判断中可以知道,当开始头解析时,如果能解析到'{'或'[',就会赋值token,指明当前读到的token类型(在Fastjson中,会对json数据字符串一位一位的提取,然后比对,得出当前位置的词法类型,也即token),接着继续执行next()滑动到下一个字符。如果不能解析到'{'或'['开头,就会执行nextToken(),后续parse也会继续执行nextToken()
nextToken,顾名思义就是下一个token,其中实现逻辑会对字符一个一个的进行一定的解析,判断出下一个token类型
而整个Fastjson反序列化时,就是这样根据不断的next()提取出字符,然后判断当前token类型,接着根据token类型的不同,会有不一样的处理逻辑,表现为根据token类型做一定的数据字符串读取,并根据读取出来的字符串数据,进行反序列化成Java Object
我们回到nextToken中来:
public final void nextToken() {
sp = 0;
for (;;) {
pos = bp;
if (ch == '/') {
skipComment();
continue;
}
if (ch == '"') {
scanString();
return;
}
if (ch == ',') {
next();
token = COMMA;
return;
}
if (ch >= '0' && ch <= '9') {
scanNumber();
return;
}
if (ch == '-') {
scanNumber();
return;
}
switch (ch) {
case '\'':
if (!isEnabled(Feature.AllowSingleQuotes)) {
throw new JSONException("Feature.AllowSingleQuotes is false");
}
scanStringSingleQuote();
return;
case ' ':
case '\t':
case '\b':
case '\f':
case '\n':
case '\r':
next();
break;
case 't': // true
scanTrue();
return;
case 'f': // false
scanFalse();
return;
case 'n': // new,null
scanNullOrNew();
return;
case 'T':
case 'N': // NULL
case 'S':
case 'u': // undefined
scanIdent();
return;
case '(':
next();
token = LPAREN;
return;
case ')':
next();
token = RPAREN;
return;
case '[':
next();
token = LBRACKET;
return;
case ']':
next();
token = RBRACKET;
return;
case '{':
next();
token = LBRACE;
return;
case '}':
next();
token = RBRACE;
return;
case ':':
next();
token = COLON;
return;
case ';':
next();
token = SEMI;
return;
case '.':
next();
token = DOT;
return;
case '+':
next();
scanNumber();
return;
case 'x':
scanHex();
return;
default:
if (isEOF()) { // JLS
if (token == EOF) {
throw new JSONException("EOF error");
}
token = EOF;
eofPos = pos = bp;
} else {
if (ch <= 31 || ch == 127) {
next();
break;
}
lexError("illegal.char", String.valueOf((int) ch));
next();
}
return;
}
}
}
可以看到,就是前面所说的,根据当前读到的字符,而选择执行不同的字符串提取逻辑,我们这小节最核心的代码就位于scanString(),当判断当前字符为双引号时,则执行这个方法,我们看一下具体实现
public final void scanString() {
np = bp;
hasSpecial = false;
char ch;
for (;;) {
ch = next();
if (ch == '\"') {
break;
}
if (ch == EOI) {
if (!isEOF()) {
putChar((char) EOI);
continue;
}
throw new JSONException("unclosed string : " + ch);
}
if (ch == '\\') {
if (!hasSpecial) {
...扩容
}
ch = next();
switch (ch) {
case '0':
putChar('\0');
break;
case '1':
putChar('\1');
break;
case '2':
putChar('\2');
break;
case '3':
putChar('\3');
break;
case '4':
putChar('\4');
break;
case '5':
putChar('\5');
break;
case '6':
putChar('\6');
break;
case '7':
putChar('\7');
break;
case 'b': // 8
putChar('\b');
break;
case 't': // 9
putChar('\t');
break;
case 'n': // 10
putChar('\n');
break;
case 'v': // 11
putChar('\u000B');
break;
case 'f': // 12
case 'F':
putChar('\f');
break;
case 'r': // 13
putChar('\r');
break;
case '"': // 34
putChar('"');
break;
case '\'': // 39
putChar('\'');
break;
case '/': // 47
putChar('/');
break;
case '\\': // 92
putChar('\\');
break;
case 'x':
char x1 = next();
char x2 = next();
boolean hex1 = (x1 >= '0' && x1 <= '9')
|| (x1 >= 'a' && x1 <= 'f')
|| (x1 >= 'A' && x1 <= 'F');
boolean hex2 = (x2 >= '0' && x2 <= '9')
|| (x2 >= 'a' && x2 <= 'f')
|| (x2 >= 'A' && x2 <= 'F');
if (!hex1 || !hex2) {
throw new JSONException("invalid escape character \\x" + x1 + x2);
}
char x_char = (char) (digits[x1] * 16 + digits[x2]);
putChar(x_char);
break;
case 'u':
char u1 = next();
char u2 = next();
char u3 = next();
char u4 = next();
int val = Integer.parseInt(new String(new char[] { u1, u2, u3, u4 }), 16);
putChar((char) val);
break;
default:
this.ch = ch;
throw new JSONException("unclosed string : " + ch);
}
continue;
}
if (!hasSpecial) {
sp++;
continue;
}
if (sp == sbuf.length) {
putChar(ch);
} else {
sbuf[sp++] = ch;
}
}
token = JSONToken.LITERAL_STRING;
this.ch = next();
}
从代码中可以看到,当下一个字符遇到也是双引号时,就会结束scanString()循环,因为Fastjson认为读取到的字符串为空字符串,接着就是EOI的判断,不过我们这边关心的是'\\'的处理,因为在java中,只要是硬编码的字符串,对于一些转义字符,都需要使用'\'对其转义,那么'\\'其实正真就代表则字符'\'
接着看后面的switch-case处理,可以看出,其实就是对json数据字符串中'\0 \1 \2 \3 \4 \5 \6 \7 \b \t \n \u000B \f \F \r \" \\ \x \u'等双字节字符的处理,总结一下:
例:
\0 \1 \2 \3 \4 \5 \6 \7 \b \t \n \r \" \' \/ \\\
等,java字符串读入之后会变成两个字符,因此,fastjson会把它转换会单个字符
\f \F双字符都会转成单字符\f
\v双字符转成\u000B单字符
\x..四字符16进制数读取转成单字符
\u....六字符16进制数读取转成单字符
其实,对于\x和\u的词法处理,才是反序列化RCE中的核心,也就是我这一节词法解析中最想要讲的内容,我曾经遇到某道CTF题目,它在程序的filter层,对json数据的@type进行的过滤处理,而唯一能绕过它的办法,正恰恰是词法解析中对\x和\u也即16进制、Unicode的处理,通过对字符的十六进制转换或者Unicode处理,我们就可以通过以下的方式进行filter过滤器的绕过,而对于开发人员来说,也可以通过针对这种绕过方式进行filter的加强:
例:
@\u0074ype -> @type
@\x74ype -> @type
接着就是执行DefaultJSONParser.parse(),根据上一步中token的识别,进行解析处理
public Object parse(Object fieldName) {
final JSONLexer lexer = this.lexer;
switch (lexer.token()) {
case SET:
...HashSet集合的处理
case TREE_SET:
...TreeSet集合的处理
case LBRACKET:
...读取到"[",数组的处理
case LBRACE:
...读取到"{",对象解析的处理
case LITERAL_INT:
...
case LITERAL_FLOAT:
...
}
}
对象解析,反序列化的利用流程,基本都是走到LBRACE或LBRACKET中,进入对象的解析,而对象解析中,基本都会利用到符号表进行数据的提取:
public final Object parseObject(final Map object, Object fieldName) {
final JSONLexer lexer = this.lexer;
if (lexer.token() == JSONToken.NULL) {
lexer.nextToken();
return null;
}
if (lexer.token() == JSONToken.RBRACE) {
lexer.nextToken();
return object;
}
if (lexer.token() == JSONToken.LITERAL_STRING && lexer.stringVal().length() == 0) {
lexer.nextToken();
return object;
}
if (lexer.token() != JSONToken.LBRACE && lexer.token() != JSONToken.COMMA) {
throw new JSONException("syntax error, expect {, actual " + lexer.tokenName() + ", " + lexer.info());
}
ParseContext context = this.context;
try {
boolean isJsonObjectMap = object instanceof JSONObject;
Map map = isJsonObjectMap ? ((JSONObject) object).getInnerMap() : object;
boolean setContextFlag = false;
for (;;) {
lexer.skipWhitespace();
char ch = lexer.getCurrent();
if (lexer.isEnabled(Feature.AllowArbitraryCommas)) {
while (ch == ',') {
lexer.next();
lexer.skipWhitespace();
ch = lexer.getCurrent();
}
}
boolean isObjectKey = false;
Object key;
//判断到双引号开端的,利用符号表读取双引号闭合之间字符串,从而提取出key
if (ch == '"') {
key = lexer.scanSymbol(symbolTable, '"');
lexer.skipWhitespace();
ch = lexer.getCurrent();
if (ch != ':') {
throw new JSONException("expect ':' at " + lexer.pos() + ", name " + key);
}
} else if (ch == '}') {
...
}
...
//判断到key为@type,则进行checkAutoType,然后反序列化成Java Object
if (key == JSON.DEFAULT_TYPE_KEY
&& !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
...
clazz = config.checkAutoType(typeName, null, lexer.getFeatures());
...
ObjectDeserializer deserializer = config.getDeserializer(clazz);
Class deserClass = deserializer.getClass();
if (JavaBeanDeserializer.class.isAssignableFrom(deserClass)
&& deserClass != JavaBeanDeserializer.class
&& deserClass != ThrowableDeserializer.class) {
this.setResolveStatus(NONE);
} else if (deserializer instanceof MapDeserializer) {
this.setResolveStatus(NONE);
}
Object obj = deserializer.deserialze(this, clazz, fieldName);
return obj;
}
}
} finally {
this.setContext(context);
}
}
最后,总结一下:在反序列化RCE中,我们可以利用词法解析中\x\u的十六进制或者Unicode的处理,进行绕过一些检查机制。
构造方法的选择,我这一小节中,主要想讲解的是,在Fastjson反序列化中,针对每个class的特点,到底Fastjson会选择class的哪个构造方法进行反射实例化,到底是否可以不存在无参构造方法。
在上一节:
clazz = config.checkAutoType(typeName, null, lexer.getFeatures());
...
ObjectDeserializer deserializer = config.getDeserializer(clazz);
Class deserClass = deserializer.getClass();
if (JavaBeanDeserializer.class.isAssignableFrom(deserClass)
&& deserClass != JavaBeanDeserializer.class
&& deserClass != ThrowableDeserializer.class) {
this.setResolveStatus(NONE);
} else if (deserializer instanceof MapDeserializer) {
this.setResolveStatus(NONE);
}
Object obj = deserializer.deserialze(this, clazz, fieldName);
return obj;
在通过config.checkAutoType后会返回一个class,接着会根据class选择一个ObjectDeserializer,做Java Object的反序列化
而对于ObjectDeserializer的选择,很多class返回的都是一些没有利用价值的ObjectDeserializer:
deserializer
├─ASMDeserializerFactory.java
├─AbstractDateDeserializer.java
├─ArrayListTypeFieldDeserializer.java
├─AutowiredObjectDeserializer.java
├─ContextObjectDeserializer.java
├─DefaultFieldDeserializer.java
├─EnumDeserializer.java
├─ExtraProcessable.java
├─ExtraProcessor.java
├─ExtraTypeProvider.java
├─FieldDeserializer.java
├─FieldTypeResolver.java
├─JSONPDeserializer.java
├─JavaBeanDeserializer.java
├─JavaObjectDeserializer.java
├─Jdk8DateCodec.java
├─MapDeserializer.java
├─NumberDeserializer.java
├─ObjectDeserializer.java
├─OptionalCodec.java
├─ParseProcess.java
├─PropertyProcessable.java
├─PropertyProcessableDeserializer.java
├─ResolveFieldDeserializer.java
├─SqlDateDeserializer.java
├─StackTraceElementDeserializer.java
├─ThrowableDeserializer.java
└TimeDeserializer.java
以及一些根据JSONType注解等不太会存在安全漏洞的条件处理,而对于大部分可利用gadget chains的处理,最终都会走到
com.alibaba.fastjson.parser.ParserConfig#getDeserializer(java.lang.reflect.Type)
->
com.alibaba.fastjson.parser.ParserConfig#getDeserializer(java.lang.Class<?>, java.lang.reflect.Type)
->
com.alibaba.fastjson.parser.ParserConfig#createJavaBeanDeserializer
接着在其中,构建了JavaBeanInfo,在build方法中,会构建一个JavaBeanInfo对象,其中存储了选择哪个构造方法、字段信息、反射调用哪个方法等等,用于在最后的反射实例化时,做相应的处理
JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz
, type
, propertyNamingStrategy
,false
, TypeUtils.compatibleWithJavaBean
, jacksonCompatible
);
跟进JavaBeanInfo.build
JSONType jsonType = TypeUtils.getAnnotation(clazz,JSONType.class);
if (jsonType != null) {
PropertyNamingStrategy jsonTypeNaming = jsonType.naming();
if (jsonTypeNaming != null && jsonTypeNaming != PropertyNamingStrategy.CamelCase) {
propertyNamingStrategy = jsonTypeNaming;
}
}
可以看到,一开始就会从class中取JSONType注解,根据注解配置去选择参数命名方式,默认是驼峰式
接着会取出class的字段、方法、构造方法等数据,并且判断出class非kotlin实现时,如果构造方法只有一个,则调用getDefaultConstructor获取默认的构造方法
Class<?> builderClass = getBuilderClass(clazz, jsonType);
Field[] declaredFields = clazz.getDeclaredFields();
Method[] methods = clazz.getMethods();
Map<TypeVariable, Type> genericInfo = buildGenericInfo(clazz);
boolean kotlin = TypeUtils.isKotlin(clazz);
Constructor[] constructors = clazz.getDeclaredConstructors();
Constructor<?> defaultConstructor = null;
if ((!kotlin) || constructors.length == 1) {
if (builderClass == null) {
defaultConstructor = getDefaultConstructor(clazz, constructors);
} else {
defaultConstructor = getDefaultConstructor(builderClass, builderClass.getDeclaredConstructors());
}
}
从getDefaultConstructor的实现中可以清楚的看到,对于这个构造方法,如果它是无参构造方法或一参(自身类型)构造方法,则就会作为默认构造方法(反序列化对Java Object实例化时反射调用的构造方法),即defaultConstructor
static Constructor<?> getDefaultConstructor(Class<?> clazz, final Constructor<?>[] constructors) {
if (Modifier.isAbstract(clazz.getModifiers())) {
return null;
}
Constructor<?> defaultConstructor = null;
for (Constructor<?> constructor : constructors) {
if (constructor.getParameterTypes().length == 0) {
defaultConstructor = constructor;
break;
}
}
if (defaultConstructor == null) {
if (clazz.isMemberClass() && !Modifier.isStatic(clazz.getModifiers())) {
Class<?>[] types;
for (Constructor<?> constructor : constructors) {
if ((types = constructor.getParameterTypes()).length == 1
&& types[0].equals(clazz.getDeclaringClass())) {
defaultConstructor = constructor;
break;
}
}
}
}
return defaultConstructor;
}
若不存在这样特性的构造方法,则
boolean isInterfaceOrAbstract = clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers());
if ((defaultConstructor == null && builderClass == null) || isInterfaceOrAbstract) {
...抽象类或接口类
} else if ((factoryMethod = getFactoryMethod(clazz, methods, jacksonCompatible)) != null) {
...使用JSONCreator注解指定构造工厂方法
} else if (!isInterfaceOrAbstract) {
...
for (Constructor constructor : constructors) {
Class<?>[] parameterTypes = constructor.getParameterTypes();
...
boolean is_public = (constructor.getModifiers() & Modifier.PUBLIC) != 0;
if (!is_public) {
continue;
}
String[] lookupParameterNames = ASMUtils.lookupParameterNames(constructor);
if (lookupParameterNames == null || lookupParameterNames.length == 0) {
continue;
}
if (creatorConstructor != null
&& paramNames != null && lookupParameterNames.length <= paramNames.length) {
continue;
}
paramNames = lookupParameterNames;
creatorConstructor = constructor;
}
...
}
从上述代码中可以了解到,若非接口类,并且没有使用JSONCreator注解的话,则会对构造方法进行遍历选择,如果是以下三个class的话,会直接作为构造方法creatorConstructor
org.springframework.security.web.authentication.WebAuthenticationDetails
org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken
org.springframework.security.core.authority.SimpleGrantedAuthority
而非public的构造方法会被直接跳过。
接着使用com.alibaba.fastjson.util.ASMUtils#lookupParameterNames获取出所有的构造方法参数,若取出的参数为空,则也会直接跳过。
若前面遍历构造方法时,已有creatorConstructor选择,以及asm取出的参数数量<=构造方法参数数量,则也会直接跳过。
也就是说,除非是非公有public的,否则必然会选择一个creatorConstructor。
defaultConstructor:无参和一参(自身类型入参)构造方法
creatorConstructor:非defaultConstructor,遍历取最后的一个构造方法
总结的来讲:若可以找到defaultConstructor,则不再遍历选择creatorConstructor,否则必须遍历查找creatorConstructor。
缓存绕过,这是什么意思?我们上一小节已经详细的描述并总结了构造方法的选择逻辑。其中构造方法的选择分为defaultConstructor和creatorConstructor
我们这一节主要分析的关键点:缓存绕过,位于com.alibaba.fastjson.parser.ParserConfig#checkAutoType(java.lang.String, java.lang.Class<?>, int),它是在json数据反序列化时,通过@type指定class后,对class是否可被反序列化进行检查,其中检查包括黑名单、白名单、构造方法等
我们跟进com.alibaba.fastjson.parser.ParserConfig#checkAutoType(java.lang.String, java.lang.Class<?>, int):
if (typeName == null) {
return null;
}
if (typeName.length() >= 192 || typeName.length() < 3) {
throw new JSONException("autoType is not support. " + typeName);
}
可以看到,一开始的地方,会对typeName做一定的检查,typeName是我们传入json数据@type这个key对应的值value
final boolean expectClassFlag;
if (expectClass == null) {
expectClassFlag = false;
} else {
if (expectClass == Object.class
|| expectClass == Serializable.class
|| expectClass == Cloneable.class
|| expectClass == Closeable.class
|| expectClass == EventListener.class
|| expectClass == Iterable.class
|| expectClass == Collection.class
) {
expectClassFlag = false;
} else {
expectClassFlag = true;
}
}
接着是对一些期望class的判断,若不是期望中反序列化指定的class,后续黑白名单的检查会不管是否启用autoTypeSupport。
String className = typeName.replace('$', '.');
Class<?> clazz = null;
final long BASIC = 0xcbf29ce484222325L;
final long PRIME = 0x100000001b3L;
final long h1 = (BASIC ^ className.charAt(0)) * PRIME;
if (h1 == 0xaf64164c86024f1aL) { // [
throw new JSONException("autoType is not support. " + typeName);
}
if ((h1 ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) {
throw new JSONException("autoType is not support. " + typeName);
}
final long h3 = (((((BASIC ^ className.charAt(0))
* PRIME)
^ className.charAt(1))
* PRIME)
^ className.charAt(2))
* PRIME;
boolean internalWhite = Arrays.binarySearch(INTERNAL_WHITELIST_HASHCODES,
TypeUtils.fnv1a_64(className)
) >= 0;
if ((!internalWhite) && (autoTypeSupport || expectClassFlag)) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
hash ^= className.charAt(i);
hash *= PRIME;
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);
if (clazz != null) {
return clazz;
}
}
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
再接着,会对className的前两位字符进行判断是否允许,然后会二分查找内部白名单INTERNAL_WHITELIST_HASHCODES,若不在内部白名单内,并且开启了autoTypeSupport或者是预期以外的class,则会对className后面的字符继续进行hash处理后与外部白名单、黑名单进行判断,决定其是否被支持反序列化。
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}
if (clazz == null) {
clazz = typeMapping.get(typeName);
}
if (internalWhite) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);
}
if (clazz != null) {
if (expectClass != null
&& clazz != java.util.HashMap.class
&& !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
从上面的代码中,我们可以看到有好几个if流程从jvm缓存中获取class,也即会对class进行一定的判断,决定是否从缓存map中加载,我们这一节重点关注的其实是TypeUtils.getClassFromMapping:
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}
跟进TypeUtils.getClassFromMapping代码实现,可以看到,其具体是从mappings缓存中获取class
public static Class<?> getClassFromMapping(String className){
return mappings.get(className);
}
接着会判断class是否在内部白名单内,若在白名单内,会直接通过检查,返回class
if (internalWhite) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);
}
跟进TypeUtils.loadClass,可以看到第三个参数true,决定了在其方法实现中是否会对查找出来的class进行缓存到mappings
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
...
try{
if(classLoader != null){
clazz = classLoader.loadClass(className);
if (cache) {
mappings.put(className, clazz);
}
return clazz;
}
} catch(Throwable e){
e.printStackTrace();
// skip
}
try{
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if(contextClassLoader != null && contextClassLoader != classLoader){
clazz = contextClassLoader.loadClass(className);
if (cache) {
mappings.put(className, clazz);
}
return clazz;
}
} catch(Throwable e){
// skip
}
try{
clazz = Class.forName(className);
if (cache) {
mappings.put(className, clazz);
}
return clazz;
} catch(Throwable e){
// skip
}
return clazz;
}
下一步,可以看到,又是一段黑白名单的检查代码,不过这次是autoTypeSupport不启用的情况下
if (!autoTypeSupport) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
char c = className.charAt(i);
hash ^= c;
hash *= PRIME;
if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
throw new JSONException("autoType is not support. " + typeName);
}
// white list
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);
}
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}
接着,在这段代码中通过asm对其class进行visit,取出JsonType注解信息
boolean jsonType = false;
InputStream is = null;
try {
String resource = typeName.replace('.', '/') + ".class";
if (defaultClassLoader != null) {
is = defaultClassLoader.getResourceAsStream(resource);
} else {
is = ParserConfig.class.getClassLoader().getResourceAsStream(resource);
}
if (is != null) {
ClassReader classReader = new ClassReader(is, true);
TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]);
classReader.accept(visitor);
jsonType = visitor.hasJsonType();
}
} catch (Exception e) {
// skip
} finally {
IOUtils.close(is);
}
而从后续代码中也可以了解到,若到这一步,class还是null的时候,就会对其是否注解了JsonType、是否期望class、是否开启autotype进行判断。若判断通过,然后会判断是否开启autotype或是否注解了JsonType,从而觉得是否会在加载class后,对其缓存到mappings这个集合中,那也就是说,我只要开启了autoType的话,在这段逻辑就会把class缓存道mappings中
if (clazz == null && (autoTypeSupport || jsonType || expectClassFlag)) {
boolean cacheClass = autoTypeSupport || jsonType;
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}
上面这一块是一个很关键的地方,也是我这一小节缓存绕过的主要核心
最后,也就是我们需要去绕过的地方了,像一般大部分情况下,我们基本不可能找到注解有JsonType的class的gadget chains,所以,这一步中对jsonType判断,然后缓存class到mappings基本就没什么利用价值了。但这块逻辑中,我们需要注意的其实是JavaBeanInfo在build后,对其creatorConstructor的判断
if (clazz != null) {
if (jsonType) {
TypeUtils.addMapping(typeName, clazz);
return clazz;
}
if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
|| javax.sql.DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
|| javax.sql.RowSet.class.isAssignableFrom(clazz) //
) {
throw new JSONException("autoType is not support. " + typeName);
}
if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
TypeUtils.addMapping(typeName, clazz);
return clazz;
} else {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}
JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, propertyNamingStrategy);
if (beanInfo.creatorConstructor != null && autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
}
if (!autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
从creatorConstructor和autoTypeSupport的判断流程中,我们可以得知,只要autoTypeSupport为true,并且creatorConstructor(上一小节就是描述构造方法的选择,这里判断的构造方法是第二种选择)不为空,则会抛出异常,而后面的!autoTypeSupport判断,也表示了,就算上一步通过设置autoTypeSupport为true可以绕过,但是最终也避免不了它抛出异常的制裁:
if (!autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
那怎么办呢?这时候就得看前面的代码了,我前面也说了,在对黑白名单进行一轮检查后的时候,会有这个判断:
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}
public static Class<?> getClassFromMapping(String className){
return mappings.get(className);
}
从mappings中直接获取,接着在后面判断道class不为空时,直接就返回了,从而提前结束该方法执行,绕过构造方法creatorConstructor和autoTypeSupport的判断
if (clazz != null) {
if (expectClass != null
&& clazz != java.util.HashMap.class
&& !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
那我们怎么才能从缓存中获取到class呢?答案其实前面也说了:
if (clazz == null && (autoTypeSupport || jsonType || expectClassFlag)) {
boolean cacheClass = autoTypeSupport || jsonType;
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}
对,没错,就是这里,我们只要开启了autoTypeSupport,绕后通过两次反序列化,在第一次反序列化时,虽然最后会抛出异常,但是在抛异常前,做了上述代码中的缓存class到mappings的处理,那么,在第二次反序列化该class的时候,我们就可以顺利的从缓存中取出了,从而绕过后面的判断。
反射调用,就是fastjson反序列化的最后一个阶段了,当经历了前面:词法解析、构造方法选择、缓存绕过阶段之后,我们离RCE就差最后的一步了,也就是反射调用,从而触发gadget chain的执行,最终实现RCE。
接着,又回到DefaultJSONParser.parseObject来,也就是第2小节构造方法选择部分
ObjectDeserializer deserializer = config.getDeserializer(clazz);
Class deserClass = deserializer.getClass();
if (JavaBeanDeserializer.class.isAssignableFrom(deserClass)
&& deserClass != JavaBeanDeserializer.class
&& deserClass != ThrowableDeserializer.class) {
this.setResolveStatus(NONE);
} else if (deserializer instanceof MapDeserializer) {
this.setResolveStatus(NONE);
}
Object obj = deserializer.deserialze(this, clazz, fieldName);
return obj;
前面也说了,大部分可利用的gadget chain,config.getDeserializer(clazz)最终都会走到
com.alibaba.fastjson.parser.ParserConfig#getDeserializer(java.lang.reflect.Type)
->
com.alibaba.fastjson.parser.ParserConfig#getDeserializer(java.lang.Class<?>, java.lang.reflect.Type)
->
com.alibaba.fastjson.parser.ParserConfig#createJavaBeanDeserializer
而反射调用,是选择setter还是getter方法调用,亦或者是直接反射field设值,它需要一系列的判断处理,最终确定下来在JavaBeanDeserializer中执行deserialze时,到底会做什么样的反射调用处理
我们跟进JavaBeanInfo.build,前面一大段,我们在讲构造方法选择的时候已经简单讲过了,但是我们并没讲一个小地方,就是FieldInfo的创建和添加
if (creatorConstructor != null && !isInterfaceOrAbstract) { // 基于标记 JSONCreator 注解的构造方法
...
if (types.length > 0) {
...
FieldInfo fieldInfo = new FieldInfo(fieldName, clazz, fieldClass, fieldType, field,
ordinal, serialzeFeatures, parserFeatures);
add(fieldList, fieldInfo);
...
}
//return new JavaBeanInfo(clazz, builderClass, null, creatorConstructor, null, null, jsonType, fieldList);
} else if ((factoryMethod = getFactoryMethod(clazz, methods, jacksonCompatible)) != null) {
...
Field field = TypeUtils.getField(clazz, fieldName, declaredFields);
FieldInfo fieldInfo = new FieldInfo(fieldName, clazz, fieldClass, fieldType, field,
ordinal, serialzeFeatures, parserFeatures);
add(fieldList, fieldInfo);
...
} else if (!isInterfaceOrAbstract) {
...
if (paramNames != null
&& types.length == paramNames.length) {
...
FieldInfo fieldInfo = new FieldInfo(paramName, clazz, fieldClass, fieldType, field,
ordinal, serialzeFeatures, parserFeatures);
add(fieldList, fieldInfo);
...
}
}
在省略大部分无关代码后,可以看到,对于这三种情况下的处理,最终都是实例化FieldInfo,然后直接调用add添加到集合fieldList中来,但是细心去看FieldInfo重载的构造方法可以发现,它存在多个构造方法,其中就有入参method的构造方法:
public FieldInfo(String name, //
Class<?> declaringClass, //
Class<?> fieldClass, //
Type fieldType, //
Field field, //
int ordinal, //
int serialzeFeatures, //
int parserFeatures)
public FieldInfo(String name, //
Method method, //
Field field, //
Class<?> clazz, //
Type type, //
int ordinal, //
int serialzeFeatures, //
int parserFeatures, //
JSONField fieldAnnotation, //
JSONField methodAnnotation, //
String label)
public FieldInfo(String name, //
Method method, //
Field field, //
Class<?> clazz, //
Type type, //
int ordinal, //
int serialzeFeatures, //
int parserFeatures, //
JSONField fieldAnnotation, //
JSONField methodAnnotation, //
String label,
Map<TypeVariable, Type> genericInfo)
这种构造方法意味着什么?在后面执行JavaBeanDeserializer.deserialze时,会发现,具有method入参的字段,很有可能会触发方法的执行,从而可以触发gadget chain的执行。
接着,后面就是一串惆怅的代码,无非就是根据setter方法名称智能提取出field名字...,其中会对所有的方法进行两次的遍历,我这边简单总结一下:
根据setter方法确定的字段名添加到集合
```
第二遍
1. 判断方法名长度是否大于4,不大于4则跳过
2. 静态方法跳过
3. 判断方法名称是否get前缀,并且第四个字符为大写,不符合则跳过
4. 方法有入参则跳过
5. 方法返回值不是Collection.class、Map.class、AtomicBoolean.class、AtomicInteger.class、AtomicLong.class或其子孙类则跳过
6. 获取方法上的注解JSONField,根据注解取字段名称
7. 根据getter方法名从第四个字符开始确定字段field名称(需把第一个字符转小写),若是boolean类型,则需把字段第一个字符转大写
8. 根据字段名获取到字段Field后,判断是否注解了JSONField,获取JSONField注解,确定字段field是否可以被反序列化,不可被反序列化则跳过
9. 根据字段名获取集合中是否已有FieldInfo,有则跳过
10. 根据getter方法确定的字段名添加到集合
以上就是总结,从这些总结,我们就不难分析,fastjson反序列化时,class到底哪个方法能被触发。
最后,对于这些添加到集合fieldList中的FieldInfo,会在JavaBeanDeserializer.deserialze中被处理
protected <T> T deserialze(DefaultJSONParser parser, //
Type type, //
Object fieldName, //
Object object, //
int features, //
int[] setFlags) {
...
try {
Map<String, Object> fieldValues = null;
if (token == JSONToken.RBRACE) {
lexer.nextToken(JSONToken.COMMA);
if (object == null) {
object = createInstance(parser, type);
}
return (T) object;
}
...
} finally {
if (childContext != null) {
childContext.object = object;
}
parser.setContext(context);
}
}
从上述代码可以看到,配对"@type":"..."之后,如果下一个token不为"}",即JSONToken.RBRACE,则获取反序列化器进行反序列化,根据前面扫描Field得到的信息以及json后续的key-value进行反序列化,如果下一个token为"}",则直接反射实例化返回
判断下一个token为"[",即JSONToken.LBRACKET,则进行数组处理
if (token == JSONToken.LBRACKET) {
final int mask = Feature.SupportArrayToBean.mask;
boolean isSupportArrayToBean = (beanInfo.parserFeatures & mask) != 0 //
|| lexer.isEnabled(Feature.SupportArrayToBean) //
|| (features & mask) != 0
;
if (isSupportArrayToBean) {
return deserialzeArrayMapping(parser, type, fieldName, object);
}
}
调用构造方法
if (beanInfo.creatorConstructor != null) {
...
try {
if (hasNull && beanInfo.kotlinDefaultConstructor != null) {
object = beanInfo.kotlinDefaultConstructor.newInstance(new Object[0]);
for (int i = 0; i < params.length; i++) {
final Object param = params[i];
if (param != null && beanInfo.fields != null && i < beanInfo.fields.length) {
FieldInfo fieldInfo = beanInfo.fields[i];
fieldInfo.set(object, param);
}
}
} else {
object = beanInfo.creatorConstructor.newInstance(params);
}
} catch (Exception e) {
throw new JSONException("create instance error, " + paramNames + ", "
+ beanInfo.creatorConstructor.toGenericString(), e);
}
...
}
最后,通过FieldDeserializer对字段进行反序列化处理,其中,会利用到FieldInfo前面构建时,收集到的信息,例如method、getOnly等,进行判断是否调用某些方法
FieldDeserializer fieldDeserializer = getFieldDeserializer(entry.getKey());
if (fieldDeserializer != null) {
fieldDeserializer.setValue(object, entry.getValue());
}
可以看到,对于method不为空的fieldInfo,若getOnly为false,则直接反射执行method,若getOnly为true,也就是只存在对应字段field的getter,而不存在setter,则会对其method的返回类型进行判断,若符合,才会进行反射执行该method
Method method = fieldInfo.method;
if (method != null) {
if (fieldInfo.getOnly) {
if (fieldInfo.fieldClass == AtomicInteger.class) {
AtomicInteger atomic = (AtomicInteger) method.invoke(object);
if (atomic != null) {
atomic.set(((AtomicInteger) value).get());
}
} else if (fieldInfo.fieldClass == AtomicLong.class) {
AtomicLong atomic = (AtomicLong) method.invoke(object);
if (atomic != null) {
atomic.set(((AtomicLong) value).get());
}
} else if (fieldInfo.fieldClass == AtomicBoolean.class) {
AtomicBoolean atomic = (AtomicBoolean) method.invoke(object);
if (atomic != null) {
atomic.set(((AtomicBoolean) value).get());
}
} else if (Map.class.isAssignableFrom(method.getReturnType())) {
Map map = (Map) method.invoke(object);
if (map != null) {
if (map == Collections.emptyMap()
|| map.getClass().getName().startsWith("java.util.Collections$Unmodifiable")) {
// skip
return;
}
map.putAll((Map) value);
}
} else {
Collection collection = (Collection) method.invoke(object);
if (collection != null && value != null) {
if (collection == Collections.emptySet()
|| collection == Collections.emptyList()
|| collection.getClass().getName().startsWith("java.util.Collections$Unmodifiable")) {
// skip
return;
}
collection.clear();
collection.addAll((Collection) value);
}
}
} else {
method.invoke(object, value);
}
}
而对于method为空的情况,根本就不可能对method进行反射调用,除了构建实例时选择的构造方法
} else {
final Field field = fieldInfo.field;
if (fieldInfo.getOnly) {
if (fieldInfo.fieldClass == AtomicInteger.class) {
AtomicInteger atomic = (AtomicInteger) field.get(object);
if (atomic != null) {
atomic.set(((AtomicInteger) value).get());
}
} else if (fieldInfo.fieldClass == AtomicLong.class) {
AtomicLong atomic = (AtomicLong) field.get(object);
if (atomic != null) {
atomic.set(((AtomicLong) value).get());
}
} else if (fieldInfo.fieldClass == AtomicBoolean.class) {
AtomicBoolean atomic = (AtomicBoolean) field.get(object);
if (atomic != null) {
atomic.set(((AtomicBoolean) value).get());
}
} else if (Map.class.isAssignableFrom(fieldInfo.fieldClass)) {
Map map = (Map) field.get(object);
if (map != null) {
if (map == Collections.emptyMap()
|| map.getClass().getName().startsWith("java.util.Collections$Unmodifiable")) {
// skip
return;
}
map.putAll((Map) value);
}
} else {
Collection collection = (Collection) field.get(object);
if (collection != null && value != null) {
if (collection == Collections.emptySet()
|| collection == Collections.emptyList()
|| collection.getClass().getName().startsWith("java.util.Collections$Unmodifiable")) {
// skip
return;
}
collection.clear();
collection.addAll((Collection) value);
}
}
} else {
if (field != null) {
field.set(object, value);
}
}
}
至此,四个关键点得分析就此结束!