java反序列化漏洞基础
前言
近年来反序列化漏洞可谓被大家熟知,尤其是的在JAVA 程序中发现了大量的反序列化漏洞,这种漏洞危害极大,可以直接造成RCE,获取到权限,本人之前对于这个漏洞一致认识很浅薄,甚至对于利用方式和工具都不太清楚,本文是对java语言基础的序列化与反序列化进行学习。
序列化与反序列化
Java 序列化是指把 Java 对象转换为字节序列的过程,序列化后的字节数据可以保存在文件、数据库中;而Java 反序列化是指把字节序列恢复为 Java 对象的过程。
所有在网络上面传输的对象,必须是可以序列化的。比如RMI(remote method invoke 远程方法调用),传入的参数和返回的结构都是可序列化的。所有需要保存到磁盘的对象都要实现序列化。通常建议创建的javaBean类都实现Serializable接口。
当然在实际场景中,直接使用JDK序列化的场景是很少的,一般都是使用其他方式,这是因为其本身有很多缺陷,如:无法跨语言、易被攻击、序列化后的流太大、序列化性能太差等。
但是我们通过学习他来了解基础原理,其他方式思路上应该也是大差不差。
java序列化
java中想要实现序列化与反序列化,主要是通过Serializable和Externalizable接口实现的。
Serializable与Externalizable的不同
-
Serializable接口是不需要提供无参构造器的,因为直接由虚拟机来创建对象的,不通过构造方法。Externalizable是通过反射来创建对象的,需要类中有无参构造器。
-
采用Externalizable无需产生serialVersionUID,而Serializable接口需要。
-
Externalizable 接口继承自 Serializable 接口,实现 Externalizable 接口的类完全由自身来控制反序列化的行为,而实现 Serializable 接口的类既可以采用默认的反序列化方式,也可以自定义反序列化方式。
Serializable接口
Serializable接口并没有要实现的方法,只是类似于一个标识符,表示这个类是可以被序列化的,不实现这个接口,序列化时程序会报错。
首先,我们创建一个User 类,这个类一定要实现Serializable接口
class User implements Serializable{
private int age;
private String name;
public User(){
};
public User(String name,int age){
this.age = age;
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
然后,我们使用ObjectOutStream 和FileOutStream 将序列化后的内容输入到文件中,然后再使用ObjectInputStram 和FileInputStream 读取文件,将值反序列化为对象。
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
User peter = new User("peter",18);
FileOutputStream file = new FileOutputStream("1");
ObjectOutputStream io = new ObjectOutputStream(file);
io.writeObject(peter);
io.flush();
io.close();
file.close();
FileInputStream file1 = new FileInputStream("1");
ObjectInputStream input = new ObjectInputStream(file1);
User pople = (User) input.readObject();
System.out.println(pople.getName());
}
}
Externalizable接口
实现Externalizable接口,必须实现writeExternal、readExternal方法。除此之外,实现该接口的类必须提供无参构造器,这是因为在反序列化时,是通过反射来创建对象的。
这里和Serializable接口不一样,
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
序列化版本号
反序列化必须拥有class文件,但随着项目的升级,class文件也会升级,序列化怎么保证升级前后的兼容性呢?
java序列化提供了一个private static final long serialVersionUID 的序列化版本号,这个值可以自由指定,也可由JVM根据类信息计算出一个值;由JVM计算的出的值会导致问题,如在程序移植后,JVM的计算方法可能存在差异,导致值不相同。
只有版本号相同,即使更改了序列化属性,对象也可以正确被反序列化回来。如果反序列化使用的class的版本号与序列化时使用的不一致,程序会报InvalidClassException异常。
下面是类被修改后的几种情况。
- 如果修改了非瞬态变量,则可能导致反序列化失败。如新类中实例变量的类型
- 序列化时类的类型不一致,则会反序列化失败,这时候需要更改serialVersionUID
- 如果只是新增了实例变量,则反序列化回来新增的是默认值;如果减少了实例变量,反序列化时会忽略掉减少的实例变量。
反序列化漏洞
Java反序列化了被恶意修改的序列化对象(须是服务器中存在的对象或者依赖包中的对象)。
根据Java官方说明,任何实现Serializable接口的Class都可以定义自己的readObject()方法,只要在重写方法的同时执行了defaultReadObject()方法即可。这样在反序列化的时候会自动invoke该Class下自己定义的readObject()方法。如果该方法书写不当的话就有可能引发恶意代码的执行,如
package evilSerialize;
import java.io.*;
public class Evil implements Serializable{
public String cmd;
private void readObject(java.io.ObjectInputStream stream) throws Exception {
stream.defaultReadObject();
Runtime.getRuntime().exec(cmd);
}
}
但肯定不会有程序员写出这样的代码,所以往往实际中反序列化漏洞的构造比较复杂,而且需要借助Java的一些特性如Java的反射来进行漏洞利用。
参考
https://www.jianshu.com/p/c25c3eea9276
https://zhuanlan.zhihu.com/p/474825041
https://www.jianshu.com/p/729941f4e00c
https://zhuanlan.zhihu.com/p/474825041
https://www.huaweicloud.com/zhishi/vss-002.html
https://leihehe.top/2021/07/28/Java反序列化漏洞之Java反序列化流程与分析-3/
https://xz.aliyun.com/t/6787