Java反序列化漏洞(一)- 前置知识

主要说一下反序列化漏洞原理、常见 gadget、常见的漏洞触发位置。

0x01 序列化与反序列化

在 Java 中的序列化与反序列化,就是将一个 Java 对象当前状态以字符串(字节序列)的形式描述出来,这串字符可能被储存/发送到任何需要的位置,在适当的时候,再将它转回原本的 Java 对象。
这中间需要一个规则,规则中描述了序列化和反序列化时究竟该如何把一个对象处理成字符串,又如何把字符串变回对象,因为这一过程必须是可逆的。

Java 提供了两个类 java.io.ObjectOutputStreamjava.io.ObjectInputStream 来实现序列化和反序列化的功能,其中 ObjectInputStream 用于恢复那些已经被序列化的对象,ObjectOutputStream 将 Java 对象的原始数据类型和图形写入 OutputStream。

在 Java 的类中,必须要实现 java.io.Serializablejava.io.Externalizable 接口才可以使用,而实际上 Externalizable 也是实现了 Serializable 接口。

ObjectOutputStream

ObjectOutputStream 继承的父类或实现的接口如下:

  • 父类 OutputStream:所有字节输出流的顶级父类,用来接收输出的字节并发送到某些接收器(sink)。
  • 接口 ObjectOutput:ObjectOutput 扩展了 DataOutput 接口,DataOutput 接口提供了将数据从任何 Java 基本类型转换为字节序列并写入二进制流的功能,ObjectOutput 在 DataOutput 接口基础上提供了 writeObject 方法,也就是类(Object)的写入。
  • 接口 ObjectStreamConstants:定义了一些在对象序列化时写入的常量。常见的一些的比如 STREAM_MAGIC、STREAM_VERSION 等。

通过这个类的父类及父接口,我们大概可以理解这个类提供的功能:能将 Java 中的类、数组、基本数据类型等对象转换为可输出的字节,也就是反序列化。接下来看一下这个类中几个关键方法。

writeObject

这是 ObjectOutputStream 对象的核心方法之一,用来将一个对象写入输出流中,任何对象,包括字符串和数组,都是用 writeObject 写入到流中的。

之前说过,序列化的过程,就是将一个对象当前的状态描述为字节序列的过程,也就是 Object -> OutputStream 的过程,这个过程由 writeObject 实现。writeObject 方法负责为指定的类编写其对象的状态,以便在后面可以使用与之对应 readObject 方法来恢复它。

writeUnshared

用于将非共享对象写入 ObjectOutputStream,并将给定的对象作为刷新对象写入流中。

使用 writeUnshared 方法会使用 BlockDataOutputStream 的新实例进行序列化操作,不会使用原来 OutputStream 的引用对象。

writeObject0

writeObjectwriteUnshared 实际上调用 writeObject0 方法,也就是说 writeObject0是上面两个方法的基础实现。具体的实现流程将会在后面再进行详细研究。

writeObjectOverride

如果 ObjectOutputStream 中的 enableOverride 属性为 true,writeObject 方法将会调用 writeObjectOverride,这个方法是由 ObjectOutputStream 的子类实现的。

在由完全重新实现 ObjectOutputStream 的子类完成序列化功能时,将会调用实现类的 writeObjectOverride 方法进行处理。

ObjectInputStream

ObjectInputStream 继承的父类或实现的接口如下:

  • 父类 InputStream:所有字节输入流的顶级父类。
  • 接口 ObjectInput:ObjectInput 扩展了 DataInput 接口,DataInput 接口提供了从二进制流读取字节并将其重新转换为 Java 基础类型的功能,ObjectInput 额外提供了 readObject 方法用来读取类。
  • 接口 ObjectStreamConstants:同上。

ObjectInputStream 实现了反序列化功能,看一下其中的关键方法。

readObject

从 ObjectInputStream 读取一个对象,将会读取对象的类、类的签名、类的非 transient 和非 static 字段的值,以及其所有父类类型。

我们可以使用 writeObjectreadObject 方法为一个类重写默认的反序列化执行方,所以其中 readObject 方法会 “传递性” 的执行,也就是说,在反序列化过程中,会调用反序列化类的 readObject 方法,以完整的重新生成这个类的对象。

