Java序列化反序列化介绍及其漏洞

什么是序列化?

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

序列化就是把一个java对象写入到IO流中,以便存入磁盘或者是数据库里,亦或是通过网络进行传输。

如果一个类的数据,需要通过网络进行传输,那么推荐它实现Serializable接口。下面是一个例子。

1
2
3
4
5
6
7
public static void main(String[] args) throws IOException {
// 将一个对象写入到文件中
FileOutputStream fos = new FileOutputStream(new File("object.txt"));
Date date = new Date();
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(date);
}

因为Date本身是实现了接口的,所以可以直接写到文件里,文件内容长这样:

如果用十六进制来查看是这样子的:

其中ac ed 00 05是序列化内容的特征。

对应的反序列化代码也很简单:

1
2
3
4
5
6
7
public static void main(String[] args) throws IOException, ClassNotFoundException {
FileInputStream fis = new FileInputStream(new File("object.txt"));
ObjectInputStream ois = new ObjectInputStream(fis);
Object object = ois.readObject();
object = (Date)object;
System.out.println(object);
}

当然有几个要注意的点:

  • 反序列创建对象并不会调用构造方法。
  • 一个对象如果想要序列化,那么它的所有成员必须也要支持序列化。

序列化的总体算法是这样的:所有保存到磁盘里的对象都会有一个编号,只有虚拟机检查到这个对象从来没有被序列化过,就会序列化它,否则就只要输出编号即可。

序列化漏洞

示例代码:

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
public class SerializableTest implements Serializable {

public static void main(String[] args) throws IOException, ClassNotFoundException {
// 将一个对象写入到文件中
FileOutputStream fos = new FileOutputStream(new File("object.txt"));
ObjectOutputStream oos = new ObjectOutputStream(fos);
MyObject myObject = new MyObject();
myObject.name = "test";
oos.writeObject(myObject);
oos.close();

// 反序列化出来
FileInputStream fis = new FileInputStream(new File("object.txt"));
ObjectInputStream ois = new ObjectInputStream(fis);
//恢复对象
MyObject objectFromDisk = (MyObject)ois.readObject();
System.out.println(objectFromDisk.name);
ois.close();
}
}

class MyObject implements Serializable {
public String name;

private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
// 1.执行默认的readObject()方法
in.defaultReadObject();
// 2.执行打开QQ命令
Runtime.getRuntime().exec("open /Applications/QQ.app/");
}
}

可以看到MyObject实现了readObject方法,该方法的作用正是从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并且加入打开QQ的逻辑。

更加具体的PoC

服务器部分

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
public class ExploitableServer {
public static void main(String[] args) {
try {
//创建socket
ServerSocket serverSocket = new ServerSocket(Integer.parseInt("9999"));
System.out.println("Server started on port " + serverSocket.getLocalPort());
while (true) {
//等待链接
Socket socket = serverSocket.accept();
System.out.println("Connection received from " + socket.getInetAddress());
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
try {
//读取对象
Object object = objectInputStream.readObject();
System.out.println("Read object " + object);
} catch (Exception e) {
System.out.println("Exception caught while reading object");
e.printStackTrace();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

服务器部分很简单,就是接收来自客户端的消息,并且将其反序列化即可。

客户端部分

这里因为我更新了java10,导致有些API有些变动。暂时先搁浅吧