概述

最近在做代码静态检查的时候接触到序列化、反序列化的漏洞,之前也断断续续的使用过序列化、反序列化的知识, 一般是通过实现Serializable接口,并提供readObject、writeObject的方法来实现反序列化和序列化,当然序列化的目的 则是让java对象脱离jvm而存在,这样就可以在网络上传输数据了,或者持久化到磁盘、数据库中,等到需要的时候 再通过反序列化的方式将其转换成java对象。使用过fastjson的人应该差不多都知道,这个小工具确实很好用,也 很快,不过网上却爆出各种安全问题,应该也算的上誉满天下、谤满天下了吧。接下来我们就来认识一下序列化、反序列化所 带来的问题吧。

详解

使用场景分析

  • 平台之间的通信:我们最长使用的序列化、反序列化的场景应该就是在后端了,不过大家可能平时都没有太留意,我们常使用的 http请求,前端传的是json,到了后台之后就被反序列化成java bean了,这本质上就是反序列化,前端请求接口,后台返回java bean, 数据传到前端的时候已经是json了,那么该过程就是序列化的过程。
  • RMI:也即是远程方法调用(remote method invocation),相信接触过分布式的同学应该会对这很熟悉了,这是实现 不通操作系统之间程序方法调用的关键,Java RMI 默认的端口是1099
  • JMX:JMX是一套标准的代理和服务,主要用于监控业务的实现,有时间需要整理一下这一块的内容

漏洞原因

暴露或者间接暴露反序列化API,导致用户可以操作传入的数据,用户可以在篡改的数据中随意注入自定义的逻辑,这样 系统的一些函数就赤裸裸的暴露在篡改者的面前。

基本原理

序列化、反序列化演示

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

public class MashaTest {

public static void main(String[] args) {

// java 对象
String text = "hello world";

// 对象的序列化,写入文件
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("text"));) {
oos.writeObject(text);
} catch (Exception e) {
e.printStackTrace();
}

// 反序列化成java对象
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("text"))) {
String res = (String) ois.readObject();
System.out.println(res);
} catch (Exception e) {
e.printStackTrace();
}

}
}

如上所示,我们展示了一个java对象如何序列化成文件,并从文件反序列化成对象的过程,这里需要注意的是待序列化的对象 是一个String类型,我们查看一下累的继承关系不难发现,String继承了Serializable接口,这也是在不引入三方库的前提下 对象序列化和反序列化必须要具备的条件:实现Serializable接口

漏洞演示

上面我们介绍了序列化和反序列化的基本的知识,接下来我们可以看一下序列化、反序列化如何注入用户自身的程序:

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

public class UnmashaTest {

public static void main(String[] args) {

// 构造对象
Test test = new Test();
test.name = "hello wes";

// 序列化
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("text"))) {
oos.writeObject(test);
} catch (Exception e) {
e.printStackTrace();
}

// 反序列化,这里可以看到被篡改后的数据会触发什么操作
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("text"))) {
Test res = (Test) ois.readObject();
System.out.println(res.name);
} catch (Exception e) {
e.printStackTrace();
}
}


public static class Test implements Serializable {

public String name;

// 注意这里的方法一定是private类型的
private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException {
inputStream.defaultReadObject();
Runtime.getRuntime().exec("open /Applications/Calculator.app/");
}
}
}

执行后会发现该程序会启用计算器的程序,如下: 通过上面程序的演示我们可以看到反序列化中确实存在一些安全隐患。尽管上面的代码能够演示反序列化中存在的一些漏洞,不过 看起来还是太demo化了,接下来我们来看一个稍微复杂一点的例子。

解决方案

常用的用于防范反序列化安全问题的方案有以下两种:

  • 禁止jvm执行外部命令Runtime.exec方法,具体的可以通过扩展 SecurityManager 可以实现,详细方案后面有时间再补齐
  • 类的白名单校验:具体的是自定义ObjectInputStream,重载resolveClass方法,对class进行白名单的限制,详细方案后面有时间再补齐

小结

上面简单的对java序列化、反序列化中的使用以及可能存在的漏洞进行了分析,同时也提供了解决这种漏洞的机制,更多 扩展知识后面有时间再补齐吧。