fastjson1.2.80 in Springtboot新链学习
首发于先知社区:https://xz.aliyun.com/t/16708
https://www.geekcon.top/doc/ppt/GC24_SpringBoot%E4%B9%8B%E6%AE%87.pdf
http://squirt1e.top/2024/11/08/fastjson-1.2.80-springboot-xin-lian/
GitHub - luelueking/CVE-2022-25845-In-Spring: CVE-2022-25845(fastjson1.2.80) exploit in Spring Env!
前言
所有依赖 Fastjson 版本 1.2.80 或更早版本的程序,在应用程序中如果包含使用用户数据调用 JSON.parse 或 JSON.parseObject 方法,但不指定要反序列化的特定类,都会受此漏洞的影响。

在之前的研究中针对fj1.2.80已经有了三种常见的利用场景
GitHub - su18/hack-fastjson-1.2.80

漏洞复现
需要的依赖
- jackson
- commons-io
思路
- 将InputStream放入fastjson缓存
- 读取/tmp文件下的文件,找到docbase的文件名。
- 往${docbase}/WEB-INF/classes/路径下写入恶意类
- 通过fastjson触发类加载
GitHub - ph0ebus/CVE-2022-25845-In-Spring: exploit by python

漏洞分析
cache
这个新链子也是利用缓存机制

fastjson反序列化符合条件的期望类时,会将setter参数、public字段、构造函数参数加到缓存中。

先分析一下添加缓存的过程,以下面payload为例
1 | {"@type":"java.lang.Exception","@type":"com.fasterxml.jackson.core.exc.InputCoercionException"} |

在TypeUtils.getClassFromMapping()尝试从缓存中获取java.lang.Exception类

在com.alibaba.fastjson.util.TypeUtils#addBaseClassMappings初始化中默认添加了一些作为缓存了的类,其中就包含Exception.class

可以看到有95个缓存过的类

从缓存中获取class后返回,然后继续恢复其字段信息
com.alibaba.fastjson.parser.ParserConfig#getDeserializer先通过获取到的class获取对应的反序列化器


可以跟踪到这行关键代码

根据异常处理类的继承关系可以发现,java.lang.Exception类符合这个判断条件,于是反序列化器被设置为ThrowableDeserializer

在com.alibaba.fastjson.parser.deserializer.ThrowableDeserializer#deserialze反序列化过程中会将Exception作为期望类

然后解析json中的键值对,这里key是@type

当key为@type时会将Throwable.class作为期望类传入com.alibaba.fastjson.parser.ParserConfig#checkAutoType()


需要经过黑名单过滤和白名单校验

继续跟进到这段代码,根据传入的Typename来加载类,加载后,如果是期望类的子类则加入到缓存mapping中

