浅谈下Fastjson的autotype绕过
继去年1.2.47 Fastjson被绕过之后,最近的1.2.68又出现了绕过。
正好前段时间翻了一遍Fastjson的源码,对整体逻辑有了一些了解,就尝试分析下autotype的校验过程,以及这两次绕过的思路。若有错误,还望指出。
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
先简单说下 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 = 0) { throw new JSONException("autoType is not support. " + typeName); } } } if ((!internalWhite) && (autoTypeSupport || expectClassFlag)) { long hash = h3; for (int i = 3; i = 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 deserializers.findClass typeMapping.get JsonType exceptClass
1.2.47的绕过
主要分析思路,这回的绕过主要靠的是 mappings
缓存的绕过
根据之前分析的流程可以知道,当 mappings
缓存中存在指定类时,可以直接返回并且不受 SupportAutoType
的校验。
在 TypeUtils.loadClass
中,如果参数中 cache
值为 true
时,则会在加载到类之后,将类加入 mappings
缓存
寻找所有调用了该函数,并且 cache
设置为 true
的只有它的重载函数,然后继续寻找调用了该重载的地方
可以看到除了 TypeUtils
中,还有 MiscCodec
中调用了该方法
这里的逻辑是当class是一个 java.lang.Class
类时,会去加载指定类(从而也就无意之间加入了 mappings
缓存)
而 java.lang.Class
同时也是个默认特殊类,可以直接反序列化。
因此就可以首先通过反序列化 java.lang.Class
指定恶意类,然后恶意类被加入 mappings
缓存后,第二次就可以直接从缓存中获取到恶意类,并进行反序列化。
Throwable和1.2.68的绕过
这两个的绕过主要都是基于 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内部也是通过逻辑来保证校验前的返回类不会出现恶意类的情况,但是当整个项目变大之后,相互之间的调用会使得逻辑变得复杂,从而也就出现了逻辑绕过。
一次次的绕过和修复,对研究人员的代码功底要求也比较高,这种相互之间的博弈也相当精彩,值得好好学习一番。