3-java安全——java序列化机制

Posted songly_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了3-java安全——java序列化机制相关的知识,希望对你有一定的参考价值。

最近在学习java反序列化漏洞,为了更好的理解漏洞原理,打算从零开始学习java序列化和反序列化机制。

java序列化跟C语言类似,C语言序列化的数据类型对象是结构体,java反序列化的数据类型则是java对象,但java对象反序列化不涉及方法和静态属性的操作。

为什么不涉及方法和静态属性?因为方法一般都固定存放在代码区,内容不变,静态属性则是属于java类的,而对象的成员属性的内容是不固定,说白了java反序列化是对java对象成员属性的操作。

java序列化实现方式:

  1. 实现java.io.Serializable接口,使用默认java内置的序列化过程
  2. 实现java.io.Externalizable接口,并实现readExternal方法和writeExternal方法,自定义序列化过程

Serializable接口为例,创建一个文件对象流,文件对象输出流objectOutputStream类的writeObject函数完成对象的序列化,文件对象输入流ObjectInputStream类的readObject函数完成对象反序列化

package com.test;
import java.io.*;

//实现Serializable接口
class Student implements Serializable{
    private int id;
    private String name;
    private float score;
//transient关键字修饰成员属性
    private transient String address;

    public Student(int id, String name, float score, String address) {
        this.id = id;
        this.name = name;
        this.score = score;
        this.address = address;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\\'' +
                ", score=" + score +
                ", address='" + address + '\\'' +
                '}';
    }
}

class Serialize {
//反序列化
    public static Student Student_Unserialize() throws IOException, ClassNotFoundException{
        File file = new File("stu.txt");
        FileInputStream fileInputStream = new FileInputStream(file);
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        Student student = (Student) objectInputStream.readObject();
        objectInputStream.close();
  System.out.println("反序列完成......" + student);
        return student;
    }

//序列化
    public static void Student_Serialize() throws  IOException{
        File file = new File("stu.txt");
        FileOutputStream fileOutputStream = new FileOutputStream(file);
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        Student student = new Student(10,"liubei",66.5f, "beijing");
        objectOutputStream.writeObject(student);
        objectOutputStream.close();
        System.out.println("序列化完成......");
    }
}

public class Serialize_Test3 {
    public static void main(String[] args) throws Exception {
        Serialize.Student_Serialize();
        Serialize.Student_Unserialize();
    }
}

student对象序列化后存储到stu文件中的数据格式如下所示

       writeObject函数序列化后以字节流形式将数据写入到永久保存在磁盘文件中,但这样的数据格式根本无法阅读。

借助一个java反序列化字节转字符串工具SerializationDumper,将字节数据转换成可阅读的字符串数据,下载工具后,如果是在windows下就运行build批处理文件生成SerializationDumper.jar工具(linux下运行build.sh文件),将序列化后的stu.txt文件拷贝到该目录下,在cmd窗口执行java -jar SerializationDumper.jar -r stu.txt > res.txt命令,生成转换后的字符串文件res,如下所示:

转换后的res.txt文件内容如下所示:


STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
  TC_OBJECT - 0x73
    TC_CLASSDESC - 0x72
      className
        Length - 16 - 0x00 10
        Value - com.test.Student - 0x636f6d2e746573742e53747564656e74
      serialVersionUID - 0xeb 7d 39 0e 5a 26 89 3e
      newHandle 0x00 7e 00 00
      classDescFlags - 0x02 - SC_SERIALIZABLE
      fieldCount - 3 - 0x00 03
      Fields
        0:
          Int - I - 0x49
          fieldName
            Length - 2 - 0x00 02
            Value - id - 0x6964
        1:
          Float - F - 0x46
          fieldName
            Length - 5 - 0x00 05
            Value - score - 0x73636f7265
        2:
          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
      com.test.Student
        values
          id
            (int)10 - 0x00 00 00 0a
          score
            (float)1.11601254E9 - 0x42 85 00 00
          name
            (object)
              TC_STRING - 0x74
                newHandle 0x00 7e 00 03
                Length - 6 - 0x00 06
                Value - liubei - 0x6c6975626569

从文件中可以看到java对象序列后的内容由两部分组成:

  1. 类描述
  2. 一部分是对象的数据,类字段的值

具体属性信息参考:https://xz.aliyun.com/t/8686

STREAM_MAGIC:表示魔数,可以理解为这是java序列化后的数据文件的填充,并且这个值是固定的,用于表示该类型的数据是由java序列化产生的

STREAM_VERSION:表示java序列化流的版本信息,并且这个值会写入到文件头的位置。

魔术和版本两个标记定义在接口中java.io.ObjectStreamConstants,该接口中定义了很多标记,如下所示

    final static short STREAM_MAGIC = (short)0xaced;
    final static short STREAM_VERSION = 5;
    final static byte TC_NULL = (byte)0x70;
    final static byte TC_REFERENCE = (byte)0x71;
    final static byte TC_CLASSDESC = (byte)0x72;
    final static byte TC_OBJECT = (byte)0x73;
    final static byte TC_STRING = (byte)0x74;
    final static byte TC_ARRAY = (byte)0x75;
    final static byte TC_CLASS = (byte)0x76;
    final static byte TC_BLOCKDATA = (byte)0x77;
    final static byte TC_ENDBLOCKDATA = (byte)0x78;
    final static byte TC_RESET = (byte)0x79;
    final static byte TC_BLOCKDATALONG = (byte)0x7A;
    final static byte TC_EXCEPTION = (byte)0x7B;
    final static byte TC_LONGSTRING = (byte) 0x7C;
    final static byte TC_PROXYCLASSDESC = (byte) 0x7D;
    final static byte TC_ENUM = (byte) 0x7E;
    final static int  baseWireHandle = 0x7E0000;