read
进一步分析一下任意读的payload
1 | { |
利用循环引用尝试将字符串转换为对象并获取对象的值,按作者的话来说,这里是利用JsonPath来忽略本有的异常
接着上面继续分析,恢复好com.fasterxml.jackson.core.exc.InputCoercionException后,继续利用com.alibaba.fastjson.parser.deserializer.ThrowableDeserializer#deserialze获取字段,根据key实例化出FieldDeserializer进一步处理

继续,调用TypeUtils#cast进行类型转换

com.alibaba.fastjson.util.TypeUtils#cast(java.lang.Object, java.lang.Class<T>, com.alibaba.fastjson.parser.ParserConfig)会根据传入的obj进行相应的类型转换,这里会进入Map类型这个分支

跟进到com.alibaba.fastjson.util.TypeUtils#castToJavaBean(java.util.Map<java.lang.String,java.lang.Object>, java.lang.Class<T>, com.alibaba.fastjson.parser.ParserConfig),根据构造方法参数类型clazz获取反序列化器,clazz为com.fasterxml.jackson.core.JsonParser

获取到反序列化器后,调用putDeserializer函数this.deserializers.put(type, deserializer)

这里就会将type和deserializer存入com.alibaba.fastjson.util.IdentityHashMap#buckets中

在后续恢复com.fasterxml.jackson.core.JsonParser中,调用this.deserializers.findClass(typeName)就可以从com.alibaba.fastjson.util.IdentityHashMap#buckets中获取到这个类


而com.fasterxml.jackson.core.json.UTF8StreamJsonParser是com.fasterxml.jackson.core.JsonParser的子类,类似前面利用java.lang.Exception恢复com.fasterxml.jackson.core.exc.InputCoercionException一样

因为实现JsonParser的类中只有UTF8StreamJsonParser的构造参数存在InputStream,因此可以进一步获取到InputStream
1 | public UTF8StreamJsonParser(IOContext ctxt, int features, InputStream in, ObjectCodec codec, ByteQuadsCanonicalizer sym, byte[] inputBuffer, int start, int end, int bytesPreProcessed, boolean bufferRecyclable) { |

而获取InputStream就是为了实现任意文件读
原blackhat usa 21的议题ppt
https://i.blackhat.com/USA21/Wednesday-Handouts/US-21-Xing-How-I-Used-a-JSON.pdf
这里就是通过org.apache.commons.io.input.BOMInputStream来逐字节盲读取文件

在org.apache.commons.io.input.BOMInputStream#getBOM中会调用org.apache.commons.io.input.BOMInputStream#find方法

跟进find方法可以发现,这里先把 delegate 输入流的字节码转成 int 数组,然后拿 ByteOrderMark里的 bytes 挨个字节遍历去比对,如果遍历过程有比对错误的,getBom方法 就会返回null,如果遍历结束,没有比对错误那就会返回一个ByteOrderMark对象

因此逐字节盲读取的关键差异点就在这里
最后输入流来源来自于jdk.nashorn.api.scripting.URLReader,public URLReader(URL url)可以传入一个 URL 对象。这就意味着 file jar http 等协议都可以使用。这里传入了file协议用于列举目录
write
然后分析一下任意文件写的payload
1 | { |
这里和blackhat的议题提到的也有很多共通之处,都是利用org.apache.commons.io.input.TeeInputStream#read()方法来写入数据

其中的一些细节可以参考
Fastjson 1.2.68 反序列化漏洞 Commons IO 2.x 写文件利用链挖掘分析
但是这里作者似乎找到了一个更好的链子规避blackhat议题中原Poc链子中存在的写入缓冲区的8192字节限制


write2RCE
然后需要讨论的就是如何在任意文件写入的情况下RCE
Spring Boot Fat Jar 写文件漏洞到稳定 RCE 的探索
常见的做法比如覆盖charsets.jar就是利用jvm的懒加载,覆盖JDK HOME 目录下原有的 jar中未被加载的charsets.jar包。但这个做法需要事先知道 JDK HOME 的目录路径,并且需要root权限。而且需要针对目标服务jdk版本准备恶意charsets.jar文件,否则可能影响正常服务;又比如利用类加载,在jdk home目录下向classes目录写入恶意class文件,然后利用fastjson的@type触发类加载即可RCE
这里作者也是利用了类加载,不过这里换了一个新的类加载口子
在fastjson反序列化过程中,针对不在黑白名单,并且缓存中没有的类会通过com.alibaba.fastjson.util.TypeUtils#loadClass()尝试加载类,其中会通过通过TomcatEmbeddedWebappClassLoader类加载器加载类

根据双亲委派机制会委派WebappClassLoaderBase来加载,一路跟下去可以发现在org.apache.catalina.loader.WebappClassLoaderBase#findClass中会调用org.apache.catalina.loader.WebappClassLoaderBase#findClassInternal方法来寻找内部类

跟进findClassInternal

进一步跟进org.apache.catalina.webresources.StandardRoot#getClassLoaderResource跟踪类加载路径


这里会判断isCachingAllowed(),而属性cachingAllowed默认为true
1 | public boolean isCachingAllowed() { |

所以进到org.apache.catalina.webresources.Cache#getResource方法

首先调用noCache方法,很明显这里会返回true,从而调用到this.root.getResourceInternal(path, useClassLoaderResources)
1 | private boolean noCache(String path) { |
跟进org.apache.catalina.webresources.StandardRoot#getResourceInternal

就可以发现这个类加载路径

如果这个class文件存在就会正常返回该文件资源,然后恶意类加载达到RCE
后记
好复杂好复杂,结合三篇议题ppt才能微懂