Java web学习之路-序列化和反序列化

0x01 序言

  步入新的一年,已经差不多快过去1/4年了,感觉自己啥都没学除了皮。所以现在立个flag,整理一些以前学过的东西,温故知新在学点新东西。那么这个第一篇就从反序列化开始。当然我觉得java难就难在读起来需要一点点的基础,我也试着尽量用通俗的语言去写。

0x02 正言

一、序列化

  现在的大环境由于CTF或者攻防赛的兴起,导致了大多数人关注的是PHP下的一些问题,自从工作之后就发现,在企业中Java还是占据了大多数。
  Java 是运行在 JVM(java虚拟机) 之上的一种语言,我们通过命令行 javac 生成的字节码格式的类文件在任何平台的 JVM 上都可以运行而 JVM(java虚拟机) 运行时会解释类文件中的命令。以前大家总会开玩笑,没有对象自己 new 一个就好了,在 Java 下对象这个概念很重要,java允许我们在内存中创建可复用的对象,但是一般情况下,只有当 JVM 属于运行状态时,这些对象才能够存在,也就是说这些对象的生命周期没有JVM的生命周期来的长。那如果我们需要保证即使在 JVM 停止运行的情况下,也能够保存相关制定对象,并且在将来某个时刻能够被读取是用得到, Java 的序列化正是为了解决这一需求而产生的。

  Java 序列化是指把 Java 对象转换为字节序列的过程便于保存在内存、文件、数据库中,ObjectOutputStream类的 writeObject() 方法可以实现序列化。
  Java 反序列化是指把字节序列恢复为 Java 对象的过程,ObjectInputStream 类的 readObject() 方法用于反序列化。

  序列化与反序列化是让 Java 对象脱离 Java 运行环境的一种手段,可以有效的实现多平台之间的通信、对象持久化存储。
  java中的一个类的对象要想序列化成功,必须满足两个条件:

  1. 该类必须实现 java.io.Serializable 接口,因为 Serializable 接口是启用其序列化功能的接口。
  2. 该类的所有属性必须是可序列化的。

  如果你想知道一个 Java 标准类是否是可序列化的,可以通过查看该类的文档,查看该类有没有实现 java.io.Serializable 接口。
  序列化可以理解为”写”,通过下面这个方法可以将对象实例进行”序列化”操作。

1
2
3
4
/**
* 写入对象内容
*/
private void writeObject(java.io.ObjectOutputStream out)

我们看一个例子:
先实现 Serializable 接口创建一个 employee

1
2
3
4
5
6
7
8
9
10
11
import java.io.Serializable;

//创建测试类,注意要实现Serializable接口
public class Employee implements Serializable{
public String name;
public String identify;
public void mailCheck()
{
System.out.println("This is the "+this.identify+" of our company");
}
}

序列化生成 employee1.obj

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
//引入必要的java包文件
import java.io.*;

//主类
public class SerializableStep{
//测试主类
public static void main(String [] args)
{
Employee e = new Employee();
e.name = "l1nk3r";
e.identify = "l1nk3r";
try
{
// 打开一个文件输入流
FileOutputStream fileOut =
new FileOutputStream("/Users/l1nk3r/Desktop/employee1.obj");
// 建立对象输入流
ObjectOutputStream out = new ObjectOutputStream(fileOut);
//输出反序列化对象
out.writeObject(e);
out.close();
fileOut.close();
System.out.printf("Serialized data is saved in /Users/l1nk3r/Desktop/employee1.obj");
}catch(IOException i)
{
i.printStackTrace();
}
}
}

  通过运行代码,我们在目录下生成了一个employee1文件,并且将我们需要序列化的字符串l1nk3r写入。
1

这里需要注意一点的是aced0005是 java 序列化内容的特征。相关序列化等字节码特征可移步先知-浅析Java序列化和反序列化
这里其实还是想再说一点的是,我们看到上图中存在 java.lang.String ,其实这个类继承了 java.lang.Object ,并且实现了 Serializable ,因此在对 String 类的对象进行序列化,相关内容才会保存在里面。

实现 Serializable 接口

  ava的序列化常见一般是通过两种方式,一种是实现 Serializable 接口,另一种是实现 Externalizable 接口。关于第一种 Serializable 接口的方法,上面的例子也介绍了,但是这里有几点注意事项:

  1. 在序列化时,只保存实例变量,静态变量不保存。
  2. 当一个类的父类是实现了Seriazable时,子类自动是Serializable的。如enum类型,自动继承java.lang.Enum,而Enum是Serializable的,则enum也是可序列化的
  3. 在序列化时,同时也会保存private变量,而存在安全方面的问题
  4. 序列化时,不会序列化transient修饰的变量。
  5. 在反序列化构造出实例变量时,不会调用对象的任何构造方法。

  当然在我们实现过程中,总有一些内容,不希望它进行序列化,下面来看一下。

