Java序列化和反序列化
Java反序列化基础-01
向drun1baby大佬学习
0x01序列化和反序列化
1.什么是序列化和反序列化?
Java序列化是指把Java对象转换为字节序列的过程;
Java反序列化是指把字节序列恢复为Java对象的过程;
2.为什么要序列化?
对象不只是存储在内存中,它还需要在传输网络中进行传输,并且保存起来之后下次再加载出来,这时候就需要序列化技术。Java的序列化技术就是把对象转换成一串由二进制字节组成的数组,然后将这二进制数据保存在磁盘或传输网络。而后需要用到这对象时,磁盘或者网络接收者可以通过反序列化得到此对象,达到对象持久化的目的。
3.几种创建的序列化和反序列化协议
XML&SOAP
JSON
Protobuf
0x02ObjectOutputStream 与 ObjectInputStream类
1.ObjectOutputStream类
java.io.ObjectOutputStream 类,将Java对象的原始数据类型写出到文件,实现对象的持久存储。
序列化操作
一个对象要想序列化,必须满足两个条件:
1.该类必须实现 java.io.Serializable 接口, Serializable 是一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出 NotSerializableException 。
2.该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用transient 关键字修饰。
2.ObjectInputStream类
如果能找到一个对象的class文件,我们可以进行反序列化操作,调用 ObjectInputStream 读取对象的方法:打印结果:反序列化操作就是从二进制文件中提取对象
0x03序列化与反序列化代码实现
1.这边直接提供代码
01.类文件:Person.java
1 | package Serialize; |
02.序列化文件 SerializationTest.java
1 | package Serialize; |
03.反序列化文件 UnserializeTest.java
1 | package Serialize; |
我们可以先跑一下研究这些代码起到了什么作用
Run SerializationTest.java
Run UnserializationTest.java
序列化是为了什么?传输数据把对象转换成字符串
在SerializationTest.java中
1 | ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin")); |
这里我们将代码进行了封装,将序列化功能封装进了 serialize 这个方法里面,在序列化当中,我们通过这个 FileOutputStream
输出流对象,将序列化的对象输出到 ser.bin
当中。再调用 oos 的 writeObject
方法,将对象进行序列化操作。
在UnserializeTest.java中
1 | ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename)); |
反序列化刚刚的数据
2.Serializable 接口
01.序列化类的属性没有实现 Serializable 那么在序列化就会报错
只有实现 了Serializable 或者 Externalizable 接口的类的对象才能被序列化为字节序列。(不是则会抛出异常)。
Serializable 接口是 Java 提供的序列化接口,它是一个空接口,所以其实我们不需要实现什么。
1 | public interface Serializable { |
Serializable 用来标识当前类可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列化。如果我们此处将 Serializable 接口删除掉的话,会导致如下结果。
02.在反序列化过程中,它的父类如果没有实现序列化接口,那么将需要提供无参构造函数来重新创建对象
03.一个实现 Serializable 接口的子类也是可以被序列化的。
04.静态成员变量是不能被序列化
序列化是针对对象属性的,而静态成员变量是属于类的。
05.transient 标识的对象成员变量不参与序列化
我们可以利用上面实例代码来尝试一下将 Person.java 中的name加上transient的类型标识。加完之后再跑我们的序列化与反序列化的两个程序
Person.java
SerializationTest.java
UnserializeTest.java
最后结果变成了
1 | Person{name='null', age=22} |
0x04 为什么会产生序列化的安全问题
1.反序列化漏洞的基本原理
在Java反序列化中,会调用被反序列化的readObject方法,当readObject方法被重写不当时产生漏洞
只要服务端反序列化数据,客户端传递类的readObject中代码会自动执行,赋予攻击者在服务器上运行代码的能力。
2.可能存在安全漏洞的形式
01.入口类的 readObject直接调用危险方法
这种情况,在实际开发场景中并不特别常见,我们还是跟着代码来走一遍,写一段弹计算器的代码
1 | private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException{ |
先运行序列化程序 ———— “SerializationTest.java“,再运行反序列化程序 ———— “UnserializeTest.java“
这时候就会弹出计算器,当然也可以弹出其他启动程序,是不是帅的飞起🍔。
这是黑客最理想的情况,但是这种情况几乎不会出现。
02.入口参数中包含可控类,该类有危险方法,readObject 时调用
03.入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject时调用
04.构造函数/静态代码块等类加载时隐式执行
3. 产生漏洞的攻击路线
01.前言
首先的攻击前提:继承 Serializable
入口类:source (重写 readObject 调用常见的函数;参数类型宽泛,比如可以传入一个类作为参数;最好 jdk 自带)
找到入口类之后要找调用链 gadget chain 相同名称、相同类型
执行类 sink(RCE SSRF 写文件等等)比如exec这种函数
02.以 HashMap 为例说明,如何找到入门类
Map是一个集合,本质上还是数组,HashMap是Map的子接口。该集合的结构为key—>value,两个一起称为一个Entry(jdk7),在jdk8中底层的数组为Node[]。当new HashMap()时,在jdk7中会直接创建一个长度为16的数组;jdk8中并不直接创建,而是在调用put方法时才去创建一个长度为16的数组。
hashmap作为入口类。反序列化的入口类就是jdk中经常被使用的类,继承了Serialize接口,具备readObject方法,调用一些类和该类中的某种方法(方法中可以直接或者间接调用危险函数)。
利用过程
我们可以先在某个文件写一个HashMap
1 | //vscode中查看内置类是直接右键选择转到定义就行了 |
有人可能发现自己的这个大纲为啥不显示HashMap的结构,就像IDEA一样,其实是可以开启的
让我们继续,其实可以直接搜索readObject
往下分析
1 | for (int i = 0; i < mappings; i++) { |
Key 与 Value 的值执行了 readObject
的操作,再将 Key 和 Value 两个变量扔进 hash
这个方法里,我们再跟进(对准方法右键转到定义,或者直接F12) hash 。
若传入的参数 key 不为空,则 h = key.hashCode()
,于是乎,继续跟进 hashCode
当中。
hashCode 位置处于 Object 类当中,满足我们 调用常见的函数 这一条件。
1 | Object类是Java类层次结构的根,也是所有Java类的父类。这个类提供了一些通用方法,可以在任何Java对象中使用,这些方法可用于比较、克隆、打印对象等。 |
03.URLDNS链分析
URLDNS链是java原生态的一条利用链,通常用于存在反序列化漏洞进行验证的,因为是原生态,不存在什么版本限制。
HashMap结合URL触发DNS检查的思路。在实际过程中可以首先通过这个去判断服务器是否使用了readObject()以及能否执行。之后再用各种gadget去尝试试RCE。
我们先去到 ysoserial 的项目当中,去看看它是如何构造 URLDNS 链的。
https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/URLDNS.java
ysoserial 对 URLDNS 的利用链看着无比简单,就这么几行代码。
1 | Gadget Chain: |
开始自己复现一遍 URLDNS 的利用链。
URL 是由 HashMap 的 put方法产生的,所以我们先跟进put方法当中。put方法之后又是调用了 hash方法;hash方法则是调用了 hashcode这一函数。
我们看到这个 hashCode
函数的变量名是 key;那这个 key 是啥啊?
其实~就是我们前面分析HashMap时发现的
1 | for (int i = 0; i < mappings; i++) { |
原来是hash这一方法传进的参数!
这里如果hash()里边传入了一个对象的话就会调用这个hashCode()进行利用,那就继续找一下哪里使用了hashCode。结合URLDNS.java
我们跟进一下URL的定义
在左边大纲直接寻找hashCode方法,URL 中的hashCode被handler这一对象所调用,handler又是 URLStreamHandler的抽象类。我们再去跟进找URLStreamHandler的hashCode方法。
终于找到了,这个用于 URLDNS 的方法——getHostAddress
再跟进getHostAddress
这⾥InetAddress.getByName(host)的作⽤是根据主机名,获取其 IP 地址,在⽹络上其实就是⼀次 DNS 查询。到这⾥就不必要再跟了。
所以,⾄此,整个 URLDNS 的Gadget其实就很清晰了
1 | HashMap->readObject() |
04.复现过程
在SerializationTest.java 文件下添加如下代码
1 | HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>(); |
我们注意到在序列化的时候就已经可以收到请求了,那我们有没有办法让他序列化的时候不发送请求,反序列化再发送?
我们继续去看代码逻辑,看看能不能尝试改变掉
我们回到 URL 这个对象,回到hashCode这里。
我们发现,当 hashCode的值不等于 -1 的时候,函数就会直接return hashCode而不执行hashCode = handler.hashCode(this);。而一开始定义 HashMap 类的时候hashCode的值为 -1,便是发起了请求。
所以我们在没有反序列化的情况下,就收到了 DNS 请求,这不是我们想要的
1 | HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>(); |
反射再说篇幅就太长了
先看看POC吧
05-1URLDNS 反序列化利用链的 POC
根据我们的思路,将 Main 函数进行修改,我这里直接全部挂出来了,不然师傅们容易看错。
SerializationTest.java
1 | public static void main(String[] args) throws Exception{ |
意外情况出现
在进行操作的时候发现竟然会报错
用了IDEA还是报错什么情况???
大概意思是缺少包,去搜索了一下貌似JDK-8也就是1.8的版本是可以用的这里如果用的是VsCode但是你的环境变量设的又是JDK-17那么你可以看看这个文章,但是还是要有JDK-8
https://www.cnblogs.com/qun-/p/18098226
博主的设置是
1 | ,"java.configuration.runtimes": [ |
如果用的是IDEA的话
在这直接改版本就行了
05-2继续复现
反序列化的文件无需更改
接着我们运行序列化文件,发现序列化的时候确实收不到了
而当我们运行反序列化的文件时候,可以收到请求,这就代表着我们的 URLDNS 链构造成功了。
0x05鸣谢
https://xz.aliyun.com/t/13060?time__1311=GqmhBK4GxRhx%2FWNiQo47IL80WDkKUAeD#toc-14