Java的序列化与反序列化
一、序列化与反序列化
1、序列化的方式
如果需要将某个对象保存到磁盘上或者通过网络传输,那么这个类应该实现 Serializable 接口或者Externalizable接口之一。
使用到JDK中关键类 :ObjectOutputStream (对象输出流) 和 ObjectInputStream (对象输入流)ObjectOutputStream 类中:通过使用 writeObject (Object object) 方法,将对象以二进制格式进行写入。
ObjectInputStream 类中:通过使用 readObject() 方法,从输入流中读取二进制流,转换成对象。
Transient关键字序列化的时候不会序列化Transient关键字修饰的变量(private transient String phone;),这个关键字不能修饰类和方法**Static。**静态变量也不会被序列化。
serialVersionUID:这里是指序列化的版本号,版本不一致会导致抛出错误,并且拒绝载入序列化与反序列化。
序列化必须满足以下几个条件:
1、实现Serializable接口或Externalizable接口
比如:class Student implements Serializable 定义类时要这样 implements
不实现接口:报错无法执行
实现接口:正常执行代码
2、当前类提供一个全局常量 serialVersionUID,每个类都有一个约定好的 serialVersionUID
意思是,序列化后结果的UID,在反序列化时,该UID必须保持不变
要么序列化和反序列化全都使用默认分配的UID,要么二者都是用自定义的UID
如果序列化时使用的时默认分配的 UID,那么反序列化时也必须使用默认分配的UID:
如上图进行序列化后再进行反序列化:并没有报错
但如果我将serialVersionUID打开进行序列化,然后注释掉再进行反序列化
或者
将 serialVersionUID 注释掉进行序列化,然后打开进行反序列化
都会报错
3、必须保证其内部所有属性也必须是可序列化的(默认情况下,基本数据类型可序列化)
4、ObjectInputStream和ObjectOutputStream不能序列化static和transient修饰的成员变量
transient修饰的成员变量在序列化时,不会参与序列化,其值要么是整型的0,要么就是String类型的null
transient修饰String类型成员变量时序列化
transient修饰int类型成员变量时序列化:
一个基础的序列化和反序列化操作(针对字符串对象,非核心操作):
package com.woniusec.serial;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class BasicSerial {
public static void main(String[] args) throws Exception {
//序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("basicSerial.ser"));
oos.writeObject(new String("这是一条序列化字符串"));
oos.close();
//反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("basicSerial.ser"));
Object o = ois.readObject();
String s = (String) o;
ois.close();
System.out.println(s);
}
}
针对一个特定的类进行序列化和反序列化:
package com.woniuxy.core;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.jar.Attributes.Name;
public class Deseria {
public static void main(String[] args) throws Exception {
// 序列化//
Student student = new Student();//
student.name = "张三";//
student.id = 12345;//
student.phone = "13812345678";//
student.money = 9999;//
FileOutputStream fos = new FileOutputStream("./data/student.ser");//
ObjectOutputStream oos = new ObjectOutputStream(fos);//
oos.writeObject(student);//
fos.close();//
oos.close();
// 反序列化
FileInputStream fis = new FileInputStream("./data/student.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
Student obj = (Student)ois.readObject();
obj.study();
System.out.println(obj.name);
System.out.println(obj.money);
}
}
class Student implements Serializable {
private static final long serialVersionUID = 12345L;
// 用于反序列化时标识是否为同一个类
public String name = "";
public int id = 0;
public String phone = "";
transient int money = 0;
// transient修饰的变量不能被序列化
public Student() {
System.out.println("构造方法运行");
}
public void study() {
System.out.println("学生正在学习");
}
public void sleep() {
System.out.println("学生正在休息");
}
// 重写readObject方法,在反序列化时会优先调用,如果该方法存在可利用点,则可以自动触发
private void readObject(ObjectInputStream ois) throws Exception{
ois.defaultReadObject();
System.out.println("正在反序列化");
Runtime.getRuntime().exec("calc.exe");
}
}
2、反序列化漏洞成因
我们可以在类中重写 readObject 这个函数
一旦在类中重写了 readObject 函数之后,该类的实例化对象一旦被反序列化,那么肯定会首先自动调用该重写的 readObject 函数
比如我在 Student 类中 重写这样一个方法
private void readObject(ObjectInputStream ois) throws Exception { ois.defaultReadObject(); System.out.println("正在被反序列化ing..."); }
然后对 Student 类的某个实例进行序列化之后再进行反序列化:
由此可见,在反序列化的时候,第一时间自动调用重写的readObject函数
如果不想你的类被实例化,那么久将他的构造方法私有化
序列化指把Java对象转换为字节序列的过程,反序列化就是打开字节流并重构对象,那如果即将被反序列化的数据是特殊构造的,就可以产生非预期的对象,从而导致任意代码执行。
Java中间件通常通过网络接收客户端发送的序列化数据,而在服务端对序列化数据进行反序列化时,会调用被序列化对象的readObject( )方法。而在Java中如果重写了某个类的方法,就会优先调用经过修改后的方法。如果某个对象重写了readObject( )方法,且在方法中能够执行任意代码,那服务端在进行反序列时,也会执行相应代码。
如果能够找到满足上述条件的对象进行序列化并发送给Java中间件,Java中间件也会去执行指定的代码,即存在反序列化漏洞。
我们来看一下:
java 中执行命令我们学习了两种方式,分别是 Runtime 和 ProcessBuilder
首先是 Runtime
我们先来看一下Runtime的定义:
- 是
Runtime.getRuntime()
这个函数再执行指令,并不是 Runtime 再执行 - 但是我们看下面的图片,Runtime() 这个函数是private的,我们无法直接实例化,但是getRuntiem() 这个函数我们可以调用,而该函数会返回一个 currentRuntime 对象,该对象又是 Runtiemn类型的,所以我们应该加载一个动态类方法,而不是类
private void readObject(ObjectInputStream ois) throws Exception {
ois.defaultReadObject();
// System.out.println("正在被反序列化ing...");
// Runtime.getRuntime().exec("calc.exe");
// 如果我们在 该重写函数中 进行类反射机制来执行指令呢?
Class clazz = Class.forName("java.lang.Runtime");
Method m1 = clazz.getDeclaredMethod("getRuntime");
Object obj = m1.invoke(clazz,null);
Method m2 = clazz.getDeclaredMethod("exec",String.class);
m2.invoke(obj,"calc.exe");
}
- 所以使用类反射机制 动态加载一个类方法然后在动态加载方法指令(这里是 exec)
接下来看看 ProcessBuilder 方式,如何使用类反射机制来执行指令:
- 首先还是来看一下 ProcessBuilder 的定义
我们发现,ProcessBuilder 具有两个构造方法,但是,两个构造方法都需要传参,而且,所传参数要么是可变长String,要么是List数组,而Java中又刚好将可变长类型规划在数组中,所以,ProcessBuilder 必须要传入数组类型的参数,因此,在创建构造器的时候,我们给 getConteoller 函数传入的参数是 String[].class
- 然后 ProcessBuilder 真正开始执行的函数是 start 函数
所以 我们需要再使用类反射来获取 start 方法
private void readObject(ObjectInputStream ois) throws Exception {
ois.defaultReadObject();
System.out.println("正在被反序列化ing...");
ProcessBuilder 方式执行指令
Class clazz = Class.forName("java.lang.ProcessBuilder");
Constructor c = clazz.getConstructor(String[].class); // 由于该构造方法需要传入参数,所以必须要使用构造器来传参,普通的 newInstance 无法给构造函数传参
// 由于在创建 Controller 的时候,给的参数类型为 String[] 所以,我们需要把 calc.exe 放在一个数组中传入 newInstance
String[] cmd = {"calc.exe"};
Object obj = c.newInstance(cmd); // 给 ProcessBuilder 的构造方法传入要调用的指令,然后接收返回的对象
Method m1 = clazz.getDeclaredMethod("start"); //使用ProcessBuilder对象来获取方法
m1.invoke(obj,null); // invoke获取到的方法 start 函数不需要传参
}
但是这样执行是错的
- 因为我们现在给newInstance 传参是直接传入了变量,而不是值。那么我们再去看看 newInstance 的定义
wc,又是一个可变长数组,无敌了,所以我们在这里还需要将 cmd 这个固定的数组内容让它变得不固定,让一个固定长度的数组变成一个长度不固定的数组方法就是,将一维数组变成二维数组,长度确实不固定了,同时依旧保持了数组类型
private void readObject(ObjectInputStream ois) throws Exception {
ois.defaultReadObject();
System.out.println("正在被反序列化ing...");
ProcessBuilder 方式执行指令
Class clazz = Class.forName("java.lang.ProcessBuilder");
Constructor c = clazz.getConstructor(String[].class); // 由于该构造方法需要传入参数,所以必须要使用构造器来传参,普通的 newInstance 无法给构造函数传参
// 由于在创建 Controller 的时候,给的参数类型为 String[] 所以,我们需要把 calc.exe 放在一个数组中传入 newInstance
// String[] cmd = {"calc.exe"};
// 然后,又因为newInstance所需要传入的参数,又是一个可变长的Object对象,所以我们在这里要把这一个固定的数组编程二维数组,才能确保他是可变长的
String[][] cmd = {{"calc.exe"}};
Object obj = c.newInstance(cmd); // 给 ProcessBuilder 的构造方法传入要调用的指令,然后接收返回的对象
Method m1 = clazz.getDeclaredMethod("start"); //使用ProcessBuilder对象来获取方法
m1.invoke(obj,null); // invoke获取到的方法 start 函数不需要传参
}
- 最终运行结果
3、利用Base64对序列化数据进行转换
(1)将序列化二进制数据进行Base64编码,便于在网络上进行传输
File file = new File("./data/student.ser");
FileInputStream inputFile = new FileInputStream(file);
byte[] buffer = new byte[(int) file.length()];inputFile.read(buffer);
inputFile.close();byte[] file64 = Base64.getEncoder().encode(buffer);
System.out.println(new String(file64, "utf-8"));
(2)将接收到的Base64编码还原为序列化文件
File file = new File("./data/base64.ser");
FileOutputStream fos = new FileOutputStream(file);
String source = "rO0ABXNyABdjb20ud29uaXV4eS52dWwuU3R1ZGVudAAAAAAAADA5AgADSQACaWRMAARuYW1ldAASTGphdmEvbGFuZy9TdHJpbmc7TAAFcGhvbmVxAH4AAXhwAAAwOXQABuW8oOS4iXQACzEzODEyMzQ1Njc4";
byte[] content = Base64.getDecoder().decode(source.getBytes());
fos.write(content);fos.flush();fos.close();
(3)也可以将Base64字符串解码后直接在内存中反序列化
String b64str = "rO0ABXNyABdjb20ud29uaXV4eS52dWwuU3R1ZGVudAAAAAAAvGFOAgACTAAEbmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO0wABXBob25lcQB+AAF4cHQABuW8oOS4iXQACzE4ODEyMzQ1Njc4";
byte[] b64byte = Base64.getDecoder().decode(b64str);
ByteInputStream bis = new ByteInputStream(b64byte, b64byte.length);
ObjectInputStream ois = new ObjectInputStream(bis);
Student student = (Student) ois.readObject();student.study();
System.out.println(student.phone);