Java与代码审计之Java的序列化与反序列化

Java的序列化与反序列化

一、序列化与反序列化
1、序列化的方式

如果需要将某个对象保存到磁盘上或者通过网络传输,那么这个类应该实现 Serializable 接口或者Externalizable接口之一。

使用到JDK中关键类 :ObjectOutputStream (对象输出流) 和 ObjectInputStream (对象输入流)ObjectOutputStream 类中:通过使用 writeObject (Object object) 方法,将对象以二进制格式进行写入。

ObjectInputStream 类中:通过使用 readObject() 方法,从输入流中读取二进制流,转换成对象。

Transient关键字序列化的时候不会序列化Transient关键字修饰的变量(private transient String phone;),这个关键字不能修饰类和方法**Static。**静态变量也不会被序列化。

serialVersionUID:这里是指序列化的版本号,版本不一致会导致抛出错误,并且拒绝载入序列化与反序列化。

序列化必须满足以下几个条件:

1、实现Serializable接口或Externalizable接口
    比如:class Student implements Serializable  定义类时要这样 implements

不实现接口:报错无法执行

image-20250113213540983

实现接口:正常执行代码

image-20250113213633684

2、当前类提供一个全局常量 serialVersionUID,每个类都有一个约定好的 serialVersionUID
    意思是,序列化后结果的UID,在反序列化时,该UID必须保持不变
    要么序列化和反序列化全都使用默认分配的UID,要么二者都是用自定义的UID

如果序列化时使用的时默认分配的 UID,那么反序列化时也必须使用默认分配的UID:

image-20250113214609606

如上图进行序列化后再进行反序列化:并没有报错

image-20250113214647517

但如果我将serialVersionUID打开进行序列化,然后注释掉再进行反序列化

image-20250113214853819

image-20250113214923268

或者

将 serialVersionUID 注释掉进行序列化,然后打开进行反序列化

image-20250113214958941

image-20250113214826081

都会报错

3、必须保证其内部所有属性也必须是可序列化的(默认情况下,基本数据类型可序列化)
4ObjectInputStreamObjectOutputStream不能序列化statictransient修饰的成员变量
    transient修饰的成员变量在序列化时,不会参与序列化,其值要么是整型的0,要么就是String类型的null

transient修饰String类型成员变量时序列化

image-20250113213801337

image-20250113213916250

transient修饰int类型成员变量时序列化:

image-20250113214016073

image-20250113214041846

一个基础的序列化和反序列化操作(针对字符串对象,非核心操作):

package com.woniusec.serial;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class BasicSerial {    
    public static void main(String[] args) throws Exception {        
        //序列化        
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("basicSerial.ser"));        
        oos.writeObject(new String("这是一条序列化字符串"));        
        oos.close();        
        
        //反序列化        
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("basicSerial.ser"));        
        Object o = ois.readObject();        
        String s = (String) o;        
        ois.close();        
        System.out.println(s);    
    }
}

针对一个特定的类进行序列化和反序列化:

package com.woniuxy.core;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.jar.Attributes.Name;

public class Deseria {    
    public static void main(String[] args) throws Exception {        
        // 序列化//        
        Student student = new Student();//        
        student.name = "张三";//        
        student.id = 12345;//        
        student.phone = "13812345678";//        
        student.money = 9999;//        
        FileOutputStream fos = new FileOutputStream("./data/student.ser");//        
        ObjectOutputStream oos = new ObjectOutputStream(fos);//        
        oos.writeObject(student);//        
        fos.close();//        
        oos.close();        
        
        // 反序列化        
        FileInputStream fis = new FileInputStream("./data/student.ser");        
        ObjectInputStream ois = new ObjectInputStream(fis);        
        Student obj = (Student)ois.readObject();        
        obj.study();        
        System.out.println(obj.name);        
        System.out.println(obj.money);    
    }
}

