什么是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