在各种编程语言的反序列化漏洞之中,Java是引人瞩目的。因为Java的开发生态中各种第三方库组件相互依赖,经常出现开发中常用的基础底层组件出现安全问题时,会引发核弹式反应,进而影响到上层得操作系统。
目录
序列化和反序列化概述
序列化是将对象的状态信息转换成可以存储或传输的形式的过程。在序列化期间,对象将当前的状态写入到临时或持久性的存储区,简单点说就是将状态对象保存为字符串。
反序列化就是把序列化之后的字符串在转化为对象的过程。
反序列化漏洞分析
在序列化过程中,其实是没有漏洞的。产生漏洞的主要原因是因为在反序列化的过程中,会自动调用一些魔法函数(例如PHP中的__construct()),如果函数的传入参数是可控的,攻击者就可以输入一些恶意代码到函数中,从而导致反序列化漏洞。
Java序列化基础知识
在Java中,只要一个类实现了java.io.Serializable接口,那么它就可以通过ObjectInputStream与ObjectOutputStream序列化。序列化的实现有两种方法。
默认序列化机制
通过writeObject方法实现对象序列化
FileOutputStream f = new FileOutputStream("date.ser");
ObjectOutputStream s = new ObjectOutputStream(f);
s.writeObject(new Date());
s.flush();
通过readObject方法实现字节流反序列化
FileInputStream in = new FileInputStream("date.ser");
ObjectInputStream s = new ObjectInputStream(in);
Date date = (Date)s.readObject();
自定义序列化机制
虽然Java序列化有默认机制,但也支持用户自定义。例如对象的一些成员变量没必要序列化保存或传输,就可以不序列化,或者是对一些敏感字段进行处理等。而自定义序列化就是重写writeObject与readObject。
例如:
Person类:
import java.io.*;
public class Person implements Serializable {
private String name;
public Person(String name){
this.name = name;
}
@Serial
private void readObject(ObjectInputStream s) throws IOException {
//readObject反序列化函数重写
System.out.println("Person.readObject method is being called");
//从输入数据流中解码读取一个字符串,并将其设置为name属性
name = s.readUTF();
}
@Serial
private void writeObject(ObjectOutputStream s) throws IOException {
//wireObject序列化函数重写
System.out.println("Person.writeObject method is being called");
//将name属性输出到数据流里
s.writeUTF(name);
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
'}';
}
}
创建Person对象
public static void main(String[] args) throws IOException, ClassNotFoundException {
Person person = new Person("hacker");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(person); // 对 Person 对象进行序列化
byte[] bytes = baos.toByteArray(); // 得到序列化后得到的字节数组
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bais);
Person p = (Person) ois.readObject(); // 对 bytes 字节数组进行反序列化,得到 Person 对象
}
运行结果
Java反序列化漏洞原理
到这里感觉一切都很正常,但当如果反序列过程中提供了命令执行的机会,且对来自外部输入的数据没有进行检查过滤,那么反序列化漏洞就产生了,攻击者可以进行任意命令执行。
例如:
测试代码:
import java.io.*;
public class Example implements Serializable {
private String cmd;
public Example(String cmd){
this.cmd = cmd;
}
@Serial
private void readObject(ObjectInputStream s) throws IOException {
cmd = s.readUTF(); //从输入的数据流中读取一个字符串
Runtime.getRuntime().exec(cmd); //这里进行命令执行
}
@Serial
private void writeObject(ObjectOutputStream s) throws IOException {
s.writeUTF(cmd);
}
}
攻击代码:
import java.io.*;
import java.nio.charset.StandardCharsets;
public class Testing {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Example example =new Example("calc"); //计算器命令
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(example);
String str = baos.toString(StandardCharsets.UTF_8); //按指定字符编码转换成字符串
System.out.println(str);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois =new ObjectInputStream(bais);
Example e = (Example) ois.readObject();
}
}
结果:
梳理一下,这里的漏洞的条件是因为对象重写了反序列化函数readObject,并且readObject方法存在执行命令的机会,所以攻击者精心构造了序列化对象,使得数据流反序列化的过程中恶意命令得以执行。
当然,在现实世界的反序列化漏洞往往很复杂,没有这么“白痴”。但也以直观的方式演示了反序列化漏洞产生的原因,其他比较复杂的反序列化漏洞的成因都与上例相似,就是漏洞利用的payload会比较复杂。
如果还有兴趣了解复杂一点的反序列化漏洞,可以看看这一篇文章反序列化漏洞,我还是个小白就不多班门弄斧。
Java反序列化漏洞利用--ysoserial
ysoserial是一款用于生成利用不安全的Java对象反序列化的有效负载的概念验证工具。
下载地址:ysoserial项目地址
使用方法:
java -jar ysoserial-master-8eb5cbfbf6-1.jar CommonsCollections5 "cmd /c calc" |base64 -w0
# 还需要进行一次 url 编码