Java 提供了一种对象序列化的机制,该机制中,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。
将序列化对象写入文件之后,可以从文件中读取出来,并且对它进行反序列化,也就是说,对象的类型信息、对象的数据,还有对象中的数据类型可以用来在内存中新建对象。
Java 提供了两个类 java.io.ObjectOutputStream 和 java.io.ObjectInputStream 来实现序列化和反序列化的功能,其中 ObjectInputStream 用于恢复那些已经被序列化的对象,ObjectOutputStream 将 Java 对象的原始数据类型和图形写入 OutputStream。
常见类及类方法
序列化通常使用类ObjectOutputSteam,其中包含许多方法如下
writeObject
这是 ObjectOutputStream 对象的核心方法之一,用来将一个对象写入输出流中,任何对象,包括字符串和数组,都是用 writeObject 写入到流中的。
writeObject 方法负责为指定的类编写其对象的状态,以便在后面可以使用与之对应 readObject 方法来恢复它。
writeUnshared
用于将非共享对象写入 ObjectOutputStream,并将给定的对象作为刷新对象写入流中。
使用 writeUnshared 方法会使用 BlockDataOutputStream 的新实例进行序列化操作,不会使用原来 OutputStream 的引用对象。
writeObject0
writeObject 和 writeUnshared 实际上调用 writeObject0 方法,也就是说 writeObject0是上面两个方法的基础实现。具体的实现流程将会在后面再进行详细研究。
writeObjectOverride
如果 ObjectOutputStream 中的 enableOverride 属性为 true,writeObject 方法将会调用 writeObjectOverride,这个方法是由 ObjectOutputStream 的子类实现的。
在由完全重新实现 ObjectOutputStream 的子类完成序列化功能时,将会调用实现类的 writeObjectOverride 方法进行处理。
反序列化通常使用类ObjectInputSteam,其中包含许多方法如下
readObject
从 ObjectInputStream 读取一个对象,将会读取对象的类、类的签名、类的非 transient 和非 static 字段的值,以及其所有父类类型。
我们可以使用 writeObject 和 readObject 方法为一个类重写默认的反序列化执行方,所以其中 readObject 方法会 “传递性” 的执行,也就是说,在反序列化过程中,会调用反序列化类的 readObject 方法,以完整的重新生成这个类的对象。
readUnshared
从 ObjectInputStream 读取一个非共享对象。 此方法与 readObject 类似,不同点在于readUnshared 不允许后续的 readObject 和 readUnshared 调用引用这次调用反序列化得到的对象。
readObject0
readObject 和 readUnshared 实际上调用 readObject0 方法,readObject0是上面两个方法的基础实现。
readObjectOverride
由 ObjectInputStream 子类调用,与 writeObjectOverride 一致。
代码示例:
Person.java
import java.io.Serializable;
public class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void setName(){
System.out.println("name is " + this.name);
}
}
SerializableTest.java
import java.io.*;
public class SerializableTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Person person = new Person("zyer", 22);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("1.txt"));
oos.close();
FileInputStream fis = new FileInputStream("1.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
Object obj = ois.readObject();
System.out.println(obj);
ois.close();
}
}
序列化之后会生成一个二进制串,可以使用ysoserial生成一个URLDNS的payload
java -jar ysoserial.jar URLDNS "http://umulb1.dnslog.cn" | xxd
-
0xaced
,魔术头 -
0x0005
,版本号 (JDK主流版本一致,下文如无特殊标注,都以JDK8u为例) -
0x73
,对象类型标识 (0x7n
基本上都定义了类型标识符常量,但也要看出现的位置,毕竟它们都在可见字符的范围,详见java.io.ObjectStreamConstants
) -
0x72
,类描述符标识 -
0x0011...
,类名字符串长度和值 (Java序列化中的UTF8格式标准) -
0xd9353cf7d60ac6d5
,序列版本唯一标识 (serialVersionUID
,简称SUID) -
0x02
,对象的序列化属性标志位,如是否是Block Data模式、自定义writeObject()
,Serializable
、Externalizable
或Enum
类型等 -
0x0002
,类的字段个数 -
0x49
,整数类型签名的第一个字节,同理,之后的0x4c
为字符串类型签名的第一个字节 (类型签名表示与JVM规范中的定义相同) -
0x0008...
,字段名字符串长度和值,非原始数据类型的字段还会在后面加上数据类型标识、完整类型签名长度和值,如之后的0x740012...
-
0x78
Block Data结束标识 -
0x70
父类描述符标识,此处为null
-
0x00017df1
整数字段intField
的值 (Java序列化中的整数格式标准) ,非原始数据类型的字段则会按对象的方式处理,如之后的字符串字段stringField
被识别为字符串类型,输出字符串类型标识、字符串长度和值