作者:Kingkk
原文链接:https://www.kingkk.com/2020/06/%E6%B5%85%E8%B0%88%E4%B8%8BFastjson%E7%9A%84autotype%E7%BB%95%E8%BF%87/
本文为作者投稿,Seebug Paper 期待你的分享,凡经采用即有礼品相送!
投稿邮箱:[email protected]
继去年1.2.47 Fastjson被绕过之后,最近的1.2.68又出现了绕过。
正好前段时间翻了一遍Fastjson的源码,对整体逻辑有了一些了解,就尝试分析下autotype的校验过程,以及这两次绕过的思路。若有错误,还望指出。
1.2.24之后,fastjson对反序列化的类型进行了校验,主要就体现在ParserConfig.checkAutoType
函数中
里面会对反序列化的类型进行黑白名单和校验,然后获取对应的Java类。
至于为什么没开启SupportAutoType
属性依然会存在反序列化的危险呢?
可以看到在解析过程中,只要key值为@type
时,就会进入checkAutoType
函数尝试获取类。
而且校验SupportAutoType
属性的工作却是在checkAutoType
函数中完成的(跟进之后也可以看到是在函数最末端调校验的值,并且在这之前有多处return)
那为什么要有这种设计呢?主要原因在于fastjson想让一些基础类(还有一些白名单中的异常类)可以不受SupportAutoType
限制就可以反序列化。
例如之前别人提出的验证是否使用fastjson的java.net.Inet6Address
、java.net.URL
也都是这个原理。
可以看到,即使不开启SupportAutoType
依然是可以获取到具体的java类的。
所以,这就是为什么校验一直被绕过,感觉主要原因就在于为了实现这个feature,而导致的一些逻辑问题。
checkAutoType主要有三个参数
String typeName
被序列化的类名Class<?> expectClass
期望类int features
配置的feature值先简单说下expectClass
这个期望类,它的主要目的是为了让一些实现了expectClass
这个接口的类可以被反序列化。
然后来看下校验的过程,一开始就是一些非null和长度限制的判断
之后判断exceptClass
的类型,如果非null并且不是如下类型,则设置expectClassFlag
为true
简单说的话就是不允许如下类型的exceptClass
Object.class
Serializable.class
Cloneable.class
Closeable.class
EventListener.class
Iterable.class
Collection.class
之后比较长的一个部分就是比较类的哈希值,是否在内部白名单和内部黑名单中
如果在不在内部白名单并且 开启了SupportAutoType
或者 存在期望类时:如果在白名单中则直接加载,在黑名单中则异常退出。(讲起来有点绕,直接看代码可能好点)
String className = typeName.replace('$', '.');
Class<?> clazz;
final long BASIC = 0xcbf29ce484222325L;
final long PRIME = 0x100000001b3L;
final long h1 = (BASIC ^ className.charAt(0)) * PRIME;
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);
}
final long h3 = (((((BASIC ^ className.charAt(0))
* PRIME)
^ className.charAt(1))
* PRIME)
^ className.charAt(2))
* PRIME;
boolean internalWhite = Arrays.binarySearch(INTERNAL_WHITELIST_HASHCODES,
TypeUtils.fnv1a_64(className)
) >= 0;
if (internalDenyHashCodes != null) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
hash ^= className.charAt(i);
hash *= PRIME;
if (Arrays.binarySearch(internalDenyHashCodes, hash) >= 0) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
if ((!internalWhite) && (autoTypeSupport || expectClassFlag)) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
hash ^= className.charAt(i);
hash *= PRIME;
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);
if (clazz != null) {
return clazz;
}
}
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
之后就是尝试从各种地方去获取class类
首先尝试从TypeUtils
的mappings
中获取对应类
里面原本就有一些类,而且后续会被当作已获取类的缓存使用
然后是尝试从deserializers.findClass
中获取class类
这里面的类主要是在ParserConfig.initDeserializers()
中被赋值的。
也就相当于这些特殊类也可以被无条件的反序列化
然后就是尝试从typeMapping
中获取对应类,这其中默认的值为空,需要开发人员自行赋值。
之后就是类在白名单中时(但几乎不大可能),尝试自动去加载类。
最后,如果通过以上方式可以加载到类,则校验期望类,没有问题的话就直接返回对应的class。
所以其实到这里,依然还没有出现SupportAutoType
的校验,但已经可以返回类了(但正常情况下返回的一般都是程序中预先设置好的一些类,还不存在动态加载)。
然后就是在没有开启SupportAutoType
时,通过黑白名单去校验类,黑名单抛出异常,白名单加载类并返回。
之后的部分就是通过ASM的操作,去读取类是否有JSONType
的注解(有注解的类一般都是开发自行写的JavaBean)
之后如果 开启了SupportAutoType
或者 有JSONType
的注解 或者 存在期望类,则会直接去加载对应类
成功加载类之后,如果有注解,则加入mapping
缓存并直接返回
如果是继承/实现了ClassLoader
、DataSource
、RowSet
这些类的话直接异常。
如果存在期望类,则需要加载的类是期望类的子类或实现,并直接返回,否则异常。
如果类指定了JSONCreator
注解,并且开启了SupportAutoType
则抛出异常。
最后,校验了是否开启SupportAutoType
,然后将类添加至mapping
缓存,并返回对应类。
到此就是checkAutoType
的校验与加载类的过程。
可以看到虽然函数名是checkAutoType
,但是其实这是一个校验与加载类的过程。
而且真正的SupportAutoType
校验其实是被放到最后的,在这之前也存在许多加载类并返回类的地方,目的也就是一开始说的为了实现基础类的任意反序列化的feature。
这也就意味着需要通过逻辑来保证在这之前返回的类都是安全的,但也正是因为这个原因导致了autotype被逻辑绕过。
可以看到主要有如下种情况可以直接返回class
acceptHashCodes
白名单INTERNAL_WHITELIST_HASHCODES
内部白名单TypeUtils.mappings
mappings缓存deserializers.findClass
指定类typeMapping.get
默认为空JsonType
注解exceptClass
存在期望类主要分析思路,这回的绕过主要靠的是mappings
缓存的绕过
根据之前分析的流程可以知道,当mappings
缓存中存在指定类时,可以直接返回并且不受SupportAutoType
的校验。
在TypeUtils.loadClass
中,如果参数中cache
值为true
时,则会在加载到类之后,将类加入mappings
缓存
寻找所有调用了该函数,并且cache
设置为true
的只有它的重载函数,然后继续寻找调用了该重载的地方
可以看到除了TypeUtils
中,还有MiscCodec
中调用了该方法
这里的逻辑是当class是一个java.lang.Class
类时,会去加载指定类(从而也就无意之间加入了mappings
缓存)
而java.lang.Class
同时也是个默认特殊类,可以直接反序列化。
因此就可以首先通过反序列化java.lang.Class
指定恶意类,然后恶意类被加入mappings
缓存后,第二次就可以直接从缓存中获取到恶意类,并进行反序列化。
这两个的绕过主要都是基于exceptClass
期望类的feature特性。
之前分析的时候提到,期望类的功能主要是实现 继承了期望类的class能被反序列化出来(并且不受autotype影响)
但是默认情况下exceptClass
这个参数是空的,也就不存在期望类的特性。所以主要关注在程序内部别的地方的调用。
全局搜索一下可以看到主要有ThrowableDeserializer
和JavaBeanDeserializer
两个类中有调用到。
先来说ThrowableDeserializer
,它主要是对 Throwable
异常类进行反序列化的。
在ThrowableDeserializer
中可以根据第二个@type
的值来获取具体类,并且传入指定期望类进行加载。
因此对一个异常类进行反序列化时,则可以依赖exceptClass
期望类的特性去反序列化一个继承异常类的class。
但没有gadget时这也只能算作一个feature,本意也就是为了反序列化出异常类,并且异常类的限制其实比较苛刻。
其实一开始看浅蓝师傅发了这个之后,自己也关注到了JavaBeanDeserializer
中的期望类调用,然后开始尝试看何种情况会调用JavaBeanDeserializer
。
ParserConfig.getDeserializer
中可以看到,其实JavaBeanDeserializer
的优先级其实是最低的(通常情况下都是一些第三方类才会调用到这里)
当时就草草看了下一些默认的基础类发现貌似没有可以走到这部分逻辑的就没整了(然后就被打脸了)。
1.2.68的绕过主要靠的就是AutoCloseable
类,恰好fastjson没有为它指定特定的deserializer,因此会走到最后的else条件,创建对应的JavaBeanDeserializer
。并且它是默认在mappings
缓存中的,可以无条件反序列化。
在JavaBeanDeserializer
中也和之前一样,会根据第二个@type
的值去获取对应的class
这里的exceptClass
期望类也就是当前类AutoCloseable
而且相较于Throwable
来说,AutoCloseable
的范围则会大得多,常用的流操作、文件、socket之类的都继承了AutoCloseable
接口。
之后的工作则是需要找一个gadget,但相较于1.2.47的绕过来说,exceptClass
期望类的返回位置相对比较靠后。
因此会存在黑名单的校验与ClassLoader
、DataSource
、RowSet
的校验。
也就意味着之前的gadget是都不能用了,要找一条新的基于AutoCloseable
的gadget。
至于后面的利用FieldDeserializer
去拓展gadget就不在这里展开说了。
以我个人的分析来看,主要原因还是在于Fastjson为了维护最开始那些基础类的无限制反序列化的特性。
导致即使开发人员关闭了SupportAutoType
属性,但并不能阻止所有反序列化的情况。
Fastjson内部也是通过逻辑来保证校验前的返回类不会出现恶意类的情况,但是当整个项目变大之后,相互之间的调用会使得逻辑变得复杂,从而也就出现了逻辑绕过。
一次次的绕过和修复,对研究人员的代码功底要求也比较高,这种相互之间的博弈也相当精彩,值得好好学习一番。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1236/