38

浅谈下Fastjson的autotype绕过

 3 years ago
source link: https://www.kingkk.com/2020/06/浅谈下Fastjson的autotype绕过/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

继去年1.2.47 Fastjson被绕过之后,最近的1.2.68又出现了绕过。

正好前段时间翻了一遍Fastjson的源码,对整体逻辑有了一些了解,就尝试分析下autotype的校验过程,以及这两次绕过的思路。若有错误,还望指出。

autotype的校验

为什么校验一直被绕过?

1.2.24之后,fastjson对反序列化的类型进行了校验,主要就体现在 ParserConfig.checkAutoType 函数中

里面会对反序列化的类型进行黑白名单和校验,然后获取对应的Java类。

至于为什么没开启 SupportAutoType 属性依然会存在反序列化的危险呢?

fIF7ZjV.png!web

可以看到在解析过程中,只要key值为 @type 时,就会进入 checkAutoType 函数尝试获取类。

而且校验 SupportAutoType 属性的工作却是在 checkAutoType 函数中完成的(跟进之后也可以看到是在函数最末端调校验的值,并且在这之前有多处return)

那为什么要有这种设计呢?主要原因在于fastjson想让一些基础类(还有一些白名单中的异常类)可以不受 SupportAutoType 限制就可以反序列化。

例如之前别人提出的验证是否使用fastjson的 java.net.Inet6Addressjava.net.URL 也都是这个原理。

可以看到,即使不开启 SupportAutoType 依然是可以获取到具体的java类的。

E7NrumJ.png!web

所以,这就是为什么校验一直被绕过,感觉主要原因就在于为了实现这个feature,而导致的一些逻辑问题。

校验过程

checkAutoType主要有三个参数

String typeName
Class<?> expectClass
int features

先简单说下 expectClass 这个期望类,它的主要目的是为了让一些实现了 expectClass 这个接口的类可以被反序列化。

然后来看下校验的过程,一开始就是一些非null和长度限制的判断

之后判断 exceptClass 的类型,如果非null并且不是如下类型,则设置 expectClassFlagtrue

简单说的话就是不允许如下类型的 exceptClass

Object.class
Serializable.class
Cloneable.class
Closeable.class
EventListener.class
Iterable.class
Collection.class

qAZnqqY.png!web

之后比较长的一个部分就是比较类的哈希值,是否在内部白名单和内部黑名单中

如果在不在内部白名单并且 开启了 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类

zUBf6fZ.png!web

首先尝试从 TypeUtilsmappings 中获取对应类

6vuqye2.png!web

里面原本就有一些类,而且后续会被当作已获取类的缓存使用

2mEZFnQ.png!web

然后是尝试从 deserializers.findClass 中获取class类

这里面的类主要是在 ParserConfig.initDeserializers() 中被赋值的。

也就相当于这些特殊类也可以被无条件的反序列化

MB7Vvu3.png!web

然后就是尝试从 typeMapping 中获取对应类,这其中默认的值为空,需要开发人员自行赋值。

之后就是类在白名单中时(但几乎不大可能),尝试自动去加载类。

最后,如果通过以上方式可以加载到类,则校验期望类,没有问题的话就直接返回对应的class。

所以其实到这里,依然还没有出现 SupportAutoType 的校验,但已经可以返回类了(但正常情况下返回的一般都是程序中预先设置好的一些类,还不存在动态加载)。

然后就是在没有开启 SupportAutoType 时,通过黑白名单去校验类,黑名单抛出异常,白名单加载类并返回。

Q3e2yaU.png!web

之后的部分就是通过ASM的操作,去读取类是否有 JSONType 的注解(有注解的类一般都是开发自行写的JavaBean)

jeQv6fZ.png!web

之后如果 开启了 SupportAutoType 或者 有 JSONType 的注解 或者 存在期望类,则会直接去加载对应类

成功加载类之后,如果有注解,则加入 mapping 缓存并直接返回

如果是继承/实现了 ClassLoaderDataSourceRowSet 这些类的话直接异常。

如果存在期望类,则需要加载的类是期望类的子类或实现,并直接返回,否则异常。

如果类指定了 JSONCreator 注解,并且开启了 SupportAutoType 则抛出异常。