class Student implements Serializable {    
    private static final long serialVersionUID = 12345L; 
    // 用于反序列化时标识是否为同一个类    
    public String name = "";    
    public int id = 0;    
    public String phone = "";    
    transient int money = 0;        
    // transient修饰的变量不能被序列化    
    public Student() {        
        System.out.println("构造方法运行");    
    }    
    public void study() {        
        System.out.println("学生正在学习");    
    }    
    public void sleep() {        
        System.out.println("学生正在休息");    
    }    
    // 重写readObject方法,在反序列化时会优先调用,如果该方法存在可利用点,则可以自动触发    
    private void readObject(ObjectInputStream ois) throws Exception{        
        ois.defaultReadObject();        
        System.out.println("正在反序列化");        
        Runtime.getRuntime().exec("calc.exe");    
    }
}
2、反序列化漏洞成因

我们可以在类中重写 readObject 这个函数

一旦在类中重写了 readObject 函数之后,该类的实例化对象一旦被反序列化,那么肯定会首先自动调用该重写的 readObject 函数

比如我在 Student 类中 重写这样一个方法

private void readObject(ObjectInputStream ois) throws Exception {
 ois.defaultReadObject();
 System.out.println("正在被反序列化ing...");

}

然后对 Student 类的某个实例进行序列化之后再进行反序列化:

image-20250113220738457

由此可见,在反序列化的时候,第一时间自动调用重写的readObject函数

如果不想你的类被实例化,那么久将他的构造方法私有化

序列化指把Java对象转换为字节序列的过程,反序列化就是打开字节流并重构对象,那如果即将被反序列化的数据是特殊构造的,就可以产生非预期的对象,从而导致任意代码执行。

Java中间件通常通过网络接收客户端发送的序列化数据,而在服务端对序列化数据进行反序列化时,会调用被序列化对象的readObject( )方法。而在Java中如果重写了某个类的方法,就会优先调用经过修改后的方法。如果某个对象重写了readObject( )方法,且在方法中能够执行任意代码,那服务端在进行反序列时,也会执行相应代码。

如果能够找到满足上述条件的对象进行序列化并发送给Java中间件,Java中间件也会去执行指定的代码,即存在反序列化漏洞。

我们来看一下:

java 中执行命令我们学习了两种方式,分别是 Runtime 和 ProcessBuilder

首先是 Runtime

我们先来看一下Runtime的定义:

  • Runtime.getRuntime() 这个函数再执行指令,并不是 Runtime 再执行
  • 但是我们看下面的图片,Runtime() 这个函数是private的,我们无法直接实例化,但是getRuntiem() 这个函数我们可以调用,而该函数会返回一个 currentRuntime 对象,该对象又是 Runtiemn类型的,所以我们应该加载一个动态类方法,而不是类

image-20250113222559214

private void readObject(ObjectInputStream ois) throws Exception {
    ois.defaultReadObject();
    //        System.out.println("正在被反序列化ing...");
    //        Runtime.getRuntime().exec("calc.exe");

    // 如果我们在 该重写函数中 进行类反射机制来执行指令呢?
    Class clazz = Class.forName("java.lang.Runtime");
    Method m1 = clazz.getDeclaredMethod("getRuntime");
    Object obj = m1.invoke(clazz,null);
    Method m2 = clazz.getDeclaredMethod("exec",String.class);
    m2.invoke(obj,"calc.exe");
}
  • 所以使用类反射机制 动态加载一个类方法然后在动态加载方法指令(这里是 exec)

接下来看看 ProcessBuilder 方式,如何使用类反射机制来执行指令:

  • 首先还是来看一下 ProcessBuilder 的定义

image-20250113225014494

我们发现,ProcessBuilder 具有两个构造方法,但是,两个构造方法都需要传参,而且,所传参数要么是可变长String,要么是List数组,而Java中又刚好将可变长类型规划在数组中,所以,ProcessBuilder 必须要传入数组类型的参数,因此,在创建构造器的时候,我们给 getConteoller 函数传入的参数是 String[].class

  • 然后 ProcessBuilder 真正开始执行的函数是 start 函数

image-20250113225811286

所以 我们需要再使用类反射来获取 start 方法

