什么是Java序列化和反序列化?
在Java中,序列化是为了方便传输存储数据的一种方式。
If we want to transfer an object, for instance, store it on a disk or send it over a network, we need to transform it into a byte stream.
Java序列化会将一个object转换成byte[]
,也就是一个含有object状态(属性信息)的二进制数组。Java序列化会使用Java反射来获取到需要被序列化的数据,包括private和final的fields。常用writeObject()来序列化Object。
Java serialization uses reflection to scrape all the data from the object’s fields that need to be serialized. This includes private and final fields
Java反序列化会根据该二进制流,重新创建一个相同状态下的object。Java反序列化不会用constructor
来创建一个object,相反,他会创建一个空的object,然后用Java反射把数据写进属性里,所以在重新创建object的时候,constructor
里的代码是不会被执行的(除了实现Externalizable接口的Class,之后会提到)。常用readObject()来反序列化Object。
When deserializing a byte stream back to an object it does not use the constructor. It creates an empty object and uses reflection to write the data to the fields.
显而易见,Java在序列化和反序列化的过程中,都用到了Java反射机制(需要注意的是,实现Externalizable接口并不会使用Java反射机制,这一点会在后面的内容中讲到),而整个过程其实就是数据转换为二进制流,再根据数据重新创建一个相同的object。
Java序列化和反序列的实现
Serializable接口
介绍
如果一个Class想要被序列化,那么他必须implements Serializable
。
The serialization interface has no methods or fields and serves only to identify the semantics of being serializable.
Serializable接口没有任何的方法或属性,它只是用来标识该Class是否可以被序列化。
根据官方注解,序列化的Class会有一个serialVersionUID
,它会在反序列化的时候,被用来验证发送者和接收者是否有一样的serialVersionUID
。需要注意的是,我们所声明的serialVersionUID
,必须为 static final long serialVersionUID
。
如果不声明serialVersionUID
,Java在序列化的时候会计算默认的serialVersionUID
值,但官方强烈建议所有Serializable Class都声明该值,避免一些不必要的错误。
编写Serializable Class
1 | public class Student implements Serializable { |
需要被序列化的Class,都要实现Serializable
接口。
编写实现序列化与反序列化Class
1 | public class SerializableTest { |
运行测试
Externalizable接口
介绍
除了通过implements Serializable
来让Class可序列化,我们同样可以使用Externalizable
来标识Class是可序列化的。
进入Externalizable interface
可以看到以下结构:
1 | public interface Externalizable extends java.io.Serializable { |
实际上,Externalizable
是继承了Serializable
,这也就能说明为什么它也能够用来表示Class是可序列化的了。但不同的是,这里有两个额外的method,writeExternal()
和readExternal()
。
所以当我们实现Externalizable
接口的时候,我们需要重写这两个method。
writeExternal()
和readExternal()
分别替代了writeObject()
和readObject()
两个methods,开发者需要手动对数据进行序列化和反序列化,这意味着,我们可以选择性地序列化和反序列化某些属性,相比Serializable接口就更加的灵活了。
此外,实现Externalizable
的Class必须要有默认的无参构造函数,因为Externalizable在反序列化时不使用反射机制,所以它必须要有constructor
。采用Externalizable无需产生serialVersionUID
,而Serializable接口需要。
编写Externalizable Class
1 | public class XiaoXueSheng implements Externalizable { |
我们在writeExternal()
方法中写入name属性,并在readExternal()
中取出赋值给当前Class的name属性。相反另一个属性age
并没有被我们序列化。
那么可以思考一下,当我们反序列化后,我们得到的age是什么值呢?
编写实现序列化和反序列化Class
1 | public class SerializableTest { |
我们创建了一个name为Leihehe, age为21的object,将它进行序列化和反序列化。
在反序列化操作后,我们将object中的name和age输出。
运行测试
我们发现name
被正常输出了,但age
为initilised value,这就解答了之前的问题。
在可序列化Class XiaoxueSheng
中,我们只规定将name
序列化和反序列化,并未序列化age
,所以当反序列化创建新的object后(执行无参constructor
),age
并未被赋值,而是最初的状态。
Java反序列化漏洞
什么是反序列化漏洞?
A Java deserialize vulnerability is a security vulnerability that occurs when a malicious user tries to insert a modified serialized object into the system that eventually compromises the system or its data.
敲重点:Java反序列化了被恶意修改的序列化对象(须是服务器中存在的对象或者依赖包中的对象)。
案例一
根据Java官方说明,任何实现Serializable接口的Class都可以定义自己的readObject()
方法,只要在重写方法的同时执行了defaultReadObject()
方法即可。这样在反序列化的时候会自动invoke该Class下自己定义的readObject()
方法。
那么我们可以为Class重写一个它自己的readObject()
的方法,里面带有恶意执行代码,让Java去反序列化这个我们修改后的Object,这样readObject()
被执行的时候,恶意代码也就被执行了。
1 | public class Student implements Serializable { |
此处我们重写了readObject()
,其中含有命令执行代码。
重新反序列化后,发现命令被执行。
案例二
我们序列化的时候,将数据存放进了一个叫test.cer
的文件中,当我们把这个序列化后的数据修改一下,那么效果也是一样的。
这里我用Notepad++打开,再使用它的HEX-Editor插件,可以看到该文件十六进制的格式。我们将此处修改一下
我们只让它反序列化我们修改好的object
内容已经被恶意修改了。
序列化后的数据分析
在前一部分的案例二中,我们在十六进制文件的基础上修改了数据,那么其中数据到底有什么意义呢?
这里我将用到SerializationDumper来分析。将HEX的值复制粘贴到这个项目中即可(不知为何,在NodePad++中直接复制粘贴,00会变成20,需要替换回来)。
这样一个清晰的结构对我们了解序列化的数据储存结构很有帮助。
- ACED0005
0xac ed
是Java序列化的字符串魔术数,相当于是Java序列化的十六进制特征码,看到这个值我们就能知道这是Java序列化后的十六进制的值。
0x00 05
是JAVA序列化的版本号
- 7372
0x73
是TC_OBJECT
, 代表下面的内容是一个新的对象
0x72
是TC_CLASSDESC
,Class描述符,代表下面是一个新的Class
0x00 07 代表 Class Name长度
0x53747564656e74代表 Class Name - Student
0x00 00 00 00 00 00 00 01代表serialVersionUID的值
后面的就不说了,具体资料可以在网上查到,配合SerializationDumper来学习分析会很有帮助。
Reference
Serialization and deserialization in Java: explaining the Java deserialize vulnerability