包括TC_OBJECT和TC_CLASSDESC都在ObjectStreamConstants接口中有定义

TC_OBJECT:作用是用于声明,即一个新对象的声明,表示接下来的数据是新创建的一个对象

TC_CLASSDESC:一般位于TC_OBJECT,用于描述当前序列化对象的类描述信息(元数据部分)

newClassDesc表示表示对象的类描述信息,是一个ObjectStreamClass对象,该对象保存了类名、序列化ID、类字段等描述信息,可以利用反射通过ObjectStreamClass对象创建一个其中保存类名对应的对象。TC_CLASSDESC中表示的是类描述信息,从TC_OBJECT TC_CLASSDESC(73 72)开始,到TC_ENDBLOCKDATA TC_NULL(78 70)结束。

      className
        Length - 16 - 0x00 10
        Value - com.test.Student - 0x636f6d2e746573742e53747564656e74
      serialVersionUID - 0xeb 7d 39 0e 5a 26 89 3e
      newHandle 0x00 7e 00 00
      classDescFlags - 0x02 - SC_SERIALIZABLE
      fieldCount - 3 - 0x00 03
      Fields
        0:
          Int - I - 0x49
          fieldName
            Length - 2 - 0x00 02
            Value - id - 0x6964
        1:
          Float - F - 0x46
          fieldName
            Length - 5 - 0x00 05
            Value - score - 0x73636f7265
        2:
          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

className中的Length表示当前对象所属的类全名长度,Value表示当前对象所属的类全名

serialVersionUID:表示该类中定义的serialVersionUID对应的值

newHandle :表示序列中的下一个数值将赋值给一个可序列化或者可执行反序列化的对象引用

classDescFlags:标识是否具备可序列化,02表示可序列化

fieldCount:当前对象的属性字段个数

classAnnotations:表示存储对象的数据块(Data-Block)的结束标记为0x78,也就是类描述信息的结束标记,说白了就是com.test.Student类的描述信息结束了

superClassDesc:表示该类的父类的描述信息,TC_NULL表示没有父类对象引用

Fields:是当前对象的属性字段的具体描述信息(属性名,长度,数据类型),Fields中的className表示如果成员属性是一个对象,则会通过TC_STRING的引用来引用该成员属性,TC_STRING标记表示一个new String(新的字符串对象)

解释一下newHandle 中的引用概念,这里的引用是对java序列化数据(类描述信息)的引用,在进行序列化的时候,同类型对象在序列化时,第一次序列化会生成类描述信息,之后都会使用对象->引用的映射方式来操作。在第一次创建新对象时,newHandle的值是从baseWireHandle变量的值开始的。

baseWireHandle定义在ObjectStreamConstants接口中,baseWireHandle可以理解为newHandle 的引用值计数的基数,也就是说newHandle的值从0x7e 00 00开始

classdata描述的是类数据中所有内容

   classdata
      com.test.Student
        values
          id
            (int)10 - 0x00 00 00 0a
          score
            (float)1.11601254E9 - 0x42 85 00 00
          name
            (object)
              TC_STRING - 0x74
                newHandle 0x00 7e 00 03
                Length - 6 - 0x00 06
                Value - liubei - 0x6c6975626569

      可以看到classdata描述了Student中的所有成员id,score,name属性的信息。

关于transient关键字,这里有一个问题,Student类定义了4个成员属性,但序列化后只看到了id,name,score这几个成员属性

       原因在于成员属性address是被transient关键字修饰了,transient关键字在Java中是一个针对序列化的特殊关键字,被它修饰过的域具有“不会序列化”的语义,因此在序列化后的字节文件中并没有成员属性address。需要注意的是:transient关键字只能用来修饰成员属性,不能修饰类和成员方法。

接下来,我们通过程序的执行结果来理解transient关键字

       从程序输出结果来看,当Student对象序列化时,会把Student对象当前的状态存储到一个文件中,由于address成员属性被transient关键字修饰不会参与序列化过程,因此只有前三个属性的状态会存储下来,而address成员属性当时的状态不会被存储到文件中。然后在反序列化时,程序会从文件中读取Student对象当时存入的状态并还原成java对象,因此前三个属性都会被准确还原当时的状态,但address成员属性当时的状态由于没有存储到文件中,因此程序会读取到null。

         这也是为什么被transient关键字修饰的成员属性在反序列化时值为null的原因。

       通过本篇的学习,相信你对java序列化原理有了一定的认识,下一篇我们将分析java序列化底层流程,深入理解序列化原理。

以上是关于3-java安全——java序列化机制的主要内容,如果未能解决你的问题,请参考以下文章

4-Web安全——java序列化机制分析

3-Web安全——java的序列化机制

阶段1 语言基础+高级_1-3-Java语言高级_05-异常与多线程_第3节 线程同步机制_4_解决线程安全问题_同步代码块

5-java安全——java反序列化机制分析

5-Web安全——java反序列化机制分析

12.8 Java 9改进的对象序列化