0x00 前言
近一两个月遇到好多问反序列化的问题,虽然之前有了解和学习这方面的知识,但内容都比较分散,而且也没有深刻学习,本文大概就是整理一下反序列化方面的内容。
0x01 引子
了解一个东西的三大核心点就是这个东西是什么?为什么?怎么做?
所以这里大概也会从这三个角度来阐述。
我们先从一个例子切入反序列化
我们都打过游戏,特别是RPG游戏。一般的RPG游戏都具有以下几个特点:
- 角色等级、各种属性、血量、蓝量、经验值
- 怪物等级、血量、蓝量、各种属性
- 装备等级、品质、属性
在游戏世界中,我们所见的以上特点,都可以看成是一个个对象,打游戏的时候创建了一个角色,那就是创建了一个角色对象,同样的道理,野怪和装备也都是对象。
我们打游戏要变强就要获取装备以及获取经验值让角色升级。这些东西,读计算机的我们都知道,都是一条条数据。这些数据以及对象都是存储在内存中的。而读过计算机的我们也知道,内存中的数据只是暂时存储的,一旦我们关机断电,那么这些东西就会消失。但当我们第二天或者几分钟后重新打开电脑,运行这个游戏,点击这个存档时,会发现我们的角色身上的装备以及经验条、等级,都跟关机前一模一样。
为啥子呢?
这时就有人说:数据库。
是的,但数据库的数据又是存储在哪里呢?答案就是硬盘。
硬盘存储的数据在关机后是不会丢失的,这种保存技术就叫对象持久化,也叫对象序列化。而对对象序列化逆过程就叫反序列化。
简单地说,反序列化就是将硬盘中的信息读取出来形成对象。
0x02 是什么
有了上述的引子,我们很容易就能归纳出什么是序列化和反序列化了
- 序列化就是将对象实例的状态存到存储媒体的过程
- 反序列化就是将存储在存储媒体中的对象状态转换成对象的过程
所以,序列化和反序列化也可以这样理解
- 序列化:把对象转换成字节序列的过程
- 反序列化:把字节序列恢复为对象的过程
0x03 机制
序列化的最终目的就是为了对象可以跨平台存储和进行网络传输。平时我们都是通过IO进行跨平台存储和网络传输,而IO所支持的数据格式,就是字节数组。
但问题又来了,我们把对象转换成字节数组后,要怎么转回来呢?就好比密码学里面我们把明文转成密文十分简单,但是拿着密文要如何转成明文呢?
这就要求我们必须在把对象转成字节数组的时候就制定一种规则(类似密码学里面的密钥),有了这个规则,我们就可以从IO流里面读出数据的时候以这个规则把对象还原回来。
0x04 常见的规则
序列化只是定义了拆解对象的具体规则,这种规则实际上是由很多的(就跟密码学一样,有多种多样的加密方法),现在比较常见的序列化方式有:JDK原生、JSON、ProtoBuf、Hessian、Kryo等
JDK原生
可以通过ObjectOutStream类将对象变成byte[]字节数组。
不过这个方式生成的字节流比较大,不支持跨语言。
ProtoBuf
谷歌的东西,可用于通信协议、数据存储等,因为序列化后体积小,一般用于对传输性能较高要求的系统
Hessian
主要用于二进制数据
Kryo
限定在JVM语言上,在序列化速度上很有优势,底层依赖于字节码生成机制
JSON
JSON跟上面的几个方法不一样,上面的几个方法都是直接将对象变成二进制(byte[]字节数组),而JSON则是生成一串有规则的字符串。在可读性上要优于上面的几种方法,但在体积上就没啥优势了。
但是JSON不跟任何编译语言绑定,因此具备了跨平台功能。
0x05 选择
序列化有多种方式,每种方式都有它的优点和缺点,所以在使用时,要从以下几个方向来考虑
- 协议是否支持跨平台
- 序列化的速度
- 序列化生成的体积
以上是开发的时候需要思考的问题,但并不是我们今天这篇文章的重点内容,所以简单提及一下。
0x06 java反序列化漏洞
在讲解漏洞前我们需要了解两个东西,一个是java执行程序,另一个是java反射机制。
java执行程序
在java中,我们可以通过java.lang.Runtime类调用操作系统命令或执行一个可执行程序
java反射机制
java的反射机制允许程序在运行期间借助Reflection API取得任何类的内部信息,并且能够直接操作类和对象的所有属性和方法。
这是一个比较官方的说法,但我们可以梳理一下。
当我们要使用一个类时,会先把这个类加载到虚拟机中,然后堆内存的方法区中会出现一个Class类型的对象(注意,一个类只有一个对象),这个对象包含了完整的类的所有结构信息。这时我们就可以通过这个对象看到类的结构。对象就好像一个镜子一样,把类的结构全部照出来,这个就叫反射。
反射中经常用到以下的方法
1.获取Class实例的方式
方式1:调用运行时类的属性
.class方式2:通过运行时的对象调用
getClass()方式3:调用Class的静态方法
forName(String classPath)方法4:使用类的加载器
classloader2.创建运行时类的对象
newlnstance()调用此方法,创建对应的运行时类的对象3.获取运行时类的结构
getFields()获取当前运行时类及其父类中声明为public访问权限的属性
getDeclaredFields()获取当前运行时类中声明的所有属性
getMethods()获取当前运行时类及其所有父类声明为public的方法
getDeclaredMethods()获取当前运行时类中声明的方法,不包含父类
getConstructors()获取当前运行时类声明为public的构造器
getDeclaredConstructors()获取当前运行时类中声明的所有构造器
invoke()方法允许调用包装在当前Method对象中的方法
当我们了解了反射机制后,就可以尝试利用invoke()来执行命令
1 | public class test2 { |
使用runtime调用获得的Method对象所声明的公开方法,即exec,并讲calc.exe作为参数传入,而runtime为获取的Runtime.getRuntime实例对象。因此上述代码就相当于执行了Runtime.getRuntime().exec("calc.exe"),成功通过java反射机制启动了计算器。
为什么
那么我们为什么要使用java的反射机制呢?直接创建一个对象不是更方便吗?
虽然直接创建一个对象十分方便,但如果我们有很多个类,每个用户所需求的对象不同,那么我们虽然直接创建对象是否方便,但在这种情况,需要不断地去new一个对象,十分不灵活。这时我们使用java地反射机制,这样就可以在运行时确定类型,绑定对象,动态编译最大限度发挥了java的灵活性。
java反序列漏洞
通过之前的内容,我们知道序列化是指把对象转换成字节序列的过程,反序列化则是打开字节流并重构对象。那么,如果我们对将要被进行反序列化的数据进行特殊构造,那么就能产生非预期的对象,从而导致任意代码执行。
java中间件通常会通过网络接收客户端发送的序列化数据,而在服务端对序列化数据进行反序列化时,会调用被序列化对象的readObject()方法。如果java重写了某个类的方法,就会优先调用修改后的方法。
如果能够找到满足上述条件的对象进行序列化并发送给Java中间件,Java中间件也会去执行指定的代码,即存在反序列化漏洞。