• 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转为序列化文件以及相关数据结构的设计。

  • 相关阅读:
    ElasticSearch集群设置
    NEST与JSON语法对照 一 match与multi_match
    某墙尼妹,用个Response.Filter来解决StackExchange.Exceptional中google cdn的问题
    高频
    Linux 后台执行命令
    Mysql 常用函数汇总
    Golang 昨天、今天和明天
    Golang——Cron 定时任务
    Golang 资料整理
    Golang: for range
  • 原文地址:https://www.cnblogs.com/unicodeSec/p/14202792.html
Copyright © 2020-2023  润新知