Java序列化与反序列化漏洞
在这里,你将学到Java序列化与反序列化是什么,从代码的角度进行思考,进而引出安全问题,这种思考方式有利于提升你对漏洞的理解深度,也有利于往代码审计的方向靠拢。当然在学习这篇文章之前,需要各位小伙伴们具有一定的Java编程基础,学起来就更加轻松。
概述
Java序列化是指把Java对象转换为字节序列的过程;而Java反序列化是指把字节序列恢复为Java对象的过程
序列化分为两大部分:序列化和[反序列化。序列化是这个过程的第一部分,将数据分解成字节流,以便存储在文件中或在网络上传输。反序列化就是打开字节流并重构对象。对象序列化不仅要将基本数据类型转换成字节表示,有时还要恢复数据。恢复数据要求有恢复数据的对象实例。
静态成员变量是不能序列化的,因为序列化是针对对象的,而静态成员变量是属于类的。
transient修饰的变量也不能被序列化
原生序列化与反序列化的案例实现
我将创建4个类,从编程的角度实现序列化,这4个类分别是Car,Student,PC1,PC2,其中PC1模拟服务器1,PC2模拟服务器2,Student是一个对象,模拟在PC1中通过ObjectOutputStream将Student对象序列化为一个文件,然后在PC2中通过ObjectInputStream将PC1序列化的文件反序列化成一个Student对象。
不过要完成序列化反序列化的前提是对象必须实现Serializable接口,不然会报错,并且被序列化的对象必须有get和set方法,这里我使用的是lombok插件,内置了get和set方法
从开发者的角度实现序列化
Car
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Car implements Serializable {
private String name;
private int wheels;
}
Student
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Student implements Serializable {
private String name;
private int age;
/**
* Car 类也是需要实现序列化接口的。
*/
private Car car;//这里如果没有实现序列化接口,那么在 Student 对象序列化时将会报错
}
PC1
public class PC1 {
public static void main(String[] args) throws IOException {
Car car = new Car("BYD",4);
Student yuanBoss = new Student("yuan_boss", 18,car);
System.out.println(yuanBoss);
serialize(yuanBoss);
}
public static void serialize(Student student) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(student);
}
}
在序列化Student对象的时候,如果Student对象中的Car对象没有实现Serializable就会报以下异常:
当实现Serializable之后,可以看到正确输出,并且可以看到当前项目的目录中生成了ser.bin文件:
PC2
public class PC2 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
unSerialize();
}
public static void unSerialize() throws IOException, ClassNotFoundException {
ObjectInput oos = new ObjectInputStream(new FileInputStream("ser.bin"));
Object obj = oos.readObject();
System.out.println(obj);
}
}
执行PC2中的代码之后,我们可以看到正确输出了Student对象:
引发思考
为什么上面的代码经过反序列化之后会还原为对象呢?原因是调用了readObject()方法,但是如果我们在要序列化的类中重写了readObject()方法,反序列化的时候就会根据我们重写的逻辑进行反序列化。
例如,我在Student类中重写writeObject()与readObject()方法:
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Student implements Serializable {
private String name;
private int age;
/**
* Car 类也是需要实现序列化接口的。
*/
private Car car;//这里如果没有实现序列化接口,那么在 Student 对象序列化时将会报错
private void writeObject(ObjectOutputStream objectOutputStream){
System.out.println("重写的writeObject方法");
}
private void readObject(ObjectInputStream objectInputStream){
System.out.println("重写的readObject方法");
}
}
当我们执行PC1之后,将不会生成ser.bin文件了,而是输出 重写的writeObject方法
这句话,执行PC2之后,也是调用重写的readObject()方法,而不会反序列化成对象了。
如图,执行PC1:
执行PC2:
引出安全问题
通过上述的思考,我们可以知道,可以通过重写readObject()与writeObject()方法,让程序进行序列化或者反序列化的时候执行重写的readObject()与writeObject()方法的逻辑,这就相当于拥有了在服务器上执行代码的能力。当然想要拥有这种能力,那个对象需要具备以下条件:
- 实现了Serializable接口
- 重写了readObject方法
Java中,HashMap就符合这个条件,为什么HashMap要重写readObject()方法呢,因为hashmap存储对象的时候要计算hash值,从而来确定对象的存储位置,但是由于不同的机器计算的hash值是不一样的,所以为了保证同一个对象在不同机器中hash值一样,就重写了readObject()方法,在反序列化的时候将对象的信息拿出来然后进行计算hash值,从而保证对象的hash值一样。
所以我们可以利用HashMap,往HashMap中放入一些恶意对象,当HashMap进行反序列化的时候,就会自动调用HashMap中的readObject()方法,从而完成对一些恶意对象的利用。在CC6利用链和URLDNS链中就利用了HashMap的readObject(),然后触发利用链。
HashMap利用案例
有Java序列化与反序列化基础的师傅可以尝试阅读以下文章:
CC6利用链(最好用的CC利用链)–EXP编写思路–源码分析
当然我还是建议按照本专栏的排列顺序阅读,效果最佳。