近期爆出的fastjson低版本(<=1.2.47)的安全漏洞,在修复上线之后简单的对其原理做下分析。
开篇前先说下这次漏洞的修复方案跟测试相关的内容:
1. 方案一: 升级 fastjson 的版本,最新版本是1.2.58
2. 方案二: 使用其它序列化技术如: fastxml jackson,gson,时间成本较大,建议先修复问题,后续作为技术优化完成此处升级。
3. 方案三:对于不再对外 提供 服务的应用,服务器上暴力 stop then delete。
测试方面 : 理论上如果编译没问题,本着向下兼容的原理,出现大的兼容性bug的可能性不大,但具体版本间做了哪些改动,你的程序用到了哪些,去梳理这些的时间可行性不高。 所以可以考虑以下测试方案(并列关系,也可组合):
1. 如果有过 tcpcopy 的实践,可以在沙箱环境做下流量回放,覆盖下线上用户场景,观察下日志,后续选择灰度或者全量上线。
2. 灰度一台线上机器,观察一段时间后全量,可以配合一些异常监控工具来监控这个灰度过程。
3. 代码层面上 review 工程引用的方法在两个版本之间是否有变更,如果没有可以快速灰度然后走全量。
回到本篇中心,我们先来看下网上给出的 POC(proof of concept),就是一些攻击 demo 。 fastjson 的这个安全漏洞主要利用了以下几个点:
1. fastjson 可以根据 @type 指定的类型进行反序列化,这个很关键,如果没有这个特性的话,正常会走类型推断,攻击value可能会被当成普通类型反序列化。
正常序列化的时候可以通过以下方式,输出类型信息:
String serializedStr = JSON.toJSONString(obj,SerializerFeature.WriteClassName);
2. 基于 1 这个特点,我们可以想到,我们可以构建一个字符串,让这个字符串反序列化到我们指定的一个类型。 但是这样就有安全问题了么? 答案是基本不会,因为反序列化的过程: 递归调用对应字段类型无参的构造函数->调用对应字段的set方法。 先看第一步,这个没有注入点; 第二步: 调用 set 方法,这个可以让我们来做一些事情,我们可以给对应字段赋上我们特定的值。 但是不管怎样,最终一个类型的所有字段最后都会落到基础类型的赋值。 所以如果没有后续动作,很难搞一些破坏,接下来会说最关键的一个环节,幕后黑手。
3. fastjson 的这个安全漏洞如果只让它来背锅可能有些牵强因为最终 hacker 利用的锚点是jdk里面rt.jar里面的一个类。 rt.jar 里面主要是包含运行时功能的一些类。 里面有一个类: com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
这个类里面有一个字段 _byteCodes ,从字面上理解它是存储存储类的字节表示的。 结合上面说的,我们可以构造一个类的字节数组给这个字段赋值,
让它引用我们设置的类型对应的字符串。 只是赋值还是不够的,要想造成破坏我们得让我们的这个类被加载,被实例化或者里面的一些特定方法被调用才可以。 这样我们才能在构造函数或者具体的方法里面去搞破坏。基于这两个前提我们做两次搜索看一下:
在该类里面搜索下load你会发现这么一行:
_class[i] = loader.defineClass(_bytecodes[i]);
在该类里面搜索下newInstance你会发现这么一行:
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance(); 注: 攻击类要继承AbstractTranslet这个抽象类,实例化之前有这个校验
这一下明朗了,我们赋值给这个字段的值: 类型的字节数组,后续会被加载并且实例化。 因此我们可以在构造函数里面实施攻击,例如执行一些系统命令。 这也是为什么这个类会在万千 jdk 的类里面被选出来作为锚点的原因。
4. _bytec odes 是私有属性,所以在 parseObject 的时候只有设置了 Feature.SupportNonPublicField 该字段才会反序列化 成功 。
进一步想一下:
我们的锚点字段除非是 Object 类型或者就是 TemplatesImpl 类型,反序列化之后的字段赋值才不会出现类型不匹配的问题,其它情况 payload 会失效么?
答:不会,payload 在赋值前已经完成所有操作,后续失败对此无影响,看一下 下面的调用链就清楚了。
JSON.parseObject -> JavaBeanDeserializer.deserialze -> FieldDeserializer.setValue -> TemplatesImpl.getOutputProperties -> TemplatesImpl .newTransformer -> TemplatesImpl.getTransletInstance (该方法内部调用加载及实例化的方法)
PS: payload 里面有对 class 文件内容进行 base64 编码的操作,我们也可以在反序列化的过程中看到对应的base64 解码过程 IOUtils.decodeBase64(payload_str)。