自JDK1.1,为方便程序之间共享数据,Java便提供了对象序列化机制。其允许将Java对象转换为字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以后恢复成原来的对象。开发者可通过ObjectInputStream、ObjectOutputStream类来实践序列化或反序列化。
关注一下序列化后的字节序列:
根据可读部分简单观察可得,字节序列中包含了序列化对象的类名、字段类型、字段值。不包含类的完整定义(如方法信息等...)
- 造个轮子:字节序列可视化工具
字节序列的协议官方文档文档:
https://docs.oracle.com/javase/8/docs/technotes/guides/serialization/index.html
https://docs.oracle.com/javase/8/docs/platform/serialization/spec/protocol.html
纯协议解析,无类加载解析可视化反序列化字节序列: SerializationProtocols
- 序列化中的其他备注点:
反序列化时, 会对字节序列中涉及的类进行实例化,若在当前classpath下加载到该类,将抛出ClassNotFoundException,这对分析反序列化payload会带来一些基础阻碍。
反序列化时, 会对字节序列中的字段类型与当前classpath下的类定义做匹配,不匹配将反序列化失败,故无法通过定制字节序列伪造字段类型。
当应用对外提供服务或依赖外部服务时(如Http、RPC), 一定会涉及数据交互,而交互的数据形式无论是字节流或者文本类型,均无法方便的直接进行操作,往往需要将数据转换成一个对象方便使用,此时便涉及到了反序列化行为,如之前文章提到的RMI,就使用了JDK序列化来做数据交互支持。攻击者可以对服务提交恶意反序列化字节流来实践攻击。
- 构造恶意反序列化字节序列, 复现攻击
举个例子
备注: 这里使用ysoserial来构造恶意字节序列, ysoserial是一个java序列化安全研究工具。在Github上可以找到。
查看恶意字节序列内容
可以看到这是一个javax.management.BadAttributeValueExpException对象,并携带了多个字段,其中“open /System/Applications/Calculator.app”这个命令字符串也在其中。同时似乎看到了Runtime、getRuntime、exec。
执行反序列化
如图,反序列化一个恶意数据,经过一系列的函数调用,实现了调用Runtime.getRuntime().exec()执行可控的系统命令。!
通过构造精巧的序列化数据,可以使得程序在对其反序列化时,实现一系列的函数调用,最终达到恶意攻击的效果。这精巧的调用链被称为Gadget。利用Gadget实现攻击需要目标环境classpath下有相关类的存在。
0x00: org.apache.commons.collections.functors.InvokerTransformer
Apache Commons Collections 提供了如上一个类,提供一个转化能力(transform method), 支持将对象经过可控的反射转化成另一个对象。除了InvokerTransformer,类似的转换能力工具类还有: ConstantTransformer、ChainedTransformer。
自由组合一下:
以上代码把0转换成了1,从零到一。(顺带弹出了一个计算器)
0x01: org.apache.commons.collections.map.LazyMap
LazyMap提供了懒加载设计的Map结构,它有个习惯,每次get()懒加载时都把key Transformer下。
0x02: org.apache.commons.collections.keyvalue.TiedMapEntry。
TiedMapEntry,平平无奇的一个类,不过它复写了hashcode(), 里面会调用它持有的map的get方法。
0x03: InvokerTransformer & LazyMap & TiedMapEntry 组合一下。
调用一个对象的hashcode(), 调用了命令执行。
0x04: java.util.HashMap#readObject()
有Java开发经验的同学都知道,HashMap在putValue的时候会通过hashcode()计算key的hash值, 来确定Value存储的hash槽。而较少人知道的是,其实它反序列化的时候也会。
0x05: 构建攻击代码(假)。
根据上述逻辑,我们可构建出这样的一个恶意对象,当序列化成字节序列,在反序列化时将执行命令。
但当实际运行以上代码时会发现无法达到预期效果,原因是在 map.put(tiedMapEntry, "xxx") 的时候, lazyMap已经完成懒加载(及命令执行),导致后续反序列化时不再transformer。ok, 我们可以先给tiedMapEntry一个无transformer的lazymap,待组装完对象后,重新赋值一个恶意lazymap。
0x06: 构建攻击代码
0x07: Run
备注: 该Gadget来自ysoserial: CommonsCollections6,ysoserial项目收集了一些公开的反序列化Gadget, 供Java序列化安全研究。
如上,Gadget会利用一些反序列化过程中的隐式调用来作为恶意调用链的桥梁。在JDK反序列化中,当一个类自定义了readObject()、readExternal()等函数,这些函数将在反序列化时被调用。同时反序列化数据包含了对象类型,即外部提交的反序列化数据可以决定调用哪个类的readObject()。一个反序列化过程,会发生哪些调用?
攻击者通过可控的反序列化数据,可一定程度上操控调用这些隐式函数,配合一些有安全风险的函数实现,带来了攻击机会。对于防护视角,也可以在反序列化流程中,通过收敛上述隐式函数,来达到收敛攻击面的效果。
这些精巧的Gadget如何挖掘出来的呢?Gadget链有长有短,通过IDEA寻找slink调用链,投入足够的精力挖掘一些短的链多少也会有收获。效率太低?自动化?试试 GadgetInspector。
Java世界中除了JDK反序列化还有很多第三方提供了反序列化能力,他们或性能更高,或字节最少,这里我们从安全角度做个简单评估。
几个小判断:
- 绝大部分反序列化机制都存在安全风险。
- 反序列化机制涉及的隐式函数调用 + 支持反序列化的风险类(提供了安全风险功能)构成攻击链。
- 反序列化机制是否支持数据指定类型很大程度影响安全风险。
- 限定可反序列化类的范围可明显收敛攻击面,当然使用白名单or黑名单是个选择。
黑名单存在无法枚举风险类,采用此种方案的Fastjson不得不持续升级,一线研发也...,后续其提供了NoneAutotype版本,关闭数据指定类型能力,终止了无限升级循环...
当你的程序涉及数据传输,并使用了对象序列化/反序列化技术,就存在被攻击的风险。典型的场景如Http服务、RPC,之前关于RMI的攻击介绍,其本质便属于RPC场景的反序列化攻击。诸如此类的还有: Shrio、Dubbo、Jenkins...反序列化攻击。它真的很好用。
如何防护反序列化攻击?选择安全的反序列化框架是个相对一劳永逸的办法,但如果是已经成型的工程,无法低成本的切换反序列化方案,通过黑名单限定可反序列化类的范围,并提供动态维护能力是个思路,RASP呼之欲出。