Fastjson基础
介绍
Fastjson 是阿里巴巴开源的一个 Java 语言编写的高性能 JSON 库
主要就是序列化:将 Java Bean转换成 JSON 字符串(JSON.toJSONString(obj))。
反序列化:将 JSON 字符串转换回 Java Bean(JSON.parseObject(jsonStr, User.class))。
前端(App/网页)给后端发了一个 JSON,后端 Java 代码里有一个 User 类。如果要把这个 JSON 里的数据取出来, Fastjson 这样的工具可以实现自动化
先导入依赖
1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
</dependencies>1.2.24是最先报出漏洞的版本
分析
引出漏洞
最先开始演示一段代码
将 JSON 字符串转化为
JSONObject对象。(转成通用的java对象)1
2
3
4
5
6
7public class JSONUnser { public static void main(String[] args) throws Exception { String s = "{\"param1\":\"aaa\",\"param2\":\"bbb\"}";//将字符串键值对变成json格式 JSONObject jsonObject = JSON.parseObject(s); System.out.println(jsonObject); System.out.println(jsonObject.getString("param1"));通过
parseObject将字符串键值对变成json格式输出:

演示将写个将 JSON 字符串转化为 JavaBean 对象,获取这个对象的方法(转成特定的Person对象)
先写个java Bean的规范的Person类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31package org.example; public class Person { private int age; private String name; public Person() { System.out.println("constructor"); } public int getAge() { System.out.println("getAge"); return age; } public void setAge(int age) { System.out.println("setAge"); this.age = age; } public String getName() { System.out.println("getName"); return name; } public void setName(String name) { System.out.println("setName"); this.name = name; } }
也就是说传入age 和 name,通过set方法传入对象
还有一个功能也就是我们的漏洞产生点

代码里明明写的是
JSON.parseObject(s),但 Fastjson 却听从了 JSON 字符串里的指令,去加载了Person。因为写了@type当 Fastjson 看到
@type时,它会:去加载 这个类:
org.example.Person。去实例化 这个类(创建对象)。—————>输出
constructor**还原属性 ** 解析到后面有
age和name的值,找对应的 Setter————–>输出setAge、setName序列化输出 执行了
System.out.println(jsonObject),Fastjson 在把jsonObject转换成字符串打印出来时,会通过 Getter 获取值。————->输出:getAge、getName。要是我们传入的是一个恶意方法,就会造成问题
调试
调试一下它是如何返回这些东西的

先进来是一个只传
String text的parseObject
主要逻辑就是先将这个已经解析(parse)好的文本传成obj对象(其实就是Person)
要是这个对象继承JSONObject,就按照JSONObject类型返回对象
这个JSONObject就是个
map键值对
进
parse方法1
2
3public static Object parse(String text) { return parse(text, DEFAULT_PARSER_FEATURE); }再进

到这个地方就是将这些东西传到
DefaultJSONParser对象中他的作用主要就是一个解析器,对你传入的
text进行解析后面的
features就是对于解析的规范(比如:对出现的多个逗号的处理 啥的)接下来就是调用parse(),进行解析

到
parse()的核心逻辑1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84public Object parse(Object fieldName) { final JSONLexer lexer = this.lexer; switch (lexer.token()) { case SET: lexer.nextToken(); HashSet<Object> set = new HashSet<Object>(); parseArray(set, fieldName); return set; case TREE_SET: lexer.nextToken(); TreeSet<Object> treeSet = new TreeSet<Object>(); parseArray(treeSet, fieldName); return treeSet; case LBRACKET: JSONArray array = new JSONArray(); parseArray(array, fieldName); if (lexer.isEnabled(Feature.UseObjectArray)) { return array.toArray(); } return array; case LBRACE: JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField)); return parseObject(object, fieldName); case LITERAL_INT: Number intValue = lexer.integerValue(); lexer.nextToken(); return intValue; case LITERAL_FLOAT: Object value = lexer.decimalValue(lexer.isEnabled(Feature.UseBigDecimal)); lexer.nextToken(); return value; case LITERAL_STRING: String stringLiteral = lexer.stringVal(); lexer.nextToken(JSONToken.COMMA); if (lexer.isEnabled(Feature.AllowISO8601DateFormat)) { JSONScanner iso8601Lexer = new JSONScanner(stringLiteral); try { if (iso8601Lexer.scanISO8601DateIfMatch()) { return iso8601Lexer.getCalendar().getTime(); } } finally { iso8601Lexer.close(); } } return stringLiteral; case NULL: lexer.nextToken(); return null; case UNDEFINED: lexer.nextToken(); return null; case TRUE: lexer.nextToken(); return Boolean.TRUE; case FALSE: lexer.nextToken(); return Boolean.FALSE; case NEW: lexer.nextToken(JSONToken.IDENTIFIER); if (lexer.token() != JSONToken.IDENTIFIER) { throw new JSONException("syntax error"); } lexer.nextToken(JSONToken.LPAREN); accept(JSONToken.LPAREN); long time = ((Number) lexer.integerValue()).longValue(); accept(JSONToken.LITERAL_INT); accept(JSONToken.RPAREN); return new Date(time); case EOF: if (lexer.isBlankInput()) { return null; } throw new JSONException("unterminated json string, " + lexer.info()); case ERROR: default: throw new JSONException("syntax error, " + lexer.info()); } }
就是进行匹配解析,下面的case就对于xxxx,进行的解析逻辑(有什么左大括号,右大括号这些)
1
String s = "{\"@type\":\"org.example.Person\",\"age\":18,\"name\":\"ttt\"}";我们传的s先是有
{,所以就跳到了左大括号的解析这里
以对象解析,存到空的
JSONObject(它是一个map)里面接下来就是
parseObject()接收map对你的object解析,我们来看一下parseObject()(它是在fastjson中最重要的一个函数)
这个函数的逻辑主要就是将map解析成对象,先解析key,在解析value

这里就出现了我们前面提到的
features,此处是对逗号进行处理的解析key,对里面的值进行匹配解析

对key值的形式进行匹配

要是@type,就进第一个if逻辑、
我们看一下第一个if的逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) { String typeName = lexer.scanSymbol(symbolTable, '"'); Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader()); if (clazz == null) { object.put(JSON.DEFAULT_TYPE_KEY, typeName); continue; } lexer.nextToken(JSONToken.COMMA); if (lexer.token() == JSONToken.RBRACE) { lexer.nextToken(JSONToken.COMMA); try { Object instance = null; ObjectDeserializer deserializer = this.config.getDeserializer(clazz); if (deserializer instanceof JavaBeanDeserializer) { instance = ((JavaBeanDeserializer) deserializer).createInstance(this, clazz); } if (instance == null) { if (clazz == Cloneable.class) { instance = new HashMap(); } else if ("java.util.Collections$EmptyMap".equals(typeName)) { instance = Collections.emptyMap(); } else { instance = clazz.newInstance(); } } return instance; } catch (Exception e) { throw new JSONException("create instance error", e); } } this.setResolveStatus(TypeNameRedirect); if (this.context != null && !(fieldName instanceof Integer)) { this.popContext(); } if (object.size() > 0) { Object newObj = TypeUtils.cast(object, clazz, this.config); this.parseObject(newObj); return newObj; } ObjectDeserializer deserializer = config.getDeserializer(clazz); return deserializer.deserialze(this, clazz, fieldName); }先进行类加载

