Java序列化与反序列化基础

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

扫描二维码关注公众号,回复: 16376815 查看本文章
@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文件:

image-20230810171717269

image-20230810171841361

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对象:

image-20230810171919144

引发思考

为什么上面的代码经过反序列化之后会还原为对象呢?原因是调用了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:

image-20230810174602996

执行PC2:

image-20230810174627521

引出安全问题

通过上述的思考,我们可以知道,可以通过重写readObject()与writeObject()方法,让程序进行序列化或者反序列化的时候执行重写的readObject()与writeObject()方法的逻辑,这就相当于拥有了在服务器上执行代码的能力。当然想要拥有这种能力,那个对象需要具备以下条件:

  1. 实现了Serializable接口
  2. 重写了readObject方法

Java中,HashMap就符合这个条件,为什么HashMap要重写readObject()方法呢,因为hashmap存储对象的时候要计算hash值,从而来确定对象的存储位置,但是由于不同的机器计算的hash值是不一样的,所以为了保证同一个对象在不同机器中hash值一样,就重写了readObject()方法,在反序列化的时候将对象的信息拿出来然后进行计算hash值,从而保证对象的hash值一样。

image-20230810175807999

所以我们可以利用HashMap,往HashMap中放入一些恶意对象,当HashMap进行反序列化的时候,就会自动调用HashMap中的readObject()方法,从而完成对一些恶意对象的利用。在CC6利用链和URLDNS链中就利用了HashMap的readObject(),然后触发利用链。

HashMap利用案例

有Java序列化与反序列化基础的师傅可以尝试阅读以下文章:

URLDNS利用链EXP编写之源码分析

CC6利用链(最好用的CC利用链)–EXP编写思路–源码分析

当然我还是建议按照本专栏的排列顺序阅读,效果最佳。

猜你喜欢

转载自blog.csdn.net/weixin_46367450/article/details/132326437