private void readObject(ObjectInputStream ois) throws Exception {
    ois.defaultReadObject();
    System.out.println("正在被反序列化ing...");

    ProcessBuilder 方式执行指令
    Class clazz = Class.forName("java.lang.ProcessBuilder");
    Constructor c = clazz.getConstructor(String[].class);  // 由于该构造方法需要传入参数,所以必须要使用构造器来传参,普通的 newInstance 无法给构造函数传参
    // 由于在创建 Controller 的时候,给的参数类型为 String[] 所以,我们需要把 calc.exe 放在一个数组中传入 newInstance
    String[] cmd = {"calc.exe"};
    Object obj = c.newInstance(cmd);  // 给 ProcessBuilder 的构造方法传入要调用的指令,然后接收返回的对象
    Method m1 = clazz.getDeclaredMethod("start");  //使用ProcessBuilder对象来获取方法
    m1.invoke(obj,null);  // invoke获取到的方法  start 函数不需要传参
}

但是这样执行是错的

image-20250113231135202

  • 因为我们现在给newInstance 传参是直接传入了变量,而不是值。那么我们再去看看 newInstance 的定义

image-20250113231210903

wc,又是一个可变长数组,无敌了,所以我们在这里还需要将 cmd 这个固定的数组内容让它变得不固定,让一个固定长度的数组变成一个长度不固定的数组方法就是,将一维数组变成二维数组,长度确实不固定了,同时依旧保持了数组类型

private void readObject(ObjectInputStream ois) throws Exception {
    ois.defaultReadObject();
    System.out.println("正在被反序列化ing...");

    ProcessBuilder 方式执行指令
    Class clazz = Class.forName("java.lang.ProcessBuilder");
    Constructor c = clazz.getConstructor(String[].class);  // 由于该构造方法需要传入参数,所以必须要使用构造器来传参,普通的 newInstance 无法给构造函数传参
    // 由于在创建 Controller 的时候,给的参数类型为 String[] 所以,我们需要把 calc.exe 放在一个数组中传入 newInstance
    //        String[] cmd = {"calc.exe"};
    // 然后,又因为newInstance所需要传入的参数,又是一个可变长的Object对象,所以我们在这里要把这一个固定的数组编程二维数组,才能确保他是可变长的
    String[][] cmd = {{"calc.exe"}};
    Object obj = c.newInstance(cmd);  // 给 ProcessBuilder 的构造方法传入要调用的指令,然后接收返回的对象
    Method m1 = clazz.getDeclaredMethod("start");  //使用ProcessBuilder对象来获取方法
    m1.invoke(obj,null);  // invoke获取到的方法  start 函数不需要传参
}
  • 最终运行结果

image-20250113231511419

3、利用Base64对序列化数据进行转换

(1)将序列化二进制数据进行Base64编码,便于在网络上进行传输

File file = new File("./data/student.ser");
FileInputStream inputFile = new FileInputStream(file);
byte[] buffer = new byte[(int) file.length()];inputFile.read(buffer);
inputFile.close();byte[] file64 = Base64.getEncoder().encode(buffer);
System.out.println(new String(file64, "utf-8"));

(2)将接收到的Base64编码还原为序列化文件

File file = new File("./data/base64.ser");
FileOutputStream fos = new FileOutputStream(file);
String source = "rO0ABXNyABdjb20ud29uaXV4eS52dWwuU3R1ZGVudAAAAAAAADA5AgADSQACaWRMAARuYW1ldAASTGphdmEvbGFuZy9TdHJpbmc7TAAFcGhvbmVxAH4AAXhwAAAwOXQABuW8oOS4iXQACzEzODEyMzQ1Njc4";
byte[] content = Base64.getDecoder().decode(source.getBytes());
fos.write(content);fos.flush();fos.close();

(3)也可以将Base64字符串解码后直接在内存中反序列化

String b64str = "rO0ABXNyABdjb20ud29uaXV4eS52dWwuU3R1ZGVudAAAAAAAvGFOAgACTAAEbmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO0wABXBob25lcQB+AAF4cHQABuW8oOS4iXQACzE4ODEyMzQ1Njc4";
byte[] b64byte = Base64.getDecoder().decode(b64str);
ByteInputStream bis = new ByteInputStream(b64byte, b64byte.length);
ObjectInputStream ois = new ObjectInputStream(bis);
Student student = (Student) ois.readObject();student.study();
System.out.println(student.phone);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值