Java代码审计学习笔记之JAVA基础系列(七、序列化与反序列化)

1.序列化简介

所谓的序列化过程,就是将某一个对象按照某种约定好的结构转换成二进制数据进行传输,这段二进制数据中就包含了数据类型、和值的描述信息。使用序列化和反序列化能够方便数据的传输和存储。Java中最常用的序列化和反序列化方法是java.io.ObjectOutputStream#writeObjectjava.io.ObjectInputStream#readObject方法。

2.writeObject

java中一个对象能够被序列化首先它所属的类需要实现了java.io.Serializable接口。该接口中没有定义任何方法,只是为了说明是否能够被序列化。例如这里自己编写一个Person类:

//Person.java
import java.io.Serializable;

public class Person implements Serializable{
    private String name;
    private int age;
    public Person(String name,int age){
        this.age = age;
        this.name = name;
    }
    public void printInfo(){
        System.out.println(this.name);
        System.out.println(this.age);
    }
}

之后可以实例化一个Person类的对象,我们就可以使用writeObject方法来将这个对象进行序列化。

//testObject.java
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.util.Arrays;

public class testObject {
    public static void main(String[] argv) throws Exception{
        Person p = new Person("join",18);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();//保存序列化结果的对象
        ObjectOutputStream objout = new ObjectOutputStream(baos);//执行序列化的对象
        objout.writeObject(p);//执行writeObject方法将对象p写入baos
        for (int i=0;i<baos.toByteArray().length;i++){
            System.out.print(String.format("%02x", baos.toByteArray()[i]));//以十六进制形式输出序列化数据
        }
    }
}

执行完成后将会输出如下结果:

使用SerializationDumper工具对输出16进制结果进行解析可以看到如下内容,可以看到这段序列化后的数据保存的主要内容就是字段的名称、类型、数值等信息:

λ java -jar SerializationDumper-v1.12.jar -f 1.txt

STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
  TC_OBJECT - 0x73
    TC_CLASSDESC - 0x72
      className
        Length - 6 - 0x00 06
        Value - Person - 0x506572736f6e
      serialVersionUID - 0xd4 9c 72 9f 45 ae c4 3b
      newHandle 0x00 7e 00 00
      classDescFlags - 0x02 - SC_SERIALIZABLE
      fieldCount - 2 - 0x00 02
      Fields
        0:
          Int - I - 0x49
          fieldName
            Length - 3 - 0x00 03
            Value - age - 0x616765
        1:
          Object - L - 0x4c
          fieldName
            Length - 4 - 0x00 04
            Value - name - 0x6e616d65
          className1
            TC_STRING - 0x74
              newHandle 0x00 7e 00 01
              Length - 18 - 0x00 12
              Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b
      classAnnotations
        TC_ENDBLOCKDATA - 0x78
      superClassDesc
        TC_NULL - 0x70
    newHandle 0x00 7e 00 02
    classdata
      Person
        values
          age
            (int)18 - 0x00 00 00 12
          name
            (object)
              TC_STRING - 0x74
                newHandle 0x00 7e 00 03
                Length - 4 - 0x00 04
                Value - join - 0x6a6f696e

3.readObject

readObject方法属于ObjectInputStream类。反序列化一个对象时,实例化ObjectInputStream类的时候需要传入一个Inputstream类型的参数来初始化该实例的值。反序列化Person的代码如下:

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class testObject {
    public static void main(String[] argv) throws Exception{
        Person p = new Person("join",18);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream objout = new ObjectOutputStream(baos);
        objout.writeObject(p);//本行代码运行完成,序列化对象便写入了baos变量中

        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream objin = new ObjectInputStream(bais);
        Person p2 = (Person)objin.readObject();
        p2.printInfo();
    }
}

4.类中的writeObject和readObject方法

之所以会有各种反序列化漏洞的出现,主要就是因为java中在对某个对象进行序列化和反序列化的过程时,会自动调用该对象的writeObjectreadObject方法。通过在对象中编写这两种方法,我们可以再序列化的输出流中自定义一段输出数据并在反序列化时读出。可以看出一个明显的区别就是PHP中如果我们想要把一段数据反序列化时存储起来,必须要添加一个属性,而这里则不需要。示例代码如下:

import java.io.*;

public class Test1 {
    public static class Person implements Serializable {
        public String name;
        public int age;
        public Person(String name,int age){
            this.name = name;
            this.age = age;
        }
        private void writeObject(ObjectOutputStream ObjOps) throws IOException {
            ObjOps.defaultWriteObject();
            ObjOps.writeObject("Hi,this is writeObject!");
        }
        private void readObject(ObjectInputStream ObjIs) throws IOException, ClassNotFoundException {
            ObjIs.defaultReadObject();
            System.out.println(ObjIs.readObject());
        }
    }
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Person p = new Person("join",18);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream objout = new ObjectOutputStream(baos);
        objout.writeObject(p);

        for(int i=0;i<baos.toByteArray().length;i++){
            System.out.print(String.format("%x",baos.toByteArray()[i]));
        }

        ByteArrayInputStream bins = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream objin = new ObjectInputStream(bins);
        objin.readObject();

    }
}

使用SerializationDumper工具可以看到,这段我们自定义的数据存储在了objectAnnotation中。