readUnshared

从 ObjectInputStream 读取一个非共享对象。 此方法与 readObject 类似,不同点在于readUnshared 不允许后续的 readObjectreadUnshared 调用引用这次调用反序列化得到的对象。

readObject0

readObjectreadUnshared 实际上调用 readObject0 方法,readObject0是上面两个方法的基础实现。

readObjectOverride

由 ObjectInputStream 子类调用,与 writeObjectOverride 一致。

通过上面对 ObjectOutputStream 和 ObjectInputStream 的了解,两个类的实现几乎是一种对称的、双生的方式进行。

0x02 反序列化漏洞

在前面提到过,一个类想要实现序列化和反序列化,必须要实现 java.io.Serializablejava.io.Externalizable 接口。

Serializable 接口是一个标记接口,标记了这个类可以被序列化和反序列化,而 Externalizable 接口在 Serializable 接口基础上,又提供了 writeExternalreadExternal 方法,用来序列化和反序列化一些外部元素。

其中,如果被序列化的类重写了 writeObject 和 readObject 方法,Java 将会委托使用这两个方法来进行序列化和反序列化的操作。

正是因为这个特性,导致反序列化漏洞的出现:在反序列化一个类时,如果其重写了 readObject 方法,程序将会调用它,如果这个方法中存在一些恶意的调用,则会对应用程序造成危害。

在这里我们利用写一个简单的测试程序,如下代码创建了 Person 类,实现了 Serializable 接口,并重写了 readObject 方法,在方法中使用 Runtime 执行命令弹出计算器。

public class Person implements Serializable {

    private String name;

    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
        Runtime.getRuntime().exec("open -a Calculator.app");
    }

}

然后我们将这个类序列化并写在文件中,随后对其进行反序列化,就触发了命令执行。

public class SerializableTest {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Person person = new Person("zhangsan", 24);

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("abc.txt"));
        oos.writeObject(person);
        oos.close();


        FileInputStream   fis = new FileInputStream("abc.txt");
        ObjectInputStream ois = new ObjectInputStream(fis);
        ois.readObject();
        ois.close();
    }
}

那么为什么重写了就会执行呢?我们来看一下 java.io.ObjectInputStream#readObject() 方法的具体实现代码。

readObject 方法实际调用 readObject0 方法反序列化字符串。

readObject0 方法以字节的方式去读,如果读到 0x73,则代表这是一个对象的序列化数据,将会调用 readOrdinaryObject 方法进行处理

readOrdinaryObject 方法会调用 readClassDesc 方法读取类描述符,并根据其中的内容判断类是否实现了 Externalizable 接口,如果是,则调用 readExternalData 方法去执行反序列化类中的 readExternal,如果不是,则调用 readSerialData 方法去执行类中的 readObject 方法。

readSerialData 方法中,首先通过类描述符获得了序列化对象的数据布局。通过布局的 hasReadObjectMethod 方法判断对象是否有重写 readObject 方法,如果有,则使用 invokeReadObject 方法调用对象中的 readObject

通过上述分析,我们就了解了反序列化漏洞的触发原因。与反序列漏洞的触发方式相同,在序列化时,如果一个类重写了 writeObject 方法,并且其中产生恶意调用,则将会导致漏洞,当然在实际环境中,序列化的数据来自不可信源的情况比较少见。

那接下来该如何利用呢?我们需要找到那些类重写了 readObject 方法,并且找到相关的调用链,能够触发漏洞,接下来,我们将分析 ysoserial 中的调用链,积累一些思路。

0x03 反序列化 Gadget 分析

一个能成功执行的反序列化调用链需要三个元素:“kick-off”“sink”“chain”。翻译成中文来说就是 “入口点(重写了 readObject 的类)”“sink 点(最终执行恶意动作的点:RCE…)”“chain (中间的调用链)”

免责声明

免责声明:本博客的内容仅供合法、正当、健康的用途,切勿将其用于违反法律法规的行为。如因此导致任何法律责任或纠纷,本博客概不负责。谢谢您的理解与配合!

本文链接:

https://sanshiok.com/archive/18.html

# 最新文章