qiqing's Blog.

反序列化学习

字数统计: 1.7k阅读时长: 6 min
2020/03/09 Share

反序列化学习

学习反序列化漏洞是如何产生的,和反序列化漏洞的利用过程中需要注意哪些影响。

序列化

序列化 (Serialization) 是指将数据结构或对象状态转换成字节流 (例如存储成文件、内存缓冲,或经由网络传输) ,以留待后续在相同或另一台计算机环境中,能够恢复对象原来状态的过程。序列化机制在Java中有着广泛的应用,EJB、RMI、Hessian等技术都以此为基础。

在php中,序列化之后的字符串是可读的,在java中,序列化之后有些字符串不可读,但是,有规律可循。

1
2
3
4
5
6
ac ed 00 05 73 72 00 11  53 65 72 69 61 6c 69 7a    ....sr.. Serializ
61 74 69 6f 6e 44 65 6d 6f d9 35 3c f7 d6 0a c6 ationDem o.5<....
d5 02 00 02 49 00 08 69 6e 74 46 69 65 6c 64 4c ....I..i ntFieldL
00 0b 73 74 72 69 6e 67 46 69 65 6c 64 74 00 12 ..string Fieldt..
4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e Ljava/la ng/Strin
67 3b 78 70 00 01 7d f1 74 00 05 67 79 79 79 79 g;xp..}. t..gyyyy
  • 0xaced,魔术头
  • 0x0005,版本号 (JDK主流版本一致,下文如无特殊标注,都以JDK8u为例)
  • 0x73,对象类型标识 0x7n基本上都定义了类型标识符常量,但也要看出现的位置,毕竟它们都在可见字符的范围,详见java.io.ObjectStreamConstants
  • 0x72,类描述符标识
  • 0x0011...,类名字符串长度和值 (Java序列化中的UTF8格式标准)
  • 0xd9353cf7d60ac6d5,序列版本唯一标识 serialVersionUID,简称SUID,在反序列化中非常重要)
  • 0x02,对象的序列化属性标志位,如是否是Block Data模式、自定义writeObject()SerializableExternalizableEnum类型等
  • 0x0002,类的字段个数
  • 0x49,整数类型签名的第一个字节,同理,之后的0x4c为字符串类型签名的第一个字节 (类型签名表示与JVM规范中的定义相同)
  • 0x0008...,字段名字符串长度和值,非原始数据类型的字段还会在后面加上数据类型标识、完整类型签名长度和值,如之后的0x740012...
  • 0x78 Block Data结束标识
  • 0x70 父类描述符标识,此处为null
  • 0x00017df1 整数字段intField的值 (Java序列化中的整数格式标准) ,非原始数据类型的字段则会按对象的方式处理,如之后的字符串字段stringField被识别为字符串类型,输出字符串类型标识、字符串长度和值

需要注意的是,Java序列化中对字段进行封装时,会按原始和非原始数据类型排序 (有同学可能想问为什么要这么做,这里我只能简单解释原因有两个,一是因为它们两个的表现形式不同,原始数据类型字段可以直接通过偏移量读取固定个数的字节来赋值;二是在封装时会计算原始类型字段的偏移量和总偏移量,以及非原始类型字段的个数,这使得反序列化阶段可以很方便的把原始和非原始数据类型分成两部分来处理) ,且其中又会按字段名排序。

序列化的过程,此处略过,目前只有反序列化时有漏洞,我们只需要了解序列化后的数据格式就好

反序列化

这里需要了解反序列化时,Java是如何处理的。

它的执行流程如下:

  1. ObjectInputStream实例初始化时,读取魔术头和版本号进行校验

  2. 调用

    1
    ObjectInputStream.readObject()

    开始读对象数据

    • 读取对象类型标识

    • readOrdinaryObject()
      
      1
      2
      3
      4

      读取数据对象

      -
      readClassDesc()
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16

      读取类描述数据

      - 读取类描述符标识,进入分支`readNonProxyDesc()`
      - 读取类名
      - 读取SUID
      - 读取并分解序列化属性标志位
      - 读取字段信息数据
      - `resolveClass()`根据类名获取待反序列化的类的`Class`对象,如果获取失败,则抛出`ClassNotFoundException`
      - `skipCustomData()`循环读取字节直到Block Data结束标识为止
      - 读取父类描述数据
      - `initNonProxy()`中判断对象与本地对象的SUID和类名 *(不含包名)* 是否相同,若不同,则抛出`InvalidClassException`

      - `ObjectStreamClass.newInstance()`获取并调用离对象最近的非`Serializable`的父类的无参构造方法 *(若不存在,则返回`null`)* 创建对象实例

      -
      readSerialData()

      读取对象的序列化数据

      • 若类自定义了readObject(),则调用该方法读对象,否则调用defaultReadFields()读取并填充对象的字段数据

之后再学习yoserial的时候,会详细跟踪CommonsCollections的漏洞利用过程,从源码层次学习反序列化漏洞是如何利用的。

通过反序列化的流程可以知道影响反序列化的成功与否,与3个校验有关,第一个魔术头和版本号,这个一般都没问题,第二个是在类的加载路径中是否有该类(因为该类可能在项目中,有多个jar包中都存在该类,需要对类的加载路径进行校验),第三个SUID和类名是否相同,这3个校验中,最重要的就是SUID能否和目标的保持一致。

关于SUID

在Java的序列化机制中,SUID占据着很重要的位置,它相当于一个对象的指纹信息,可以直接决定反序列化的成功与否,通过上面对序列化和反序列化流程的分析也可以看出来,若SUID不一致,是无法反序列化成功的。

SUID的生成关联了对象的许多东西,但是父类和非原始数据类型字段的类内部发生变更时,不会影响到当前类的SUID值。但是,反序列化漏洞利用过程中,一般不太需要考虑这个值,只是有了这个值,不同的版本不能相互使用。

反序列化的常用接口

反序列化的接口很多:

最常见的 readObject

readResolve

许多应用会实现自己的反序列化接口,之后找到了就记录下

到这里,反序列化的过程已经很清晰了,对于我而言,不需要知道序列化的过程,只需要知道序列化后的数据,需要知道反序列化的过程,如何找反序列化的漏洞。

参考:

浅析Java序列化和反序列化

CATALOG
  1. 1. 反序列化学习
    1. 1.0.0.0.1. 序列化
    2. 1.0.0.0.2. 反序列化
    3. 1.0.0.0.3. 关于SUID
    4. 1.0.0.0.4. 反序列化的常用接口