写点什么

什么是反序列化?反序列化的过程,原理

  • 2021 年 12 月 16 日
  • 本文字数:5368 字

    阅读完需:约 18 分钟

什么是反序列化?反序列化的过程,原理

介绍

本篇主要分析 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 序列化对象例子:


class Person implements Serializable{    public String name;    public int age;    Person(String name,int age){        this.name = name;        this.age = age;    }}
public class Main { public static void main(String []args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException { FileOutputStream out =new FileOutputStream("person.txt"); ObjectOutputStream obj_out = new ObjectOutputStream(out); obj_out.writeObject(new Person("z3",12)); }}
复制代码


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 接口,接口是空的


public interface Serializable {}
复制代码


调试看一下,为什么非要继承个空接口才能序列化?这空接口起到了什么作用?如图



原因在这里,如果类不继承 Serializable,那对象不属于字符串、数组、枚举类型,那就会进入 else,抛出 NotSerializableException 异常,所以,Serializable,只是用来标志,这个对象可以被序列化的。


在查看 Serializable 接口时,在注释中看到,建议在类里声明 serialVersionUID 变量,不然自动生成的 serialVersionUID 很容易出问题。



因为默认的 serialVersionUID 是 jdk 生成的,不同版本 jdk 可能生成的值不同,看名字应该能猜到 serialVersionUID 是序列化版本的标志,如果序列化数据的 serialVersionUID 和对应类的 serialVersionUID 不一致,就会导致反序列化失败。

java 反序列化

首先看下反序列化代码


class Person implements Serializable{    public String name;    public int age;    Person(String name,int age){        this.name = name;        this.age = age;    }}
public class Main { public static void main(String []args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException { FileInputStream in =new FileInputStream("person.txt"); ObjectInput obj_in = new ObjectInputStream(in); Person p = (Person) obj_in.readObject(); System.out.println(p.name); }}
复制代码


成功将上面序列化的对象读出,


和上面的代码区别只是把 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 方法。


private Constructor<?> cons;
复制代码


所以在这一步,通过调用 Object 类的无参构造函数,创建了 obj 对象,这个对象有 Person 类的所有属性


继续调试,调用了方法。


readSerialData(obj, desc);
复制代码


看起来是将读出的序列化数据赋值给对象,然后继续调试,跟了很久。



putObject 又是个 native 方法,将键值赋值给 obj 对象,这也就能解释的通,为什么在反序列化时,java 不会发出警告。因为如果通过反射修改 private 成员函数值,java 会发出警告,正常反射修改 private 变量会发出警告,如图:



可以利用下吗?执行下面代码试下:


Unsafe unsafe = Unsafe.getUnsafe();Person p = new Person("z3",12);unsafe.putObject(p, 16, "z5"); // 至于第二个参数为什么是16,不知道,调试时发现java就这么调的,可能16是"name"的一个标志?System.out.println(p.getName());
复制代码


报错



好吧,先不管它了,估计是用不了。


忘了说,调试时发现 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 方法,因为它是唯一一个,在反序列化时,受变量值影响代码执行过程的函数,所以推倒第一个零件的动作,由它发出。

用户头像

我是一名网络安全渗透师 2021.06.18 加入

关注我,后续将会带来更多精选作品,需要资料+wx:mengmengji08

评论

发布
暂无评论
什么是反序列化?反序列化的过程,原理