在学习java的过程中,了解java中常见的安全漏洞,本文记录了java反序列化漏洞的学习。
什么是java序列化和反序列化?
Java 序列化(Serialization)是指把Java对象保存为二进制字节码的过程,是把 Java 对象转换为字节序列的过程便于保存在内存、文件、数据库中,ObjectOutputStream类的 writeObject() 方法可以实现序列化。
Java 反序列化(deserialization)是指把二进制码重新转换成Java对象的过程。把字节序列恢复为 Java 对象的过程,ObjectInputStream 类的 readObject() 方法用于反序列化。
什么情况下需要序列化
a.当你想把的内存中的对象保存到一个文件中或者数据库中时候;
b.当你想用套接字在网络上传送对象的时候;
c.当你想通过RMI传输对象的时候;
总之,序列化的用途就是传递和存储。
如何实现序列化
将需要序列化的类实现Serializable接口就可以了,Serializable接口中没有任何方法,可以理解为一个标记,即表明这个类可以被序列化。
序列化与反序列化都可以理解为“写”和“读”操作 ,通过如下这两个方法可以将对象实例进行“序列化”与“反序列化”操作。
/**
* 写入对象内容
*/
private void writeObject(java.io.ObjectOutputStream out)
/**
* 读取对象内容
*/
private void readObject(java.io.ObjectInputStream in)
一些注意点
当然,并不是一个实现了序列化接口的类的所有字段及属性,都是可以序列化的:
如果该类有父类,则分两种情况来考虑:
1.如果该父类已经实现了可序列化接口,则其父类的相应字段及属性的处理和该类相同;
2.如果该类的父类没有实现可序列化接口,则该类的父类所有的字段属性将不会序列化,并且反序列化时会调用父类的默认构造函数来初始化父类的属性,而子类却不调用默认构造函数,而是直接从流中恢复属性的值。
如果该类的某个属性标识为static类型的,则该属性不能序列化。
如果该类的某个属性采用transient关键字标识,则该属性不能序列化。
a.当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口;
b.当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化;
为了演示序列化在Java中是怎样工作的,我将使用之前教程中提到的Employee类,假设我们定义了如下的Employee类,该类实现了Serializable 接口。
public class Employee implements java.io.Serializable
{
public String name;
public String address;
public transient int SSN;
public int number;
public void mailCheck()
{
System.out.println("Mailing a check to " + name
+ " " + address);
}
}
请注意,一个类的对象要想序列化成功,必须满足两个条件
该类必须实现 java.io.Serializable 对象
该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明是短暂的
如果你想知道一个 Java 标准类是否是可序列化的,请查看该类的文档。检验一个类的实例是否能序列化十分简单, 只需要查看该类有没有实现 java.io.Serializable接口。
序列化对象
ObjectOutputStream 类用来序列化一个对象,如下的 SerializeDemo 例子实例化了一个 Employee 对象,并将该对象序列化到一个文件中
该程序执行后,就创建了一个名为 employee.ser 文件。该程序没有任何输出,但是你可以通过代码研读来理解程序的作用
注意: 当序列化一个对象到文件时, 按照 Java 的标准约定是给文件一个 .ser 扩展名。
import java.io.*;
public class SerializeDemo
{
public static void main(String [] args)
{
Employee e = new Employee();
e.name = "Reyan Ali";
e.address = "Phokka Kuan, Ambehta Peer";
e.SSN = 11122333;
e.number = 101;
try
{
FileOutputStream fileOut =
new FileOutputStream("D:/Download/employee.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(e);
out.close();
fileOut.close();
System.out.printf("Serialized data is saved in D:/Download/employee.ser");
}catch(IOException i)
{
i.printStackTrace();
}
}
}
程序执行成功,看一下保存的对象文件。
然后看一下如何将文件中的字符串反序列化为java对象。
反序列化对象
下面的 DeserializeDemo 程序实例了反序列化,D:/Download/employee.ser存储了 Employee 对象。
import java.io.*;
public class DeserializeDemo
{
public static void main(String [] args)
{
Employee e = null;
try
{
FileInputStream fileIn = new FileInputStream("D:/Download/employee.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
e = (Employee) in.readObject();
in.close();
fileIn.close();
}catch(IOException i)
{
i.printStackTrace();
return;
}catch(ClassNotFoundException c)
{
System.out.println("Employee class not found");
c.printStackTrace();
return;
}
System.out.println("Deserialized Employee...");
System.out.println("Name: " + e.name);
System.out.println("Address: " + e.address);
System.out.println("SSN: " + e.SSN);
System.out.println("Number: " + e.number);
}
}
执行结果:
这里要注意以下要点:
readObject() 方法中的 try/catch代码块尝试捕获 ClassNotFoundException 异常。对于 JVM 可以反序列化对象,它必须是能够找到字节码的类。如果JVM在反序列化对象的过程中找不到该类,则抛出一个 ClassNotFoundException 异常
注意,readObject() 方法的返回值被转化成 Employee 引用
当对象被序列化时,属性 SSN 的值为 111222333,但是因为该属性是短暂的,该值没有被发送到输出流。所以反序列化后 Employee 对象的 SSN 属性为 0。
自定义序列化和反序列化过程,就是重写writeObject和readObject方法
加入readObject方法的重写,再重写函数中加入自己的代码逻辑。
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class deserTest2 implements Serializable {
/**
* 创建一个简单的可被序列化的类,它的实例化后的对象就是可以被序列化的。
* 然后重写readObject方法,实现弹计算器。
*/
private static final long serialVersionUID = 1L;
private int n;
public deserTest2(int n){ //构造函数,初始化时执行
this.n=n;
}
//重写readObject方法,加入了弹计算器的执行代码的内容
private void readObject(java.io.ObjectInputStream in) throws IOException,ClassNotFoundException{
in.defaultReadObject();//调用原始的readOject方法
Runtime.getRuntime().exec("calc.exe");
System.out.println("test");
}
public static void main(String[] args) {
//deserTest2 x = new deserTest2(5);//实例一个对象
//operation2.ser(x);//序列化
operation2.deser();//反序列化
}
}
class operation2 {
public static void ser(Object obj) {
//序列化操作,写数据
try{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.obj"));
//ObjectOutputStream能把Object输出成Byte流
oos.writeObject(obj);//序列化关键函数
oos.flush(); //缓冲流
oos.close(); //关闭流
} catch (FileNotFoundException e)
{
e.printStackTrace();
} catch (IOException e)
{
e.printStackTrace();
}
}
public static void deser() {
//反序列化操作,读取数据
try {
File file = new File("object.obj");
ObjectInputStream ois= new ObjectInputStream(new FileInputStream(file));
Object x = ois.readObject();//反序列化的关键函数
System.out.print(x);
ois.close();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
代码中,我们先取消main函数中实例化对象的注释,去实例化对象进行序列化保存在object.obj中,然后在反序列化恢复对象时会触发readObject方法弹出计算器。
这里需要注意:只有实现了Serializable接口的类的对象才可以被序列化,Serializable 接口是启用其序列化功能的接口,实现 java.io.Serializable 接口的类才是可序列化的,没有实现此接口的类将不能使它们的任一状态被序列化或逆序列化。这里的 readObject() 执行了 Runtime.getRuntime().exec(“calc.exe”),而 readObject() 方法的作用正是从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回,readObject() 是可以重写的,可以定制反序列化的一些行为。