transient关键字

  transient 关键字的作用就是用来告知JAVA我不可以被序列化,而静态变量也不会被保存,下面看个例子。
  ObjectSerTest1.java 文件在 password 字段增加了关键字 transient ,并且将 count 设置为静态变量,由于我们前面所过静态变量不会被保存,带有关键字 transient 的变量不会被序列化。

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
//ObjectSerTest1.java
import java.io.*;

class Student1 implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private transient String password; //transient关键字
private static int count = 0; //静态变量

public Student1(String name, String password) {
System.out.println("调用Student的带参的构造方法");
this.name = name;
this.password = password;
count++;
}

public String toString() {
return "人数: " + count + " 姓名: " + name + " 密码: " + password;
}
}

public class ObjectSerTest1 {
public static void main(String args[]) {
try {
FileOutputStream fos = new FileOutputStream("test.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
Student1 s1 = new Student1("张三", "12345");
Student1 s2 = new Student1("王五", "54321");
oos.writeObject(s1);
oos.writeObject(s2);
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

  我们看一下下面这个反序列化的结果, password 字段的值应该为 nullcount 字段的值应该为 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//Test.java
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class Test{
public static void main(String args[]){
try {
FileInputStream fis = new FileInputStream("test.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
Student1 s3 = (Student1) ois.readObject();
Student1 s4 = (Student1) ois.readObject();
System.out.println(s3);
System.out.println(s4);
ois.close();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e1) {
e1.printStackTrace();
}
}
}

4
  如果将 count 字段前面的 static 去掉,由于调用一次 student 类,因此 count 的值应该为 1 ,我们可以验证一下,结果如图所示。
5

实现 Externalizable 接口

  翻一下 Externalizable 接口的实现,实际上该接口也是继承了 java 序列化的接口。

1
2
3
4
public interface Externalizable extends java.io.Serializable {
/**
* The object implements the writeExternal method to save its contents
* by calling the methods of DataOutput for its primitive values or

  而其在继承的基础上增加了两个方法如下:

1
2
3
void writeExternal(ObjectOutput out) throws IOException;

void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;

  首先,我们在序列化对象的时候,由于这个类实现了 Externalizable 接口,在 writeExternal() 方法里定义了哪些属性可以序列化,哪些不可以序列化,所以,对象在经过这里就把规定能被序列化的序列化保存文件,不能序列化的不处理,然后在反序列的时候自动调用 readExternal() 方法,根据序列顺序挨个读取进行反序列,并自动封装成对象返回,然后在测试类接收,就完成了反序列。但是如果 writeExternal()readExternal() 方法未作任何处理,那么该序列化行为将不会保存/读取任何一个字段。
  这里有个注意点:通过继承 Externalizable 接口实现序列化时,不管成员变量有没有用 transient 关键字修饰,都不会影响,具体的序列化由 writeExternalreadExteranl 实现决定的,下面我们看个例子。由于代码太长了,选取部分相关代码,后续本次实验涉及代码会同步到github。

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
//Person.java
/**
* 测试实体类
*/
class Person implements Externalizable{
private static final long serialVersionUID = 1L;
transient String userName;
String password;
String age;

/**
* 序列化操作的扩展类
*/
@Override
public void writeExternal(ObjectOutput out) throws IOException {
//增加一个新的对象
Date date=new Date();
out.writeObject(userName);
out.writeObject(password);
out.writeObject(date);
}

/**
* 反序列化的扩展类
*/
@Override
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {
//注意这里的接受顺序是有限制的哦,否则的话会出错的
// 例如上面先write的是A对象的话,那么下面先接受的也一定是A对象...
userName=(String) in.readObject();
password=(String) in.readObject();
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd");
Date date=(Date)in.readObject();
System.out.println("反序列化后的日期为:"+sdf.format(date));

}
}

  首先在代码 第7行 ,我们针对 userName 变量增加了关键字 transient ,然后在序列化以及反序列化的过程中,我们不读取 age 变量,那么实际上最后的结果如下所示:

6

  也就是应验了我们前面针对通过继承 Externalizable 接口实现序列化时,所说的内容。

不管成员变量有没有用 transient 关键字修饰,都不会影响,具体的序列化由 writeExternalreadExteranl 实现决定的

  如果想要在反序列化的时候获取到age字段,那么就需要在序列化的过程以及反序列化的过程中增加下图中的内容。
7

二、反序列化

  好了上面聊完序列化的实现,现在该聊聊反序列化了。
  反序列化的过程是指把字节序列恢复为 Java 对象的过程。我们可以将反序列化理解为一个”读”操作,通过这个方法可以将对象实例进行”反序列化”操作。

1
2
3
4
/**
* 读取对象内容
*/
private void readObject(java.io.ObjectInputStream in)

  那我们继续看个例子,我们看能不能把我们刚刚序列化生成内容读回来。

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
//引入必要的java包文件
import java.io.*;

public class unSerializableStep{
public static void main(String [] args)
{
Employee e = null;
try
{
// 打开一个文件输入流
FileInputStream fileIn = new FileInputStream("/Users/l1nk3r/Desktop/employee1.obj");
// 建立对象输入流
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("This is the "+e.identify+" of our company");
}
}

2

三、弹个计算器吧

  前面我们能知道序列化过程依赖于 ObjectOutputStream 类中 writeObject 方法,而反序列化的过程是依赖于 ObjectOutputStream 类中 readObject 方法。那么如果实际情况下,我们能够重写 readObject 方法,那么就有可能达到反序列化的时候命令执行的作用。
下面来看个例子:
  这里重写了 readObject 方法,当调用 readObject 执行反序列化操作的时候,调用系统命令执行弹出计算器的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//ObjectCalc.java
import java.io.IOException;
import java.io.Serializable;

class ObjectCalc implements Serializable{
public String name;
//重写readObject()方法
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
//执行默认的readObject()方法
in.defaultReadObject();
//执行打开计算器程序命令
Runtime.getRuntime().exec("open /Applications/Calculator.app/");
}
}

然后先生成序列化的object。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//SerializableCalc.java
//引入必要的java包文件
import java.io.*;

public class SerializableCalc{
public static void main(String args[]) throws Exception{
//定义myObj对象
ObjectCalc myObj = new ObjectCalc();
myObj.name = "hi";
//创建一个包含对象进行反序列化信息的”object”数据文件
FileOutputStream fos = new FileOutputStream("/Users/l1nk3r/Desktop/object");
ObjectOutputStream os = new ObjectOutputStream(fos);
//writeObject()方法将myObj对象写入object文件
os.writeObject(myObj);
os.close();
}
}

然后反序列化的过程中弹出计算器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//unSerializableCalc.java
//引入必要的java包文件
import java.io.*;

public class unSerializableCalc{
public static void main(String args[]) throws Exception{
//从文件中反序列化obj对象
FileInputStream fis = new FileInputStream("/Users/l1nk3r/Desktop/object");
ObjectInputStream ois = new ObjectInputStream(fis);
//恢复对象
ObjectCalc objectFromDisk = (ObjectCalc)ois.readObject();
System.out.println(objectFromDisk.name);
ois.close();
}
}

3

0x03 小结

  1. 首先java的序列化而反序列化依赖于下面这两个方法。
1
2
java.io.ObjectOutputStream  ->   writeObject()           //序列化操作
java.io.ObjectInputStream -> readObject() //反序列化操作
  1. 需要确认一个java类是否可以反序列化,只需要查询其相关文档中是否实现了 java.io.Serializable 接口。
  2. readObject 方法的作用正是从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回, readObject 是可以重写的,可以定制反序列化的一些行为。
  3. 在序列化时,只保存实例变量,静态变量不保存。
  4. 当一个类的父类是实现了Seriazable时,子类自动是Serializable的。如enum类型,自动继承java.lang.Enum,而Enum是Serializable的,则enum也是可序列化的
  5. 在序列化时,同时也会保存private变量,而存在安全方面的问题
  6. 序列化时,不会序列化transient修饰的变量。 在反序列化构造出实例变量时,不会调用对象的任何构造方法。
  7. Externalizable 接口也是继承 java.io.Serializable 接口。且在实现过程中需要注意,如果你先序列化对象A后序列化B,那么在反序列化的时候一定记着 JAVA 规定先读到的对象是先被序列化的对象,不要先接收对象B,那样会报错。

0x04 结语

  开新坑,立新flag,大概率不会咕咕咕。
  项目地址:https://github.com/SukaraLin/java-coding-set
  本次实验代码均在这里

0x05 参考

深入理解 JAVA 反序列化漏洞
Java反序列化漏洞从入门到深入
Java序列化的几种方式以及序列化的作用