1
基本介绍
Java 序列化是在 JDK 1.1 中引入的,是 Java 内核的重要特性之一。Java序列化将一个对象转换为流,反序列化则是将对象流转换为实际程序中使用的Java 对象的过程。序列化可以用于轻量级的持久化、通过 Sockets 进行传输、或者用于 Java RMI。
可序列化的对象需要实现 java.io.Serializable 接口或者 java.io.Externalizable 接口。
以实现 Serializable 接口为例,Serializable 仅是一个标记接口,并不包含任何需要实现的具体方法。实现该接口只是为了声明该Java类的对象是可以被序列化的。实际的序列化和反序列化工作是通过ObjectOuputStream和ObjectInputStream来完成的。ObjectOutputStream 的 writeObject 方法可以把一个Java对象写入到流中,ObjectInputStream 的 readObject 方法可以从流中读取一个 Java 对象。在写入和读取的时候,虽然用的参数或返回值是单个对象,但实际上操纵的是一个对象图,包括该对象所引用的其它对象,以及这些对象所引用的另外的对象。Java 会自动帮你遍历对象图并逐个序列化。除了对象之外,Java 中的基本类型和数组也是可以通过 ObjectOutputStream 和 ObjectInputStream 来序列化的。
示例代码:
Staff.java (用于序列化的类)
TestStaff.java (序列化测试类)
运行单元测试方法 testSerialize ,输出如下,说明成功进行了序列化与反序列化。
2
风险分析
由于对象序列化是遵循标准协议的,所以可以轻易地分析出流中的信息,并且可以进行篡改。协议文档 http://docs.oracle.com/javase/6/docs/platform/serialization/spec/protocol.html。 那么将会面临着以下的两种风险:
信息泄露
当序列化对象中存在敏感数据时,存在着信息泄露的风险。比如说上面例子中的 salary 字段,是公司的敏感信息,在序列化对象时候要特别注意。
以下是序列化后的数据,可以看到标出的数据为工资数据,即 100000 的十六进制表达。
数据篡改
恶意攻击者可以通过篡改序列化流中的数据达到以下目的:伪造、拒绝服务、命令执行等。
01
伪造
比较容易理解,就是篡改用户的 salary 字段,给自己多发些工资(修改上图中的 0186a0 即可)。
下面我将工资修改为 200000
当读取这个序列化对象时候,会将工资修改为 200000,想想就开心。
02
拒绝服务
当篡改的数据不符合序列化对象的格式要求时候,可能会导致在反序列化对象的过程中抛出异常,从而拒绝服务。
下面我们修改文件的头几个字节。
然后反序列化,会爆出异常,从而 dos
03
命令执行
当反序列化对象时的运行环境中存在有漏洞的 jar 包(比如 commons collectis 低版本),攻击者通过构造恶意数据流可以达到命令执行的效果。
新增以下的测试用例(我的环境中存在 commons-collections-3.1.jar,这个是有漏洞的 ),以弹出计算机为例:
其中 calc.ser 是使用 ysoserial 工具生成的 CommonsCollections 的命令执行序列化对象,生成方法:
java -jar ysoserial-0.0.4-all.jar CommonsCollections1 "calc" > calc.ser
此时,我们运行这个测试用例的时候,会直接弹出计算器。
3
缓解措施
通用措施
a、对序列化的流数据进行加密
b、在传输过程中使用 TLS 加密传输
c、对序列化数据进行完整性校验
针对信息泄露
使用 transient 标记敏感字段,这样敏感字段将不进行序列化
此时,我们再运行之前的测试程序,将无法得到 salary 字段的值。
针对数据篡改
01
针对序列化对象的属性
可以通过实现 validateObject 方法来进行对象属性值的校验。
步骤如下: 实现 ObjectInputValidation 接口并重写 validateObject 方法;实现 readObject 方法,并注册 Validation。
修改后的Staff.java代码如下:
此时我们将 Staff 的 id 设置为 -1,将会抛出异常。
02
针对整个对象伪造的
上面的方法虽然能够对字段进行验证,但其验证时机是在读取流之后,所以只能够对正常的序列化对象进行验证,对于畸形或者恶意序列化对象来说无能为例。
在序列化的流中,对象的描述是先于数据的,这就给了我们在读取流完成之前进行验证的机会。
方法是,通过重写 ObjectInputStream 的 resolveClass() 方法来实现。这样,我们需要自定义一个对象流读取类继承自 ObjectInputStream,代码如下:
然后,在反序列化的时候,使用自定义的 SecObjectInputStream 。
可以看到,成功阻断了计算器的弹出。
有人利用这个原理写了一个反序列化命令执行的防护工具,可以参考 https://github.com/ikkisoft/SerialKiller 。
4
参考文献
《Java序列化示例教程》
http://www.importnew.com/14465.html
《Serialization in Java – Java Serialization》
http://www.journaldev.com/2452/serialization-in-java
《Java Object Serialization》
http://docs.oracle.com/javase/8/docs/technotes/guides/serialization/index.html
《对象序列化流协议》
http://docs.oracle.com/javase/6/docs/platform/serialization/spec/protocol.html
《Java深度历险(十)——Java对象序列化与RMI》
http://www.infoq.com/cn/articles/cf-java-object-serialization-rmi/
《怎么做才能让Java 序列化机制 更安全》
http://www.myexception.cn/software-architecture-design/1463050.html
《Look-ahead Java deserialization》
https://www.ibm.com/developerworks/library/se-lookahead/
《SerialKiller》
https://github.com/ikkisoft/SerialKiller
本文作者:VSRC
本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/60819.html