Fastjson 1.2.80漏洞

前言

学习!


环境搭建

依赖:

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.80</version>
</dependency>

漏洞分析

autotype绕过漏洞,可以在不开启的情况下反序列化对象。

整个漏洞利用分为两个部分,参照1.2.68漏洞的想法反序列化内置类,据说是通过内置类setter、构造函数、field跟攻击用类的关系将攻击用类加入缓存,最后使用攻击用类完成攻击。

autotype绕过

内置类表

关注checkAutoType函数,黑白名单之后会从内置类表、缓存类表等地方先找类,内置类表使用TypeUtils.getClassFromMapping中查找:

1
clazz = TypeUtils.getClassFromMapping(typeName);

即:

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

Exception就在这个内置类表中,所以并直接返回继续反序列化:

1
2
3
4
5
6
7
8
9
10
if (clazz != null) {
if (expectClass != null
&& clazz != java.util.HashMap.class
&& clazz != java.util.LinkedHashMap.class
&& !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}

return clazz;
}
缓存

写两个对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyClass {
public void setName(String name) {
System.out.println("Set Name");
}
}

class MyException extends Throwable {
private MyClass clazz;

public MyException(MyClass clazz) {

}

public void setClazz(MyClass clazz) {

}
}

然后是测试用代码:

1
2
3
4
5
6
7
8
9
10
11
String json1 =
" {\"a\":{" +
" \"@type\":\"java.lang.Exception\"," +
" \"@type\":\"org.example.MyException\"," +
" \"clazz\":{}" +
" }}";
try {
JSON.parse(json1);
}catch (Exception e) {
// pass
}

Fastjson根据Exception类找到对应的反序列化器ThrowableDeserializer:

1
2
3
4
5
6
7
8
9
if (JSON.DEFAULT_TYPE_KEY.equals(key)) {
if (lexer.token() == JSONToken.LITERAL_STRING) {
String exClassName = lexer.stringVal();
exClass = parser.getConfig().checkAutoType(exClassName, Throwable.class, lexer.getFeatures());
} else {
throw new JSONException("syntax error");
}
lexer.nextToken(JSONToken.COMMA);
}

调用checkAutoType函数加载org.example.MyException类,跟1.2.68版本的绕过方式相同,由于存在expectClass所以类加载正常进行:

1
2
3
4
if (autoTypeSupport || jsonType || expectClassFlag) {
boolean cacheClass = autoTypeSupport || jsonType;
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}

加载类后反序列化,首先是遍历构造函数实例化对象:

1
ex = createException(message, cause, exClass);

但是这里对构造函数的参数有要求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Class<?>[] types = constructor.getParameterTypes();
if (types.length == 0) {
defaultConstructor = constructor;
continue;
}

if (types.length == 1 && types[0] == String.class) {
messageConstructor = constructor;
continue;
}

if (types.length == 2 && types[0] == String.class && types[1] == Throwable.class) {
causeConstructor = constructor;
continue;
}

不然只会实例化一个Exception对象而不是输入的org.example.MyException,到这里为止通过构造函数参数类型似乎无法使用。

此时由于输入了clazz这个field,所以会开始准备set进去,首先找到对应的反序列化器:

1
ObjectDeserializer exDeser = parser.getConfig().getDeserializer(exClass);

找到ThrowableDeserializer:

1
2
3
else if (Throwable.class.isAssignableFrom(clazz)) {
deserializer = new ThrowableDeserializer(this, clazz);
}

构造函数会调用JavaBeanInfo.build构建成员信息,然后反序列化field:

1
2
3
4
5
6
7
8
9
10
11
String key = entry.getKey();
Object value = entry.getValue();

FieldDeserializer fieldDeserializer = exBeanDeser.getFieldDeserializer(key);
if (fieldDeserializer != null) {
FieldInfo fieldInfo = fieldDeserializer.fieldInfo;
if (!fieldInfo.fieldClass.isInstance(value)) {
value = TypeUtils.cast(value, fieldInfo.fieldType, parser.getConfig());
}
fieldDeserializer.setValue(ex, value);
}

由于输入的clazz值为{},是一个JSONObject,跟成员类型不合,所以会调用TypeUtils.cast函数进行类型转换。

由于JSONObject继承自Map,所以走到castToJavaBean函数:

1
2
3
4
5
6
7
8
9
10
11
if (obj instanceof Map) {
if (clazz == Map.class) {
return (T) obj;
}

Map map = (Map) obj;
if (clazz == Object.class && !map.containsKey(JSON.DEFAULT_TYPE_KEY)) {
return (T) obj;
}
return castToJavaBean((Map<String, Object>) obj, clazz, config);
}

关键代码:

1
2
3
4
5
6
7
8
9
10
JavaBeanDeserializer javaBeanDeser = null;
ObjectDeserializer deserializer = config.getDeserializer(clazz);
if (deserializer instanceof JavaBeanDeserializer) {
javaBeanDeser = (JavaBeanDeserializer) deserializer;
}

if (javaBeanDeser == null) {
throw new JSONException("can not get javaBeanDeserializer. " + clazz.getName());
}
return (T) javaBeanDeser.createInstance(map, config);

找反序列化器,又是一串getDeserializer函数调用,不同的是由于找不到对应的反序列化器,所以会创建:

1
2
3
deserializer = createJavaBeanDeserializer(clazz, type);
...
putDeserializer(type, deserializer);

这就导致了org.example.MyClass在deserializers中有了自己对应的反序列化缓存,下次反序列化时流程就跟Exception类似:

1
2
3
4
5
6
7
// Exception走这里
clazz = TypeUtils.getClassFromMapping(typeName);

// org.example.MyClass走这里
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}

也就绕过了autotype。

把field、setter和构造函数分开一个个测试,会发现以下两种情况可以成功利用:

  • 构造函数

  • setter

只有field的情况下,调试一下因为它获取field的函数用的是getFields,所以要改成public形式的:

1
2
3
4
5
6
7
8
9
10
11
class MyException extends Throwable {
public MyClass clazz;

public MyException(MyClass clazz) {

}

public void setClazz(MyClass clazz) {

}
}

也就是说field、setter和构造函数都可以绕过,最后调试一下构造函数的绕过点,给putDeserializer函数下断点,找到JavaBeanInfo.build函数:

1
2
3
4
5
6
Class<?> fieldClass = types[i];
Type fieldType = creatorConstructor.getGenericParameterTypes()[i];
...
FieldInfo fieldInfo = new FieldInfo(paramName, clazz, fieldClass, fieldType, field,
ordinal, serialzeFeatures, parserFeatures);
add(fieldList, fieldInfo);

根据构造函数参数类型和参数名,直接新建了一个FieldInfo出来放到fieldList。

反序列化攻击

在能加载到的类里找到能利用的就行,参考文章有groovy、jdbc和aspectj的利用链。


参考

某json 1.2.80 漏洞分析

Fastjson1.2.80漏洞复现


Fastjson 1.2.80漏洞
http://yoursite.com/2023/02/07/Fastjson-1-2-80漏洞/
作者
Aluvion
发布于
2023年2月7日
许可协议