q2YNJjB.png!web

最后,校验了是否开启 SupportAutoType ,然后将类添加至 mapping 缓存,并返回对应类。

jaYbyyR.png!web

到此就是 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 缓存

7zI3InF.png!web

寻找所有调用了该函数,并且 cache 设置为 true 的只有它的重载函数,然后继续寻找调用了该重载的地方

vya2qee.png!web

可以看到除了 TypeUtils 中,还有 MiscCodec 中调用了该方法

qia6VvY.png!web

这里的逻辑是当class是一个 java.lang.Class 类时,会去加载指定类(从而也就无意之间加入了 mappings 缓存)

ne2uUzm.png!web

java.lang.Class 同时也是个默认特殊类,可以直接反序列化。

RzABbyu.png!web

因此就可以首先通过反序列化 java.lang.Class 指定恶意类,然后恶意类被加入 mappings 缓存后,第二次就可以直接从缓存中获取到恶意类,并进行反序列化。

Throwable和1.2.68的绕过

这两个的绕过主要都是基于 exceptClass 期望类的feature特性。

之前分析的时候提到,期望类的功能主要是实现 继承了期望类的class能被反序列化出来(并且不受autotype影响)

但是默认情况下 exceptClass 这个参数是空的,也就不存在期望类的特性。所以主要关注在程序内部别的地方的调用。

全局搜索一下可以看到主要有 ThrowableDeserializerJavaBeanDeserializer 两个类中有调用到。

V3QbYz6.png!web

先来说 ThrowableDeserializer ,它主要是对 Throwable 异常类进行反序列化的。

qMbmmyJ.png!web

ThrowableDeserializer 中可以根据第二个 @type 的值来获取具体类,并且传入指定期望类进行加载。

iMNRRrB.png!web

因此对一个异常类进行反序列化时,则可以依赖 exceptClass 期望类的特性去反序列化一个继承异常类的class。

但没有gadget时这也只能算作一个feature,本意也就是为了反序列化出异常类,并且异常类的限制其实比较苛刻。

其实一开始看浅蓝师傅发了这个之后,自己也关注到了 JavaBeanDeserializer 中的期望类调用,然后开始尝试看何种情况会调用 JavaBeanDeserializer

ParserConfig.getDeserializer 中可以看到,其实 JavaBeanDeserializer 的优先级其实是最低的(通常情况下都是一些第三方类才会调用到这里)

当时就草草看了下一些默认的基础类发现貌似没有可以走到这部分逻辑的就没整了(然后就被打脸了)。

my2uA3e.png!web

1.2.68的绕过主要靠的就是 AutoCloseable 类,恰好fastjson没有为它指定特定的deserializer,因此会走到最后的else条件,创建对应的 JavaBeanDeserializer 。并且它是默认在 mappings 缓存中的,可以无条件反序列化。

JavaBeanDeserializer 中也和之前一样,会根据第二个 @type 的值去获取对应的class

这里的 exceptClass 期望类也就是当前类 AutoCloseable

eeqQBj3.png!web

而且相较于 Throwable 来说, AutoCloseable 的范围则会大得多,常用的流操作、文件、socket之类的都继承了 AutoCloseable 接口。

I7zeeyB.png!web

之后的工作则是需要找一个gadget,但相较于1.2.47的绕过来说, exceptClass 期望类的返回位置相对比较靠后。

因此会存在黑名单的校验与 ClassLoaderDataSourceRowSet 的校验。

也就意味着之前的gadget是都不能用了,要找一条新的基于 AutoCloseable 的gadget。

至于后面的利用 FieldDeserializer 去拓展gadget就不在这里展开说了。

最后

以我个人的分析来看,主要原因还是在于Fastjson为了维护最开始那些基础类的无限制反序列化的特性。

导致即使开发人员关闭了 SupportAutoType 属性,但并不能阻止所有反序列化的情况。

Fastjson内部也是通过逻辑来保证校验前的返回类不会出现恶意类的情况,但是当整个项目变大之后,相互之间的调用会使得逻辑变得复杂,从而也就出现了逻辑绕过。

一次次的绕过和修复,对研究人员的代码功底要求也比较高,这种相互之间的博弈也相当精彩,值得好好学习一番。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK