Java反序列化协议解析 一

年前开始研究Java反序列化,一直没有研究Java反序列化文件的格式。最近闲来无事,研究一下java反序列化文件的格式。扯这么多的原因主要是要达成两个目标

  1. 尝试使用python读取java反序列化文件,并转换为Json格式以方便阅读(其实没多少用,还是一样难懂)。
  2. json定义反序列化,随意编辑反序列化流,通过json生成序列化文件。缩小某些payload工具的大小,生成一个exp需要N多依赖的jar包,这不讲伍德

首先我们先大致讲解一下java序列化以及相关东西。

  1. java序列化功能只传递对象的属性,不传递对象的方法,静态变量等等。你可以把java序列化功能类比Json传输。
  2. java存在八种基本类型,分别是字节,整型数字,浮点数,布尔值。至于对象,其实是这八种基本类型的复杂组合。无论多么复杂的对象,其实都可以被解析为这八种基本类型。当然这八种基本类型在其他语言中也存在,为我们使用python解析反序列化文件提供了理论基础。

1 基础类型的读取

序列化中也有基础类型。我们需要定义几个函数用来读取基础类型。
例如

  • 一个Byte一般作为指令,标记后面的数据流是什么
  • int为4个Byte,也就是32位byte
  • Short为2个Byte,是16位
  • 以此类推,按照c语言的数据类型长度定义,千万别乱写
  • 字符串类型,在这里我们只讨论ascii编码的情况,暂时不讨论Utf编码的情况。在序列化中,一个字符串类型,首先读取两个Byte,也就是short类型,作为字符串的长度。随后按照字符串的长度,读取Byte作为字符串。

2 序列化中控制指令

前面我们提到,读取一个Byte作为控制指令,控制指令的作用是标记后面的数据类型,序列化中控制指令如下

  • TC_NULL = b'\x70' 标记后面的数据为空,对应java就是Null
  • TC_REFERENCE = b'\x71 java序列化协议是一个格式十分紧凑的协议,是不会出现两个一摸一样的对象,类等。如果第二次出现,则会通过reference去指向之前的那个内容。你可以把这个类比为指针。
  • TC_CLASSDESC = b'\x72' 这个是处理并返回类描述符。。与下面的class的区别在于,这个返回的是描述类的一个对象,主要包括类的名称,suid等各种属性。
  • TC_CLASS = b'\x76'在java序列化中,类的传输通过名称,suid等属性,对端通过名称查找classpath中该类。而这个TC_CLASS将会根据上面的类描述符,通过Class.forName去查找这个类。
  • TC_OBJECT = b'\x73' 标记后面的数据为Object对象
  • TC_STRING = b'\x74'标记后面的数据字符串。与基本类型中字符串的区别在于,这里面读取的字符串将会被缓存,如果出现第二个一模一样的字符串,则通过reference的方式,直接读取缓存中的字符串
  • TC_ARRAY = b'\x75'标记后面的数据为数组类型
  • TC_BLOCKDATA = b'\x77' 在对象的WriteObject方法中,我们可以自定义的写入数据,除了非Object数据,其他所有数据将会被写在一起,也就是BlockData。当然,只有readObject方法中,合适的读取顺序才可以成功还原blockdata。
  • TC_ENDBLOCKDATA = b'\x78' 在readObject中,表明数据已经读取完毕
  • TC_EXCEPTION = b'\x7B' 表明后面需要读取一个exception类型的对象
  • TC_PROXYCLASSDESC = b'\x7D' 读取一个动态代理的对象

3 还原反序列化流

有了上面的知识作为基础,下面我们尝试还原反序列化流中的内容。当然我们并没有按照某些特定的顺序去讲解。

3.1 还原类的描述符(ClassDesc

类的描述符为序列化协议中的基石,它表明了后面的数据类型以及读取方法。类的描述符,一共有以下几个字段

  • name 字符串类型,类的名称
  • suid long类型,类的suid,为了防止兼容性而设置的一个值。同一类的不同版本可能suid不一直,这样防止不兼容的情况发生
  • flag 表明类是否为反序列化,是否存在writeObject方法,也就是额外写入数据等等
  • field 类中包含的数据类型列表,
  • 父类,父类也是类的描述符
  • 类的额外信息

field

这里只包含类的数据类型的名称与类型,不包括值。一定注意

读取父类

由于java不支持多继承,所以在这里只有两种情况,继承自一个父类和没有父类这两种情况。在这里我们只需要递归读取,直到控制指令为TC_NULL,即父类为空,作为结束递归的条件。

类的描述符,记得要缓存。计算handle的时候,后面的值可能会引用该类的描述符

下面用一图总结类的描述符的的协议结构

| utf 类名|4Byte suid|1Byte flags|2Byte 字段数量|字段详细内容|父类|类的额外数据|

3.2 还原数组(TC_ARRAY

我们知道,java的数组中的内容只能为同一类型。所以在处理数组信息的时候,首先读取类描述符,表明数组中的内容的数据类型。然后读取数组的长度。最后按照数组长度以及数组类型,去读取并还原数组中的数据。

|ClassDesc|Int length|数组数据|

数组也会被缓存,并被计算为handle

3.3 还原对象

还原对象的数据其实特别简单。首先读取类的描述符,然后紧接着按照类描述符的字段读取数据即可。所以在这里,一个byte出错,将会导致后面的数据全部读取错误。
在这里需要注意几点

  1. 首先读取父类中字段的值,然后再读子类的字段值,在这里我们使用栈数据结构去解决读取
  2. 如果父类包含额外信息(例如writeObject写入),则首先读取父类包含的额外信息,再去读取子类的额外信息
  3. 如果对象继承自EXTERNALIZABLE接口,则无法单纯通过流中数据还原对象中的值。因为java将不会负责字段值的读取写入,这一切都由开发人员决定哪些字段被保存以及保存的方法。这也就是weblogic中反序列化触发XXE的漏洞原理,出现问题的类基本都继承自EXTERNALIZABLE接口,且通过xml定义被保存的对象
  4. 如果对象继承自Serializable接口,且存在writeObject方法。当writeObject方法中没有通过ObjectOutputStream.defaultWriteObject将类的默认字段写入到序列化流中,也无法还原对象的值。原因在于,反序列化中读取类的值按照类的字段顺序去读取,如果没有调用defaultWriteObject写入,则相当于顺序不可知,也是无法单纯通过流中数据去还原对象

当然,对象中字段的值有可能还是对象,需要递归读取,直到读取所有的字段为基础数据类型。在这里建议设置递归的最大深度,防止出现爆栈的异常。

4. 总结

目前完成各种复杂对象的读取,例如x友的exp读取,weblogic 反序列化Exp的读取,并转换为json。不过代码还未写完,地址暂时为https://github.com/potats0/javaSerializationDump/blob/main/main.py

截图如下,读取x友exp


x友exp转json的结果
https://gist.github.com/potats0/108fe530350d14b11aba79736d6a3f0c#file-ncexp-json

下篇文章将谈一下如何将json转为序列化文件以及相关数据结构的设计。

posted @ 2020-12-28 17:50  potatso  阅读(919)  评论(0编辑  收藏  举报