什么是反序列化?反序列化的过程,原理
介绍
本篇主要分析 java 序列化、反序列化的过程,原理,并且通过简化版 URLDNS 做案例分析利用链原理。
本篇很重要,打好基础是关键。
什么是序列化
我的理解很简单,就是将对象转化为可以传输、储存的数据。什么意思?正常一个对象,只能存在于程序运行时,当程序运行结束了,对象就消失了,对象只在程序运行时存在
如有这样一个需求,我想把 user 对象(包含了名字,年龄,身高,简历信息)传给数据持久化的服务(保存数据的服务)。需要怎么做?创建一个 json 对象,然后把 user 对象信息填写到 json 中,{"name":"zhangsan","age":12,"height",12,"resume":{"school":"tsinghua","level":1}}然后把这个 json 传给持久化服务,这样就要求每次传输数据时,都要将对象先转为 json 等格式数据,再进行传输,而且这只是简单的数据。如果是更复杂的,对象 A 里有对象 B、C,对象 B 又有 D,则需要手动,将这些数据转成 json,发送到远程服务器,再根据这些数据,还原对象。
而使用序列化,则是将 user 对象序列化为数据,传输到持久化服务器上时,再反序列化,就得到了 user 对象,让对象可以传输。所以序列化,就是将对象转化为数据,相当与给对象拍了个快照,传输或者储存,使用时再反序列化,又变为了对象。
看下 php 序列化和反序列化过程:
执行结果:
再反序列化看下:
php 的序列化将对象转化为了字符串,包含了对象的所有数据信息,反序列化时再根据这些信息还原对象。
【一>所有资源获取<一】1、200 份很多已经买不到的绝版电子书 2、30G 安全大厂内部的视频资料 3、100 份 src 文档 4、常见安全面试题 5、ctf 大赛经典题目解析 6、全套工具包 7、应急响应笔记 8、网络安全学习路线
JAVA 序列化
一个 java 序列化对象例子:
FileOutputStream 和 ObjectOutputStream 是 java 的流操作,可以把 OutputStream 当做一个单向流出的水管,FileOutputStream 打开了文件,就相当于给文件接了一个 File 类型水管,然后把 FileOutputStream 类型对象传给了 ObjectOutputStream,相当于把 File 类型水管接到了 Object 类型水管。
由于 Object 类是所有类的父类,所以 Object 类型水管可以投放任何对象,
这里创建了 Person 对象并传给 writeObject 方法,相当于把 Person 对象扔进了 Object 类型水管,即 Person 对象->Object 类型水管->File 类型水管->文件这样就把 Person 对象写入了文件,
java 输入输出流的方式处理数据真聪明如果我想把序列化对象写入 byte 数组,那就创建个 byteArrayOutputStream 类型水管,然后,把它接到 Object 类型水管上,后面步骤不变,则:Person 对象->Object 类型水管->byte 类型水管->byte 数组
回到正题,打开文件查看是乱码。
因为每个语言都有自己的序列化规则,java 的序列化方式不适用于用文本方式查看。可以使用SerializationDumper工具查看。
红框内对应的是类名,类属性名,成员变量值。如果在类里添加方法,会被序列化吗?答案是不会,调试发现,writeObject 时只会将 FieldValues 即 成员变量序列化,所以,序列化的并不是整个对象,只是对象的属性值。
以上就是 java 序列化的方法了,但仔细看代码,会发现 Person 类继承了 Serializable 接口,但是没有实现接口中的方法,,那继承这接口有什么用?去掉试试。报错了
这是什么原因?看一下 Serializable 接口,接口是空的
调试看一下,为什么非要继承个空接口才能序列化?这空接口起到了什么作用?如图
原因在这里,如果类不继承 Serializable,那对象不属于字符串、数组、枚举类型,那就会进入 else,抛出 NotSerializableException 异常,所以,Serializable,只是用来标志,这个对象可以被序列化的。
在查看 Serializable 接口时,在注释中看到,建议在类里声明 serialVersionUID 变量,不然自动生成的 serialVersionUID 很容易出问题。
因为默认的 serialVersionUID 是 jdk 生成的,不同版本 jdk 可能生成的值不同,看名字应该能猜到 serialVersionUID 是序列化版本的标志,如果序列化数据的 serialVersionUID 和对应类的 serialVersionUID 不一致,就会导致反序列化失败。
java 反序列化
首先看下反序列化代码
成功将上面序列化的对象读出,
和上面的代码区别只是把 Output 换为了 Input,把 writeObject 换为了 readObject。这也很好理解,把单向流出的水管换为单向流入的(Output 换为 Input),然后把写入数据的 writeObject 换为 readObject,即:序列化数据 person.txt->File 类型水管->Object 类型水管->Object 对象。(Person)这个用法是强制类型转换,将 Object 转 Person 类型,
再调试下,看 java 如何从一个二进制的序列化数据,恢复为 java 对象的,
首先读取了二进制的第一个字节,如果不是 0x73,即 10 进制的 115,就抛异常。
根据变量名TC_OBJECT
可以猜出,0x73 标志着数据是 Object 类型。继续调试
发现然后又读了一个字节 x72,对应变量TC_CLASSDESC
,是类的描述:
然后读出类名和 suid(对应 serialVersionUID)。
后面有继续读出了 classDescFlags、Fields 等信息,不一一调试了。对应数据如图:
这一步读出了类的所有信息,储存到 ObjectStreamClass 类型对象 desc 中,
接下来 desc 又 newInstance(),创建了一个对象 obj。
这个 newInstance()是不是很熟悉?在学反射时知道,Class 类、Constructor 类都有 newInstance()方法。看一下 ObjectStreamClass 类的 newInstance()方法,发现,newInstance 调用的是类成员变量 cons 的 newInstance 方法。
所以在这一步,通过调用 Object 类的无参构造函数,创建了 obj 对象,这个对象有 Person 类的所有属性
继续调试,调用了方法。
看起来是将读出的序列化数据赋值给对象,然后继续调试,跟了很久。
putObject 又是个 native 方法,将键值赋值给 obj 对象,这也就能解释的通,为什么在反序列化时,java 不会发出警告。因为如果通过反射修改 private 成员函数值,java 会发出警告,正常反射修改 private 变量会发出警告,如图:
可以利用下吗?执行下面代码试下:
报错
好吧,先不管它了,估计是用不了。
忘了说,调试时发现 readSerialData 方法,它会递归的反序列化当前对象的所有成员变量。
所以,总结,java 在反序列化时,是递归的,将对象反序列化,对象的成员变量反序列化,对象的成员变量的成员变量反序列化。一层层递归调用,直到基本变量,然后一层层再出来,完成最外层对象的反序列化,整个过程是由内向外的。例如对象 A 里包含对象 B,对象 B 包含对象 C,C 包含一个数字 1,将 A 序列化后,再反序列化的过程是,先打开 A,发现 A 里有 B,则再打开 B,发现 B 包含 C,再打开 C,发现 C 里有 1,则读取出 1,完成 C 的反序列化,再把这个 C 给 B,完成 B 的反序列化,再把 B 给 A,完成 A 的反序列化。这个过程特别像把对象 A 一层层扒开,直到遇到基本变量,然后开始由内向外反序列化,
现在知道反序列化过程了,读出序列化的数据,创建一个空的对象,再调用 native 方法,为对象赋值。
java 反序列化利用(URLDNS)
现在了解了序列化与反序列过程。怎么利用呢?
序列化是将对象里的成员变量序列化,进行传输,另一端收到序列化数据后,反序列化得到对象,
在这个过程中,反序列化的数据有可能会被拦截,修改,
所以就是说,反序列化数据可控,也就是对象的的成员变量可控。在之前的文章中提到过,对象的方法执行过程是有可能成员变量影响的,而成员变量可控,那就有希望通过构造成员变量,控制方法的执行。
怎么通过控制对象的成员变量影响方法的执行呢?首先,我们可控的只有变量的值,所以,想执行代码或命令,只能靠 java 自带的类方法,则需要寻找 java 可以被利用的类。
看下面这个代码
先调用genURLDNS
函数,生成序列化对象,保存到文件 out.bin 中,
之后,每调用一次getURLDNS
函数,都会从文件读取序列化数据,然后反序列化,然后导致本机发出一条 dns 请求。
这个 out.bin 就是构造好的恶意序列化数据,也可以叫做 payload,作用是可以控制机器向指定地址发出一条 dns 请求,
那就调试下反序列化的过程,看看什么原因造成的。readObject 之前已经调试过了,但是忽略了一个点,在 readSerialData 方法中调用了 invokeReadObject,而在 invokeReadObject 函数中,判断了当前类是否有 readObject 方法,如果有,则通过反射的方式调用该类的 readObject 方法(例如反序列化 A 类时,如果 A 类有 readObject 方法,那就按 A 类自带的 readObject 方法反序列化,如果没有,那就用默认的方式反序列化)。
而问题的产生,就在 HashMap 的 readObject 方法中。这里调用了hash(key)
调用了 key 的 hashCode 方法
而 key 是 URL 对象,看下 URL 对象的 hashCode 方法,首先判断 hashCode,不为-1,则调用 handler.hashCode。
继续向下跟
而 getHostAddress 过程中发现这一步骤
正是这句代码,导致了 dns 查询,
所以总结下,反序列化导致 dns 查询的过程,ObjectInputStream 的 readObject 方法 =》 HashMap 的 readObject 方法 => 创建 HashMap 对象,并 putVal(hash(key)) key 是 URL 对象 =》 URL 的 hashCode 方法 =》 URLStreamHandler 的 hashCode 方法 =》URLStreamHandler 的 getHostAddress 方法 =》 InetAddress.getByName 方法。
这个就是造成 dns 查询的利用链,也叫做 Gadget,
再回头看下代码,开始图片中标注的“这里先不看”。
看注释说明,是为了防止造成两次 dns 查询,现在明白了 URLDNS 的利用链,分析下原因。因为 hashMap.put 一个 url 对象时会调用,putVal(hash(key))
代码,而上面总结过 hash(URL 对象)会造成 dns 查询。
f 是 hashCode 方法的对象,把 url 对象的 hashCode 的值改为非-1,然后 put(url),再把 hashCode 的值改回-1 回想一下“URL 的 hashCode 方法”中,hashCode 不为-1 时,直接 return,就不会有后面的 dns 查询了,利用链到“URL 的 hashCode 方法”就断掉了。
而 hashCode 默认值就是-1,所以通过这种方式,就可以避免在构造恶意序列化对象时,触发 dns 请求。
总结
造成反序列化漏洞的根本原因分析
序列化的内容只有类名和成员变量,所以可控点是,类名和成员变量的值。通过控制类名、可以指定反序列化的类,通过控制变量的值,就可以影响代码执行流程。然后按照我们的期望,将这些“影响”连接在一起,就可以控制代码的执行。例如本例中,影响 1:URL 对象赋值了一个 http://开头的 url 地址,导致它只要调用 hashCode 就会查询 dns(如果不赋值一个 http://开头的 url 地址,就不会查询)。影响 2:为 HashMap 赋值了一个 URL 对象,导致它在反序列化时,会计算 URL 对象的 hash(如果不赋值,就不会计算 hash)。上面 dns 查询,计算 hash,这两个动作,都是因为两个变量的值导致的,所以:控制变量的值,会影响代码的执行流程。
当一个影响产生了“动作”,就可以触发下一个影响,再触发下一个影响,最后达到预期效果:命令执行,而这些能搭配使用且能达到我们预期效果的“影响”们,就是利用链。而这个“动作”来源,只能是类的 readObject 方法,因为默认的反序列化过程,只是简单的赋值,,变量的值不会对代码的执行造成任何影响,也就不会有利用链。类自定义的 readObject 方法,初衷是为了反序列化出完整的对象(有些类很复杂,简单的赋值不能复原对象)。但是这个 readObject 方法会受变量值得影响,产生“动作”,触发下一个“影响”。
这就像,每个类都是一个形状不确定的零件,当这个类被实例化后,对象的变量值确定了,这个零件的形状就固定了,不同的变量值,会导致零件形状不同。而我们要做的,就是从 java 自带的这些不确定的零件中找出,固定形状后可以像多米诺骨牌一样,后一个可以推倒前一个的这种零件。然后将这些零件按多米诺骨牌一样排列,最后一个零件倒下,就会按动致命令执行按钮,只要第一个零件被推倒,就会引发后面一系列连锁反应,导致命令执行。而推倒第一个零件的,就是 readObject 方法,因为它是唯一一个,在反序列化时,受变量值影响代码执行过程的函数,所以推倒第一个零件的动作,由它发出。
评论