Java序列化机制探究

0x00 前言

序列化是常见Java漏洞的起源机制,在学习了反射机制之后肯定是要再接着探索下序列化的机制。

0x01 什么是序列化

序列化是指把一个Java对象变成二进制内容,本质上就是一个byte[]数组。

序列化后可以把byte[]保存到文件中,或者把byte[]通过网络传输到远程,这样,就相当于把Java对象存储到文件或者通过网络传输出去了。

有序列化,就有反序列化,即把一个二进制内容(也就是byte[]数组)变回Java对象。有了反序列化,保存到文件中的byte[]数组又可以“变回”Java对象,或者从网络上读取byte[]并把它“变回”Java对象。

0x02 序列化有什么作用

根据原理特点,可以归纳为一下三点:

(1)永久性保存对象,保存对象的字节序列到本地文件或者数据库中;

(2)通过序列化以字节流的形式使对象在网络中进行传递和接收;

(3)通过序列化在进程间传递对象;

具体的一些应用场景:

(A) http参数,cookie,sesion,存储方式可能是base64(rO0),压缩后的base64(H4sl),MII等

(B) Servlets HTTP,Sockets,Session管理器 包含的协议就包括JMX,RMI,JMS,JNDI等(\xac\xed)

(C) xml Xstream,XMLDecoder等(HTTP Body:Content-Type:application/xml)

(D) json(Jackson,fastjson) http请求中包含

0x03 序列化机制的特性

在使用序列化时需要注意以下几个特性:

1、Java对象如果需要进行序列化,必须实现一个特殊的java.io.Serializable接口。{

Serializable接口没有定义任何方法,它是一个空接口。我们把这样的空接口称为“标记接口”(Marker Interface),实现了标记接口的类仅仅是给自身贴了个“标记”,并没有增加任何方法;

}

2、反序列化时,由JVM直接构造出Java对象,不调用构造方法,构造方法内部的代码,在反序列化时根本不可能执行。

3、若可序列化的类的成员不是基本类型,也不是String类型,那这个引用类型也必须是可序列化的(引用的这个类也必须继承Serializable接口)。

4、Java序列化同一对象,并不会将此对象序列化多次得到多个对象。{

Java序列化算法:

① 有保存到磁盘的对象都有一个序列化编码号;

② 当程序试图序列化一个对象时,会先检查此对象是否已经序列化过,只有此对象从未(在此虚拟机)被序列化过,才会将此对象序列化为字节序列输出;

③ 如果此对象已经序列化过,则直接输出编号即可;

}

5、如果序列化一个可变对象(对象内的内容可更改)后,更改了对象内容,再次序列化,并不会再次将此对象转换为字节序列,而只是保存序列化编号

6、被transient关键字修饰的字段,将不会被序列化。{

使用transient修饰的属性,java序列化时,会忽略掉此字段,所以反序列化出的对象,被transient修饰的属性是默认值。对于引用类型,值是null;基本类型,值是0;boolean类型,值是false;

}

7、如果该类的某个属性标识为static类型的,则该属性不能序列化

8、若被序列化的类有父类,则存在以下几种情况。{

① 如果该父类已经实现了可序列化接口,则其父类的相应字段及属性的处理和该类相同;

② 如果该类的父类没有实现可序列化接口,则该类的父类所有的字段属性将不会序列化,并且反序列化时会调用父类的默认构造函数来初始化父类的属性,而子类却不调用默认构造函数,而是直接从流中恢复属性的值;

当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口

}

0x04 实现序列化的过程

1、把java对象进行序列化

① 创建一个被序列化对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
文件名:Person.java

package serialize_test;

import java.io.Serializable;

