Java 代码审计---反序列化

Java 序列化是一种将对象转换为字节流的过程,以便可以将对象保存到磁盘上,将其传输到网络上,或者将其存储在内存中,以后再进行反序列化,将字节流重新转换为对象。
序列化在 Java 中是通过 java.io.Serializable 接口来实现的,该接口没有任何方法,只是一个标记接口,用于标识类可以被序列化。
当你序列化对象时,你把它包装成一个特殊文件,可以保存、传输或存储。反序列化则是打开这个文件,读取序列化的数据,然后将其还原为对象,以便在程序中使用。
序列化是一种用于保存、传输和还原对象的方法,它使得对象可以在不同的计算机之间移动和共享,这对于分布式系统、数据存储和跨平台通信非常有用。

序列化

序列化主要是将一个java对象转换为二进制字节流的过程,在学习反序列化之前,首先来学习一下序列化是什么,怎么进行,知其然,知其所以然。
序列化首先需要注意一下几点

  • 实现 Serializable 接口:序列化的对象必须要实现Serializable接口,这个接口是一个标记接口,没有任何方法,只是用于标识该类的对象可以被序列化。
  • 对象的成员变量可序列化: 如果一个类中的成员变量是基本数据类型或其他可序列化的对象,则该类的对象也是可序列化的。如果成员变量是不可序列化的对象,可以使用 transient 关键字进行标记,表示在序列化过程中不将该成员变量序列化,并且static变量正常情况下是不会被序列化的。
  • 版本控制: 在进行对象序列化时,需要注意版本控制,确保对象的类结构发生变化时不会导致序列化和反序列化的问题。可以通过 serialVersionUID 字段进行手动版本控制。
  • 对象的引用关系: 如果对象间存在引用关系,即一个对象引用了另一个对象,那么在序列化和反序列化时,需要确保所有相关的对象都可以正确地序列化和反序列化。
    ObjectOutputStream中的writeObject方法是java进行序列化的关键方法,下面是使用的具体方法
//创建一个FileOutputStream,其中写入ObjectOutputStream中的对象
FileOutputStream fileStream = new FileOutputStream(String file);

//创建ObjectOutputStream
ObjectOutputStream objStream = new ObjectOutputStream(fileStream);

//调用writeObject方法,该方法传入的是需要序列化的对象,并且该方法会将该对象进行序列化,写入到上面指定的输出流中:fileStream
objStream.writeObject(Object);

下面是序列化的示例
首先创建一个类,并且该类继承Serializable接口

package com.pwjcw.entity;  
  
import java.io.Serializable;  
  
public class Persion implements Serializable {  
    private String name;  
    private int age;  
  
    public Persion() {  
    }  
  
    @Override  
    public String toString() {  
        return "Persion{" +  
                "name='" + name + '\'' +  
                ", age=" + age +  
                '}';  
    }  
  
    public Persion(String name, int age) {  
        this.name = name;  
        this.age = age;  
    }  
  
    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;  
    }  
}

实现对该类的序列化

@Test  
public void TestSerialize() throws IOException {  
    Persion persion = new Persion("pwjcw", 1);  
    // 创建文件输出流,用于将对象序列化后的字节流写入文件  
    FileOutputStream f = new FileOutputStream("persion.ser");  
    // 创建对象输出流,用于将对象序列化后的数据写入文件  
    ObjectOutputStream outputStream = new ObjectOutputStream(f);  
    // 将 Person 对象进行序列化并写入文件  
    outputStream.writeObject(persion);  
    // 关闭对象输出流  
    outputStream.close();  
    // 关闭文件输出流  
    f.close();  
}

序列化后的数据

将一个对象进行序列化其实是将该对象的一些属性进行序列化,并不是对该对象的类,以及相关的方法也进行序列化为二进制数据。下面是persion.ser文件的二进制视图

在这里插入图片描述

serialVersionUID

在Java中,serialVersionUID是一个特殊的静态变量,用于标识序列化类的版本。当一个类被序列化时,它的serialVersionUID会被序列化到流中,以确保序列化和反序列化过程中类的版本一致性,如果反序列化时的serialVersionUID版本与本地对应的serialVersionUID版本不一致,则会导致反序列化失败。

当类的结构发生变化时(例如添加新的字段或方法),通过显式地指定serialVersionUID,可以确保旧版本的序列化数据可以与新版本的类兼容。如果不指定serialVersionUID,Java会根据类的结构自动生成一个,但是当类的结构发生变化时,自动生成的serialVersionUID也会改变,可能导致旧版本的序列化数据无法被正确反序列化

关于悖论的解释

上面也已经说到,serialVersionUID是一个静态变量,而静态变量正常情况下又不参与序列化,不过serialVersionUID是一个例外,这是java设计的一个特殊情况。

反序列化

反序列化和序列化相反,主要是从二进制字节流转换为java对象,可以通过ObjectInputStream类的readObject方法将传入的二进制流进行序列化到对象
下面是具体的使用示例

@Test  
public void TestUnSerialize() throws IOException, ClassNotFoundException {  
    //从文件中读取二进制字节流  
    FileInputStream fileInputStream=new FileInputStream("persion.ser");  
    //创建ObjectInputStream对象,并且传入FileInputStream读取到的二进制字节流  
    ObjectInputStream objectInputStream=new ObjectInputStream(fileInputStream);  
    //调用readObject方法,并且转换为Persion对象  
    Persion persion= (Persion) objectInputStream.readObject();  
    //打印反序列化得到的persion对象  
    System.out.println(persion);  
}

反序列化导致rce的原因

在反序列化中,如果反序列化的目标类自定义了readObject方法,那么在反序列化时,将会调用目标类自定义的readObject方法,这是出现反序列化漏洞的根本原因。下面是一个演示示例。
目标类

package com.pwjcw.entity;  
  
import java.io.IOException;  
import java.io.ObjectInputStream;  
import java.io.Serializable;  
  
public class Persion implements Serializable {  
    private String name;  
    private int age;  
  
    public Persion() {  
    }  
  
    @Override  
    public String toString() {  
        return "Persion{" +  
                "name='" + name + '\'' +  
                ", age=" + age +  
                '}';  
    }  
  
    public Persion(String name, int age) {  
        this.name = name;  
        this.age = age;  
    }  
  
    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;  
    }  
    private void readObject(java.io.ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException {  
        objectInputStream.defaultReadObject();  
        Runtime.getRuntime().exec("calc.exe");  
    }  
}

那么此时进行反序列化,就会调用Persion类的readObject方法,进而调用Runtime语句。
当前并不是所有的方法都会自定义readObject方法,不过后面的HashMap类自定义了,并且通过该类造成了URLDNS链的反序列化,后面文章会进行分析。

参考文档:

https://www.runoob.com/java/java-serialization.html
Java ObjectOutputStream 类 - Java教程 - 菜鸟教程 (cainiaojc.com)

  • 26
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值