Java序列化与反序列化 Java序列化
是指把java对象转换为字节序列的过程。这一过程将数据分解成字节流,以便存储在文件中或是在网络中传输
Java反序列化
是指把字节序列恢复为Java对象的过程。就是打开字节流并重构成对象
使用场景:
把内存中的对象保存到一个文件中或者数据库中的时候
想用套接字在网络上传输对象的时候
想通过RMI传输对象的时候
主要序列化的用途就是传递和存储
序列化的实现
将序列化的类实现Serializabel接口,不用实现任何方法,表明类的对象是可序列化的
并且所有属性必须是可序列化的,如果可序列化的类不是基本数据类型,也不是String,比如自定义的类,那这个引用类型必须是可序列化的,否则会导致此类不能被序列化(transient
关键字修饰的属性除外,不参与序列化过程)
需要序列化的类
User.java
package trrq;import java.io.Serializable;public class User implements Serializable { private String name; public String getName () { return name; } public void setName (String name) { this .name = name; } }
序列化
UserSerializable.java
package trrq;import java.io.File;import java.io.FileOutputStream;import java.io.ObjectOutputStream;public class UserSerializable { public static void main (String[] args) throws Exception { User user = new User(); user.setName("trrq" ); serialize(user); } public static void serialize (User user) throws Exception { FileOutputStream fout = new FileOutputStream("user.ser" ); ObjectOutputStream out = new ObjectOutputStream(fout); out.writeObject(user); out.close(); fout.close(); System.out.println("序列化完成" ); } }
运行上面这个序列化程序后,就会生成user.ser文件
我们可以看下这个文件结构,ACED表示魔术头,0005表示版本号
序列化的数据以魔术数字和版本号开头,这个值是在调用ObjectOutputStream时,由writeStreamHeader写入。开头几位一般当作Java序列化字节的特征
反序列化
Serializable package trrq;import java.io.*;public class UserSerializable { public static void main (String[] args) throws Exception { User user = new User(); user.setName("trrq" ); User user1 = unserialize(); System.out.println(user1.getName()); } public static void serialize (User user) throws Exception { FileOutputStream fout = new FileOutputStream("user.ser" ); ObjectOutputStream out = new ObjectOutputStream(fout); out.writeObject(user); out.close(); fout.close(); System.out.println("序列化完成" ); } public static User unserialize () throws Exception { File file; FileInputStream fileln = new FileInputStream("user.ser" ); ObjectInputStream in = new ObjectInputStream(fileln); User user = (User)in.readObject(); in.close(); fileln.close(); return user; } }
运行结果
读取序列化文件,将二进制文件重新恢复为user对象
Externalizable 通过实现Externalizable
接口进行序列化和反序列化,但必须实现writeExternal
、readExternal
方法,并且还要实现一个类的无参构造方法 ,Serializable
接口可以不用实现。
用法与Serializable是一致的
package trrq;import java.io.Externalizable;import java.io.IOException;import java.io.ObjectInput;import java.io.ObjectOutput;public class Evil implements Externalizable { public Evil () { System.out.println(this .getClass()+"的EvilClass的无参构造方法被调用" ); } public void writeExternal (ObjectOutput out) throws IOException { } public void readExternal (ObjectInput in) throws IOException, ClassNotFoundException { } }
readObject()方法 该方法在反序列化漏洞起了大作用,在反序列化过程中,JVM虚拟机试图调用对象类里的writeObject
和 readObject
方法,进行用户自定义的序列化和反序列化,如果没有这些方法,默认调用是ObjectOutputStream
的 defaultWriteObject
方法以及 ObjectInputStream
的 defaultReadObject
方法。用户定义的writeObject
和 readObject
方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。
Java反序列化的过程中可以自动执行序列化类的四个方法,实现了Serializable
接口可以执行的方法包括readObject
、readObjectNoData
、readResolve
,以及实现了Externalizable
接口的readExternal
方法。这些在找反序列化漏洞时都需要重点关注。
如果readObject
方法书写不当的话就有可能引发恶意代码的执行,例如
package trrq;import java.io.*;public class EvilClass implements Serializable { String name; public EvilClass () { System.out.println(this .getClass() + "的EvilClass()无参构造方法被调用!!!!!!" ); } public EvilClass (String name) { System.out.println(this .getClass() + "的EvilClass(String name)构造方法被调用!!!!!!" ); this .name = name; } public String getName () { System.out.println(this .getClass() + "的getName被调用!!!!!!" ); return name; } public void setName (String name) { System.out.println(this .getClass() + "的setName被调用!!!!!!" ); this .name = name; } @Override public String toString () { System.out.println(this .getClass() + "的toString()被调用!!!!!!" ); return "EvilClass{" + "name='" + getName() + '\'' + '}' ; } private void readObject (ObjectInputStream in) throws Exception { in.defaultReadObject(); System.out.println(this .getClass() + "的readObject()被调用!!!!!!" ); Runtime.getRuntime().exec(new String[]{"cmd" , "/c" , name}); } }
序列化与反序列化
package trrq;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;public class EvilSerialize { public static void main (String[] args) throws Exception { EvilClass evilObj = new EvilClass(); evilObj.setName("calc" ); byte [] bytes = serializeToBytes(evilObj); EvilClass o = (EvilClass)deserializeFromBytes(bytes); System.out.println(o); } public static byte [] serializeToBytes(final Object obj) throws Exception { final ByteArrayOutputStream out = new ByteArrayOutputStream(); final ObjectOutputStream objOut = new ObjectOutputStream(out); objOut.writeObject(obj); objOut.flush(); objOut.close(); return out.toByteArray(); } public static Object deserializeFromBytes (final byte [] serialized) throws Exception { final ByteArrayInputStream in = new ByteArrayInputStream(serialized); final ObjectInputStream objIn = new ObjectInputStream(in); return objIn.readObject(); } }
成功弹窗
在真实场景中,不会有人会写命令执行在readObject()中,反序列化通常会需要java的特性进行配合,然后通过利用链的寻找。反序列化漏洞需要三个东西