public class Person implements Serializable {
private String name;
private int age;
//不提供无参构造器
public Person(String name, int age) {
System.out.println("若出现该提示,则说明构造函数被执行了");
this.name = name;
this.age = age;
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

② 创建一个ObjectOutputStream输出流;并调用ObjectOutputStream对象的writeObject输出可序列化对象到本地文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
文件名:WriteObject.java

package serialize_test;

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;

public class WriteObject {
public static void main(String[] args) throws Exception {
try (//创建一个ObjectOutputStream输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"))) {
//将对象序列化到文件s
Person person = new Person("shiyan", 1);
oos.writeObject(person);
oos.flush();
oos.close();
} catch (Exception e) {
e.printStackTrace();
}finally{
System.out.println("文件已被序列化写入本地文件中");
}

}
}

③ 控制台输出结果:

1
2
反序列化,你调用我了吗?
文件已被序列化写入本地文件中

此时文件已被成功写入本地文件中,我们打开看一下文件内容。

2、反序列化读取java对象内容

① 创建一个ObjectInputStream输入流,然后调用ObjectInputStream对象的readObject()得到序列化的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package serialize_test;

import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class WriteObject {
public static void main(String[] args) throws Exception {

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
Person brady = (Person) ois.readObject();
System.out.println(brady);

}
}

② 控制台输出结果:

1
Person{name='shiyan', age=1}

注:从这里可以看出来在反序列化的时候并不会被执行构造函数的代码。

0x05 自定义序列化过程

该章节一共有三个部分,分别是:常规自定义、另一种自定义、强制自定义。

1. 常规自定义

java提供了可选的自定义序列化。可以进行控制序列化的方式,或者对序列化数据进行其它操作等。

1
2
3
4
private void writeObject(java.io.ObjectOutputStream out) throws IOException;
private void readObject(java.io.ObjectInputStream in) throws IOException,ClassNotFoundException;
private void readObjectNoData() throws ObjectStreamException;
// 当序列化流不完整时,readObjectNoData()方法可以用来正确地初始化反序列化的对象。例如,使用不同类接收反序列化对象,或者序列化流被篡改时,系统都会调用readObjectNoData()方法来初始化反序列化的对象。

通过重写writeObject与readObject方法,可以自己选择哪些属性需要序列化, 哪些属性不需要。如果writeObject使用某种规则序列化,则相应的readObject需要相反的规则反序列化,以便能正确反序列化出对象。

1、创建一个可被序列化的类,并且重写其中的writeObject与readObject方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
文件名:OverrideClass.java

package serialize_test;

import java.io.IOException;
import java.io.Serializable;

public class OverrideClass implements Serializable {

public String name;

public OverrideClass(){

}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

private void readObject(java.io.ObjectInputStream in) throws IOException,ClassNotFoundException{
in.defaultReadObject(); //调用原始的readOject方法
Runtime.getRuntime().exec("calc");
System.out.println("重写readObject函数");
}

private void writeObject(java.io.ObjectOutputStream out) throws IOException{
out.defaultWriteObject();
System.out.println("重写writeObject函数");
}

}

2、对该类进行序列化写入文件,并再反序列化读取内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
文件名:ReaderClass.java

package serialize_test;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class ReaderClass {

public static void main(String[] args) throws Exception {
ObjectOutputStream Ar1 = new ObjectOutputStream(new FileOutputStream("shiyan.txt"));
OverrideClass Ar2 = new OverrideClass();
Ar2.setName("shiyan");
Ar1.writeObject(Ar2);
Ar1.flush();
Ar1.close();
System.out.println("---------分割线-------------");

ObjectInputStream Br1 = new ObjectInputStream(new FileInputStream("shiyan.txt"));
OverrideClass Br2 = (OverrideClass) Br1.readObject();
System.out.println(Br2.getName());
}

}

3、控制台输出结果:

1
2
3
4
重写writeObject函数
---------分割线-------------
重写readObject函数
shiyan

4、效果图:

2. 另一种自定义

除了上述常规的自定义还有一种自定义方法:

1
2
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;

writeReplace:在序列化时,会先调用此方法,再调用writeObject方法。此方法可将任意对象代替目标序列化对象。

readResolve:反序列化时替换反序列化出的对象,反序列化出来的对象被立即丢弃。此方法在readeObject后调用。

注意:readResolve与writeReplace的访问修饰符可以是private、protected、public,如果父类重写了这两个方法,子类都需要根据自身需求重写,这显然不是一个好的设计。通常建议对于final修饰的类重写readResolve方法没有问题;否则,重写readResolve使用private修饰。

1、创建一个可被序列化的类,并且重写其中的readResolve与writeReplace方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
文件名:OverrideClass1.java

package serialize_test;

import java.io.IOException;
import java.io.ObjectStreamException;
import java.io.Serializable;

public class OverrideClass1 implements Serializable {

public String name;

public OverrideClass1(){

}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

private Object readResolve() throws ObjectStreamException,IOException{
Runtime.getRuntime().exec("calc");
System.out.println("重写readResolve函数");
OverrideClass1 Dr1 = new OverrideClass1();
String Dr2 = "读取的本地序列化对象,已经被替换为自定义的Dr1了";
Dr1.setName(Dr2);
return Dr1;
}

private Object writeReplace() throws ObjectStreamException{
OverrideClass1 Cr1 = new OverrideClass1();
Cr1.name = "原始将被写入的对象已经变成自定义的Cr1了";
System.out.println("重写writeReplace函数");
return Cr1;
}

}

2、对该类进行序列化写入文件,并再反序列化读取内容;这里需要注意的是因为readResolve与writeReplace方法的是相冲突的,在实际运行时我们要注释一掉1个,才能测试出具体的变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
文件名:ReaderClass1.java

package serialize_test;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class ReaderClass1 {

public static void main(String[] args) throws Exception {
ObjectOutputStream Ar1 = new ObjectOutputStream(new FileOutputStream("shiyan1.txt"));
OverrideClass1 Ar2 = new OverrideClass1();
Ar2.setName("shiyan");
Ar1.writeObject(Ar2);
Ar1.flush();
Ar1.close();
System.out.println("---------分割线-------------");

ObjectInputStream Br1 = new ObjectInputStream(new FileInputStream("shiyan1.txt"));
OverrideClass1 Br2 = (OverrideClass1) Br1.readObject();
System.out.println(Br2.getName());
}

}

3、控制台输出结果:

注释掉 writeReplace() 的运行结果:

1
2
3
---------分割线-------------
重写readResolve函数
读取的本地序列化对象,已经被替换为自定义的Dr1了

效果图:

注释掉 readResolve() 的运行结果:

1
2
3
重写writeReplace函数
---------分割线-------------
原始将被写入的对象已经变成自定义的Cr1了

3. 强制自定义

通过实现Externalizable接口,必须实现writeExternal、readExternal方法。

1
2
3
4
5
6
文件名(接口文档):Externalizable.class

public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

注意:Externalizable接口不同于Serializable接口,实现此接口必须实现接口中的两个方法实现自定义序列化,这是强制性的;特别之处是必须提供pulic的无参构造器,因为在反序列化的时候需要反射创建对象。其次使用Externalizable时,必须按照写入时的确切顺序读取所有字段状态。否则会产生异常。例如,如果更改继承该Externalizable的Demo类中的name和age属性的读取顺序,则将抛出java.io.EOFException。而Serializable接口没有这个要求。

1、创建一个可被序列化的类,并且重写其中的readExternal与writeExternal方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
文件名:OverrideClass2.java

package serialize_test;


import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

public class OverrideClass2 implements Externalizable {

public String name;

public OverrideClass2(){

}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}


public void writeExternal(ObjectOutput out) throws IOException{
out.writeObject(name);
System.out.println("----执行 writeExternal 方法------");
}

public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException{
this.name=(String)in.readObject();
System.out.println("----执行 readExternal 方法------");
}

}

2、对该类进行序列化写入文件,并再反序列化读取内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
文件名:ReaderClass2.java

package serialize_test;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class ReaderClass2 {

public static void main(String[] args) throws Exception {
ObjectOutputStream Ar1 = new ObjectOutputStream(new FileOutputStream("shiyan2.txt"));
OverrideClass2 Ar2 = new OverrideClass2();
Ar2.setName("shiyan");
Ar1.writeObject(Ar2);
Ar1.flush();
Ar1.close();
System.out.println("---------分割线-------------");

ObjectInputStream Br1 = new ObjectInputStream(new FileInputStream("shiyan2.txt"));
OverrideClass2 Br2 = (OverrideClass2) Br1.readObject();
System.out.println(Br2.getName());
}

}

3、控制台输出结果:

1
2
3
4
----执行 writeExternal 方法------
---------分割线-------------
----执行 readExternal 方法------
shiyan

0x06 什么是 serialVersionUID

serialVersionUID适用于java序列化机制。简单来说,JAVA序列化的机制是通过判断类的serialVersionUID来验证的版本一致的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID于本地相应实体类的serialVersionUID进行比较。如果相同说明是一致的,可以进行反序列化,否则会出现反序列化版本一致的异常,即是InvalidCastException。

具体的序列化过程:序列化操作的时候系统会把当前类的serialVersionUID写入到序列化文件中,当反序列化时系统会去检测文件中的serialVersionUID,判断它是否与当前类的serialVersionUID一致,如果一致就说明序列化类的版本与当前类版本是一样的,可以反序列化成功,否则失败。

serialVersionUID有两种显示的生成方式 {

一是默认的1L,比如:private static final long serialVersionUID = 1L;

二是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,比如:

private static final long serialVersionUID = xxxxL;

}

0x07 反序列攻击时序图

序列化的机制到这里已经了解的差不多了,下面贴一张廖新喜大佬在某次安全大会上的反序列攻击时序图,做为一个漏洞攻击了解。

0x08 参考文章

[1] https://www.cnblogs.com/9dragon/p/10901448.html

[2] https://www.liaoxuefeng.com/wiki/1252599548343744/1298366845681698

[3] http://xxlegend.com/2018/06/20/%E5%85%88%E7%9F%A5%E8%AE%AE%E9%A2%98%20Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%AE%9E%E6%88%98%20%E8%A7%A3%E8%AF%BB/

[4] http://code2sec.com/javafan-xu-lie-hua-lou-dong-xue-xi-shi-jian-yi-cong-serializbalejie-kou-kai-shi-xian-dan-ge-ji-suan-qi.html