Java的序列化:
与php的序列化反序列化一样,Java的序列化从理解上来看大同小异,都将对象以一连串的字节保存在磁盘文件中的过程,而反序列化也一样是将保存在磁盘文件中的java字节码重新转换成java对象的过程。(默认大家对php序列化有基本概念,以及对“类”有一定的知识基础,如果没有,可以看看我的基于js的原型链污染)
如何实现:
使用JDK类库提供的序列化API:“java.io.ObjectOutputStream”和“java.io.ObjectInputStream”
java.io.ObjectOutputStream表示对象输出流,其中writeObject(Object obj)方法可以将给定参数的obj对象进行序列化,将转换的一连串的字节序列写到指定的目标输出流中。
java.io.ObjectInputStream该类表示对象输入流,该类下的readObject(Object obj)方法会从源输入流中读取字节序列,并将它反序列化为一个java对象并返回。
有点像php的魔术方法,但又不是一个东西。
这两个类负责对象的输入输出,但要实现序列化还需要一个Serializable接口,该接口用于标记某个类可以被序列化。如果不使用标记接口,序列化过程会报错。
Java类基础:
一下代码实现了一个Java的类应用。
// Address.java
import java.io.Serializable;
public class Address implements Serializable {
private String street;
private String city;
public Address(String street, String city) {
this.street = street;
this.city = city;
}
public String getStreet() {
return street;
}
public String getCity() {
return city;
}
}
// Person.java
import java.io.Serializable;
public class Person implements Serializable {
private String name;
private Address address;
public Person(String name, Address address) {
this.name = name;
this.address = address;
}
public String getName() {
return name;
}
public Address getAddress() {
return address;
}
}
//Main.java
import com.example.Person;
import com.example.Address;
public class Main {
public static void main(String[] args) {
Address address = new Address("Main Street", "City");
Person person = new Person("John Doe", address);
System.out.println("Name: " + person.getName());
System.out.println("Address: " + person.getAddress().getStreet() + ", " + person.getAddress().getCity());
}
}
首先我们定义了一个公共类:Address类,并单独放入一个Java文件中(在Java中,每个公共的自定义类都要单独写入一个Java文件,如果在一个文件中写入了多个公共类,那么大概率会导致代码执行出错)。import java.io.Serializable用于后续序列化(当前没用),public class Address implements Serializable 定义了Address类并实现了Serializable接口(为序列化铺垫),在类中定义了两个私有字符串属性street和city, public Address(String street, String city)对Address类的实现, public String getStreet() {return street;}定义方法getStreet()获取对象的属性值,getCity()同理。
定义的Person类同理,不同的是Person类的属性中引用了Address类。(在接下来的序列化中,当一个类A引用了其他类B的对象时,若A的对象被序列化,则B在A内的对象也会被序列化,所以说序列化是递归进行的)
Main:接收字符串作为参数,创建对象address和person并输出。结果如下:
Name: John Doe
Address: Main Street, City
以上就是对Java的类进行一个简单的应用。
如何序列化:
import java.io.*;
//import java.io.FileOutputStream;
//import java.io.ObjectOutputStream;
//import java.io.IOException;
//import java.io.Serializable;
public class SerializationExample {
public static void main(String[] args) {
Address address = new Address("123 Main St", "New York");
Person person = new Person("Alice", address);
try {
FileOutputStream fileOut = new FileOutputStream("person.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(person);
out.close();
fileOut.close();
System.out.println("Serialized data is saved in person.ser");
} catch (IOException e) {
e.printStackTrace();
}
}
}
因为需要用到//import java.io.FileOutputStream;//import java.io.ObjectOutputStream;//import java.io.IOException;//import java.io.Serializable;所以直接引用所有的io类/接口即import java.io.*;之后定义了一个公共类SerializationExample,创建新的Address和Person对象并初始化传参(为了能调用没有被实例化的main函数,需要定义为静态方法)。接下来的语句涉及到FileOutputStream和ObjectOutputStream类,他们可能抛出I/O异常的风险,所以要使用try/catch进行异常情况的处理。main中创建了一个person.ser文件,并通过writeObject把序列化的结果输出在该文件中。结果:
打开查看:
发现是乱码,很正常,在 Java 中,对象序列化后的结果是一种二进制数据,使用文本文档打开会看到一些乱码内容。
接下来是反序列化:
首先要对Person类和Address类的代码进行改造:
// Address.java
import java.io.Serializable;
public class Address implements Serializable {
private String street;
private String city;
public Address(String street, String city) {
this.street = street;
this.city = city;
}
public String getStreet() {
return street;
}
public String getCity() {
return city;
}
//以下新加入的代码
@Override
public String toString() {
return "Address{" +
"street='" + street + '\'' +
", city='" + city + '\'' +
'}';
}
}
// Person.java
import java.io.Serializable;
public class Person implements Serializable {
private String name;
private Address address;
public Person(String name, Address address) {
this.name = name;
this.address = address;
}
public String getName() {
return name;
}
public Address getAddress() {
return address;
}
//以下是新加入的代码
@Override
public String toString() {
return "Person{name='" + name + "', address='" + address + "'}";
}
}
@Override是重写标识,这里需要对toString()方法进行重写,写成固定格式的字符串进行输出,如果不进行重写,在调用toString方法时会默认调用 Object 类中的 toString() 方法,并返回包含类名和哈希码的默认字符串表示。接下来写入反序列化代码:
import java.io.*;
public class DeserializationExample {
public static void main(String[] args) {
try {
FileInputStream fileIn = new FileInputStream("person.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
Person person = (Person) in.readObject();
in.close();
fileIn.close();
System.out.println("Deserialized data:");
System.out.println(person);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
改代码调用了person.ser文件,将其序列化的 结果进行反序列化然后输出到控制台:
如果在定义类的代码中不对toString类进行重写,那么输出应该是这样的:
默认输出了类名和后面的哈希码。
其他注意:
已经初步了解了Java的反序列化的原理,接下来介绍一些在Java序列化中的一些注意事项:
transient:在 Java 中,transient
是一个修饰符,用于标记类的成员变量,表示不希望被序列化。当一个对象被序列化时,会将其状态保存为字节流,transient
可以用于排除某些成员变量,使其在序列化过程中被忽略。比如我们基于Address类,把city改为transient
:
private transient String city;
再次进行序列化,然后反序列化:
city没有被序列化,因此Address类的city属性是空的。
Externalizable接口:除了使用Serializable接口外,还可以使用Externalizable接口,Externalizable接口相比于Serializable接口更加灵活。Serializable接口使用默认序列化方式,可以序列化所有非transient(即非瞬态)字段,无需编写任何额外的代码。而若使用Externalizable接口使用自定义序列化方式,需要手动控制序列化和反序列化过程。就好比要保存一本书,Serializable接口是直接将整本书存入书架,而Externalizable接口是可以手动地将其中一些需要地部分保存起来放入暑假。使用原来的例子改为Externalizable方式:
//Address
import java.io.*;
public class Address implements Externalizable {
private String street;
private String city;
public Address() {
}
public Address(String street, String city) {
this.street = street;
this.city = city;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(street);
out.writeObject(city);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
street = (String) in.readObject();
city = (String) in.readObject();
}
public String getStreet() {
return street;
}
public String getCity() {
return city;
}
@Override
public String toString() {
return "Address{street='" + street + "', city='" + city + "'}";
}
}
//Person
import java.io.*;
public class Person implements Externalizable {
private String name;
private Address address;
public Person() {
}
public Person(String name, Address address) {
this.name = name;
this.address = address;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeObject(address);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
address = (Address) in.readObject();
}
public String getName() {
return name;
}
public Address getAddress() {
return address;
}
@Override
public String toString() {
return "Person{name='" + name + "', address='" + address + "'}";
}
}
和使用Serializable接口相比,主要有以下不同:
1.增加了额外的无参数构造方法,如
public Person() {
}
这是因为使用Externalizable接口时需要用到一个无参数的构造,与Serializable接口相比,Externalizable接口更灵活,可以自己选择需要被序列化的对象,因此在使用前需要一个空白构造。
2.增加了
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeObject(address);
}
通过传递一个实现了 ObjectOutput
接口的对象(out),writeExternal()
方法可以利用这个对象的方法来完成对象的序列化过程。这个过程可能会引发I/O异常,所以要进行异常处理。方法内可以手动选择被序列化的对象。
3.增加了
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
address = (Address) in.readObject();
}
通过传递一个实现了 ObjectOutput
接口的对象(out),writeExternal()
方法可以利用这个对象的方法来完成对象的反序列化过程。这个过程可能会引发I/O异常或者类加载可能出现的异常,所以要进行异常处理。方法内可手动选择被反序列化的对象。
序列化类,反序列化类的代码和之前一样,没有区别。
注意:若使用Externalizable接口,可以手动选择对象是否被序列化/反序列化,不需要像Serializable接口一样给不被序列化/反序列化的对象的类型加上transient,就算加上了也没用:
public class Address implements Externalizable {
private String street;
private transient String city;
输出:
漏洞产生:
示例:
import java.io.*;
class test{
public static void main(String[] args) throws Exception{
MyObject myObj = new MyObject();
FileOutputStream out1 = new FileOutputStream("password.ser");
ObjectOutputStream out2 = new ObjectOutputStream(out1);
out2.writeObject(myObj);
out2.close();
FileInputStream in1 = new FileInputStream("password.ser");
ObjectInputStream in2 = new ObjectInputStream(in1);
in2.readObject();
in2.close();
}
}
class MyObject implements Serializable{
public String flag;
private void readObject(ObjectInputStream flag) throws IOException, ClassNotFoundException{
flag.readObject();
System.out.println("打开了D盘的flag");
Runtime.getRuntime().exec("explorer.exe D:\\flag.txt");
}
}
由于 MyObject
类实现了 Serializable
接口并定义了 private void readObject(ObjectInputStream flag)
方法,因此在新对象反序列化过程中,Java 虚拟机会自动调用 readObject(ObjectInputStream flag)
方法,并将反序列化流作为参数传入。因此当MyObject
类作为一个恶意类被写入到一个正常的序列化代码中时,就会引发命令执行等危险后果,上述代码的运行结果:
漏洞判断:
用16进制打开序列化结果:
都有共同点,都以AC ED开头,版本号一般时00 05。可以作为判断Java序列化的依据。