类加载逻辑:加载过,就到mapping缓存里面直接加载
后面的if是对于类加载的具体形式
字节码第一位是{,则为数组形式加载
字节码第一位是L以
;结尾的,之则自动把前后的L和;删掉,提取出中间真正的类名,然后递归调用loadClass去加载
接下来就是放东西

每次加载完一轮,然后就往里面放东西,现在由于我们object是0(这个我们前面创建的那个空JSONobject),所以直接就跳过了这个if
疑问:
size一直是 0,这个if块不久永远进不去了???当你使用
{"@type": "..."}时,Fastjson 的解析逻辑是 流式解析。- 常规路径:如果 JSON 只是普通的
{"name":"abc"},Fastjson 会把键值对一个个放进object这个容器里,此时size会大于 0,然后通过TypeUtils.cast转成类。 - AutoType 路径:当解析器读到关键的
@type时,它会立刻跳出当前的常规解析流程,直接去调用checkAutoType加载类并创建对象。 - 结果:数据直接被塞进了新创建的
Person实例里,而没有经过这个临时的object容器。所以对这个容器来说,它确实从头到尾都是空的(size = 0)。
- 常规路径:如果 JSON 只是普通的
接下来,也是最重要的一部,反序列化

先
getDeserializer获取反序列化器,然后利用反序化器的deserialze来反序列化,得到Person获取到PEerson后,进到
getDeserializer
在getDeserializer逻辑里面,这里是获取注解(自己写的活,相当于自己配了反序列化器)

接着里面有一个黑名单

里面禁止了
java.lang.Thread,不允许解析线程(既然存在黑名单,接下来的修复可能就会在这个地方有限制)一直匹配匹配,最后到了
还是匹配,都不是的话就创建一个反序列化器
按照javabean解析
到内部
createJavaBeanDeserializerasmEnable存在于java底层是动态创建类,动态加载的一个技术asmEnable默认是true
这是他不支持的一些判断
接下来到了**
JavaBeanInfo.build**
创建类对应的反序列化器的时候,要了解这个对象里面的所有东西(getname,setname….),然后组成
JavaBeanInfo说一下这个函数的逻辑:
先是遍历这个对象的字段,对应到我们这里就是age name
接下来获取默认构造器,对应我们这里的无参构造
然后就把构造函数
setAccessible,允许访问之后就到了一个for循环
先整体看一下

先遍历
methods,主要是为了得到所有的set字段再遍历
field又遍历
methods,主要是为了得到所有的get‘字段第一个
methods
遍历所有methods来获取字段(这个地方先获取的是name)
下面的if就是在满足set的条件(返回void ,字段长度…..)
上面都满足之后,就要在这个地方将其转为小写
此处propertyName值就变成了name这里的判断是为了应对一些特殊写法(_ f)

获取字段

还要找is的
看一下getfield1
2
3
4
5
6public static Field getField(Class<?> clazz, String fieldName, Field[] declaredFields) { for (Field field : declaredFields) { if (fieldName.equals(field.getName())) { return field; } }就是在所有已经有的字段去匹配
找到之后就要创建
FieldInfo
FieldInfo:

有个逻辑就是说,先创建一个getOnly字段,走else(如果长度不是一),就把getOnly改成true接下来走出
FieldInfo,add就是把字段放到filedlist里面Field(字段/成员变量)主要作用:
1处理“没有 Setter”的公有字段(public类里面没有set方法。Fastjson 遍历field时,发现这个字段是public的,它就会记录下来。这样反序列化时,它能直接通过反射把值塞进这个变量里,而不必非要找set方法。)
2识别注解(遍历field才能读到这个注解。)
3为“私有字段直接注入”做准备(遍历field拿到所有字段的名单。如果在第一步遍历methods时没找到对应的set方法,它会回过头来看看能不能直接通过修改field.setAccessible(true)来暴力赋值。)第二个
methods遍历完setter之后,获取所有的public
然后遍历所有getter
说一下接下来的逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29if (Collection.class.isAssignableFrom(method.getReturnType()) // || Map.class.isAssignableFrom(method.getReturnType()) // || AtomicBoolean.class == method.getReturnType() // || AtomicInteger.class == method.getReturnType() // || AtomicLong.class == method.getReturnType() // ) {//规定返回形式 String propertyName; JSONField annotation = method.getAnnotation(JSONField.class); if (annotation != null && annotation.deserialize()) { continue; } if (annotation != null && annotation.name().length() > 0) { propertyName = annotation.name(); } else { propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4); } FieldInfo fieldInfo = getField(fieldList, propertyName);//从fieldList里面获取propertyName,查看是否符合只有get,没有set的情况(上面说了set会放到fieldList里面) if (fieldInfo != null) { continue; } if (propertyNamingStrategy != null) { propertyName = propertyNamingStrategy.translate(propertyName); } add(fieldList, new FieldInfo(propertyName, method, null, clazz, type, 0, 0, 0, annotation, null, null));如果返回的是
Map、AtomicBoolean、AtomicInteger、AtomicLong这几种形式,而且是只有get,没有set的情况的话,就会放在新的fieldList里面不满足的话就到了

进
JavaBeanInfo看看1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42public JavaBeanInfo(Class<?> clazz, // Class<?> builderClass, // Constructor<?> defaultConstructor, // Constructor<?> creatorConstructor, // Method factoryMethod, // Method buildMethod, // JSONType jsonType, // List<FieldInfo> fieldList){ this.clazz = clazz; this.builderClass = builderClass; this.defaultConstructor = defaultConstructor; this.creatorConstructor = creatorConstructor; this.factoryMethod = factoryMethod; this.parserFeatures = TypeUtils.getParserFeatures(clazz); this.buildMethod = buildMethod; this.jsonType = jsonType; if (jsonType != null) { String typeName = jsonType.typeName(); if (typeName.length() != 0) { this.typeName = typeName; } else { this.typeName = clazz.getName(); } } else { this.typeName = clazz.getName(); } fields = new FieldInfo[fieldList.size()]; fieldList.toArray(fields); FieldInfo[] sortedFields = new FieldInfo[fields.length]; System.arraycopy(fields, 0, sortedFields, 0, fields.length); Arrays.sort(sortedFields); if (Arrays.equals(fields, sortedFields)) { sortedFields = fields; } this.sortedFields = sortedFields; defaultConstructorParameterSize = defaultConstructor != null ? defaultConstructor.getParameterTypes().length : 0; }就是把我们刚刚得到的东西传到
JavaBeanInfo
通过上面的一系列操作,找到了两个字段
name以及age。字段里面还有对应的setter方法,没有getter(前面我们提到过,有setter就不会有getter,因为反序列化的时候主要是传值赋值,主要用的就是setter)再往下就是对于修改
asmEnable值的if
对于asmEnable这个开关的作用:
如果开关是关着的,就会调用原生的JavaBeanDeserializer
如果是开着的,就会调用asmFactory临时创建一个createJavaBeanDeserializer
所以现在就有一个问题,因为是临时创建的,所以我们调试不到,流程在走但是我们跳到了
JavaBeanDeserializer(应为这是他的父类,为了能看到代码,咱们就直接看这个类)
扫描 Methods:首选
setXXX,因为这是最正规的。扫描 Fields:查漏补缺。看看有没有公有变量,或者有没有被注解标记的特殊字段。
二次扫描 Methods:寻找符合特定条件的
getXXX(比如返回类型是 Map 或 Collection 的,这种 getter 也能用来还原数据)。现在我们要想能够调试的到,就不能让
asmEnable开关开着,想办法给他关了,这样就会走默认的javabeaninfo前面我们提到了很多让开关关闭的限制

找到了这个方法,就是字段是getonly的

只有走这个地方才能让
getonly打开,让asmEnable关了
要想实现,就得不进if,进else,得满足条件,要求方法的参数长度不等于一
前面我们提到了,在setter的时候长度不为1则退出,所以在setter的时候添加不到fieldinfo

所以我们只能从getter那边入手,要想走getter就得满足上面说的
Map、AtomicBoolean、AtomicInteger、AtomicLong这几种形式返回值所以我们现在就要在Person类里面加点东西,以符合条件

只有get方法,而且返回值是map
现在的相对于上面的变化呢,就是在第二个
method这里咱们先把前两个setname和setage过了,直接看getmap

符合条件,直接就add到了fieldinfo里面
进fieldinfo:

方法的参数长度,参数值是0,所以直接走else,让getonly变成true
接下里就走到了

将开关
asmEnable关闭达到了我们的目的,走内置的javabeaninfo来断点分析

这个地方要build一下,所以还会走前面的逻辑,咱们就直接跳过

接着往下走就是将值放进去,derializer里面就有三个值(加了一个map)

先获取这个反序列化器,然后调用反序列化方法

进
deserialze具体看deserialze里面到底是如何反序列化调用 构造函数 setter getter
遍历所有字段

如果有反序列化器(前面我们调用的默认的反序列化构造器)
接下来又很多if,走的是json字符串,我们跟着代码逻辑看看
创建实例:

内部逻辑就是:要是接口就创建动态代理,否则就调构造函数

调用完构造函数之后,赋值

赋值的话,要么反射赋值,要么就是使用set方法赋值
这个地方用的是setter,直接就走到了invoke


invoke method对象的setAge方法
接下来的流程就是name和map的
name和age的流程一样样
看map
调用
getter:
obj就变成了Person对象,后面
parseObject调用toJSON的时候就调用了getter逻辑就是前面先是将json变成对象(调setter),后面tojson将对象变成json(调getter)

走getObjectWriter,因为前面走的反序列化,这边就是序列化

在这添加到field,下面在获取fieldlist

接下来寻找method

如何找到method的:
**
ParserConfig.parserAllFieldToCache**: 这个方法的名字里带有Field。它进去之后第一件事就是调用clazz.getDeclaredFields()。利用反射实现
TypeUtils.computeGetters,内部会调用clazz.getMethods()
调用这个方法
getFieldValuesMap
内部要获取值
getPropertyValue
getPropertyValue里面的get方法

到了invoke
总结:
先是按照字符串解析,对于有
@type的类型,就按照javabean解析,而且需要一个反序列化器,获取到之后调用反序列化方法
- 使用反序列化解析调用构造方法,constructor
- 赋值使用setter
- parseObject(s)会走到tojson,将getter方法调用


这个map满足我们上面提到的,四种类型返回,而且只有getter,那么也会调用


多的这个map就是上面满足条件的map
漏洞原理
Fastjson 盲目地根据这个 @type 去创建对象,我们就可以指定一个系统自带的、具有破坏性的类。
演示:
测试类:
1
2
3
4
5
6
7
8
9
package org.example;
import java.io.IOException;
public class Test {
public void setCmd(String cmd) throws IOException {
Runtime.getRuntime().exec(cmd);
}
}触发:
1
2
3
4
5
6
7
8
public class JSONUnser {
public static void main(String[] args) throws Exception {
// String s = "{\"param1\":\"aaa\",\"param2\":\"bbb\"}";
// String s = "{\"age\":18,\"name\":\"abc\"}";
// String s = "{\"@type\":\"org.example.Person\",\"age\":18,\"name\":\"ttt\",\"map\":{}}";
String s = "{\"@type\":\"org.example.Test\",\"cmd\":\"calc\"}";
JSONObject jsonObject = JSON.parseObject(s);
System.out.println(jsonObject);成功