java反序列化漏洞基础

前言

近年来反序列化漏洞可谓被大家熟知,尤其是的在JAVA 程序中发现了大量的反序列化漏洞,这种漏洞危害极大,可以直接造成RCE,获取到权限,本人之前对于这个漏洞一致认识很浅薄,甚至对于利用方式和工具都不太清楚,本文是对java语言基础的序列化与反序列化进行学习。

序列化与反序列化

Java 序列化是指把 Java 对象转换为字节序列的过程,序列化后的字节数据可以保存在文件、数据库中;而Java 反序列化是指把字节序列恢复为 Java 对象的过程。
在这里插入图片描述

所有在网络上面传输的对象,必须是可以序列化的。比如RMI(remote method invoke 远程方法调用),传入的参数和返回的结构都是可序列化的。所有需要保存到磁盘的对象都要实现序列化。通常建议创建的javaBean类都实现Serializable接口。

当然在实际场景中,直接使用JDK序列化的场景是很少的,一般都是使用其他方式,这是因为其本身有很多缺陷,如:无法跨语言、易被攻击、序列化后的流太大、序列化性能太差等。

但是我们通过学习他来了解基础原理,其他方式思路上应该也是大差不差。

java序列化

java中想要实现序列化与反序列化,主要是通过Serializable和Externalizable接口实现的。

Serializable与Externalizable的不同

  1. Serializable接口是不需要提供无参构造器的,因为直接由虚拟机来创建对象的,不通过构造方法。Externalizable是通过反射来创建对象的,需要类中有无参构造器。

  2. 采用Externalizable无需产生serialVersionUID,而Serializable接口需要。

  3. 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