反序列化

0x00 前言

近一两个月遇到好多问反序列化的问题,虽然之前有了解和学习这方面的知识,但内容都比较分散,而且也没有深刻学习,本文大概就是整理一下反序列化方面的内容。

0x01 引子

了解一个东西的三大核心点就是这个东西是什么?为什么?怎么做?

所以这里大概也会从这三个角度来阐述。

我们先从一个例子切入反序列化

我们都打过游戏,特别是RPG游戏。一般的RPG游戏都具有以下几个特点:

  • 角色等级、各种属性、血量、蓝量、经验值
  • 怪物等级、血量、蓝量、各种属性
  • 装备等级、品质、属性

在游戏世界中,我们所见的以上特点,都可以看成是一个个对象,打游戏的时候创建了一个角色,那就是创建了一个角色对象,同样的道理,野怪和装备也都是对象。

我们打游戏要变强就要获取装备以及获取经验值让角色升级。这些东西,读计算机的我们都知道,都是一条条数据。这些数据以及对象都是存储在内存中的。而读过计算机的我们也知道,内存中的数据只是暂时存储的,一旦我们关机断电,那么这些东西就会消失。但当我们第二天或者几分钟后重新打开电脑,运行这个游戏,点击这个存档时,会发现我们的角色身上的装备以及经验条、等级,都跟关机前一模一样。

为啥子呢?

这时就有人说:数据库。

是的,但数据库的数据又是存储在哪里呢?答案就是硬盘。

硬盘存储的数据在关机后是不会丢失的,这种保存技术就叫对象持久化,也叫对象序列化。而对对象序列化逆过程就叫反序列化。

简单地说,反序列化就是将硬盘中的信息读取出来形成对象

0x02 是什么

有了上述的引子,我们很容易就能归纳出什么是序列化和反序列化了

  • 序列化就是将对象实例的状态存到存储媒体的过程
  • 反序列化就是将存储在存储媒体中的对象状态转换成对象的过程

所以,序列化和反序列化也可以这样理解

  • 序列化:把对象转换成字节序列的过程
  • 反序列化:把字节序列恢复为对象的过程

0x03 机制

序列化的最终目的就是为了对象可以跨平台存储和进行网络传输。平时我们都是通过IO进行跨平台存储和网络传输,而IO所支持的数据格式,就是字节数组。

但问题又来了,我们把对象转换成字节数组后,要怎么转回来呢?就好比密码学里面我们把明文转成密文十分简单,但是拿着密文要如何转成明文呢?

这就要求我们必须在把对象转成字节数组的时候就制定一种规则(类似密码学里面的密钥),有了这个规则,我们就可以从IO流里面读出数据的时候以这个规则把对象还原回来。

0x04 常见的规则

序列化只是定义了拆解对象的具体规则,这种规则实际上是由很多的(就跟密码学一样,有多种多样的加密方法),现在比较常见的序列化方式有:JDK原生JSONProtoBufHessianKryo

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:使用类的加载器classloader

2.创建运行时类的对象

newlnstance()调用此方法,创建对应的运行时类的对象

3.获取运行时类的结构

getFields()获取当前运行时类及其父类中声明为public访问权限的属性

getDeclaredFields()获取当前运行时类中声明的所有属性

getMethods()获取当前运行时类及其所有父类声明为public的方法

getDeclaredMethods()获取当前运行时类中声明的方法,不包含父类

getConstructors()获取当前运行时类声明为public的构造器

getDeclaredConstructors()获取当前运行时类中声明的所有构造器

invoke()方法允许调用包装在当前Method对象中的方法

当我们了解了反射机制后,就可以尝试利用invoke()来执行命令

1
2
3
4
5
6
7
8
9
10
11
public class test2 {
public static void main(String[] args) throws Exception {
//forName(类名) 获取类名对应的Class对象,同时将Class对象加载进来。
//getMethod(方法名,参数类型列表) 根据方法名称和相关参数,来定位需要查找的Method对象并返回。
//invoke(Object obj,Object...args) invoke允许调用包装在当前Method对象中的方法
Object runtime=Class.forName("java.lang.Runtime").getMethod("getRuntime",new Class[]{}).invoke(null);
//获取一个Runtime的实例对象
//调用Runtime实例对象的exec()方法,并将calc.exe作为参数传入
Class.forName("java.lang.Runtime").getMethod("exec",String.class).invoke(runtime,"calc.exe");
}
}

使用runtime调用获得的Method对象所声明的公开方法,即exec,并讲calc.exe作为参数传入,而runtime为获取的Runtime.getRuntime实例对象。因此上述代码就相当于执行了Runtime.getRuntime().exec("calc.exe"),成功通过java反射机制启动了计算器。

为什么

那么我们为什么要使用java的反射机制呢?直接创建一个对象不是更方便吗?

虽然直接创建一个对象十分方便,但如果我们有很多个类,每个用户所需求的对象不同,那么我们虽然直接创建对象是否方便,但在这种情况,需要不断地去new一个对象,十分不灵活。这时我们使用java地反射机制,这样就可以在运行时确定类型,绑定对象,动态编译最大限度发挥了java的灵活性。

java反序列漏洞

通过之前的内容,我们知道序列化是指把对象转换成字节序列的过程,反序列化则是打开字节流并重构对象。那么,如果我们对将要被进行反序列化的数据进行特殊构造,那么就能产生非预期的对象,从而导致任意代码执行。

java中间件通常会通过网络接收客户端发送的序列化数据,而在服务端对序列化数据进行反序列化时,会调用被序列化对象的readObject()方法。如果java重写了某个类的方法,就会优先调用修改后的方法。

如果能够找到满足上述条件的对象进行序列化并发送给Java中间件,Java中间件也会去执行指定的代码,即存在反序列化漏洞。