根据上篇文章可以了解到,一个类想要实现序列化和反序列化,必须要实现 java.io.Serializable 或 java.io.Externalizable 接口。
Serializable 接口是一个标记接口,标记了这个类可以被序列化和反序列化,而 Externalizable 接口在 Serializable 接口基础上,又提供了 writeExternal 和 readExternal 方法,用来序列化和反序列化一些外部元素。
其中,如果被序列化的类重写了 writeObject 和 readObject 方法,Java 将会委托使用这两个方法来进行序列化和反序列化的操作。
导致反序列化漏洞的出现:在反序列化一个类时,如果其重写了 readObject 方法,程序将会调用它,如果这个方法中存在一些恶意的调用,则会对应用程序造成危害。
所以导致反序列化漏洞的出现就是重写readObject方法。
在如下的代码中,在Person中重写了readObject方法,并在其中进行了命令执行,下面就探究一下这条命令是否能执行
Person.java
import java.io.IOException;
import java.io.Serializable;
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("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
}
}
SerializableTest.java
import java.io.*;
public class SerializableTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Person person = new Person("zyer", 22);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("1.txt"));
oos.writeObject(person);
oos.close();
FileInputStream fis = new FileInputStream("1.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
ois.readObject();
ois.close();
}
}
我们直接在ois.readObject()处下断点,
然后直接跟进readObject (command/Ctrl + 左键),发现存在readObject的使用,于是在这再下断点并运行,使用单步步入的方式(F7)
进入到该函数,先是两个if判断,根据传入的type值,我们可以知道会进入try中,其中调用了 readObject0函数,继续跟进
进入到readObject0,大致看一下代码,关键就是对byte tc进行判断,且该方法是以字节将文件读入,于是继续向下单步步入
继续向下执行会到case TC_object
上边对TC_object有定义0x73,根据上篇文章可以知道,0x73是序列化的对象标志符
然后进入readOrdinaryObject,可以看到
obj = desc.isInstantiable() ? desc.newInstance() : null;
在这条代码处就有newInstance方法,将其实例化
继续向下运行发现到这果然已经有了Person的obj对象了,正常的反序列化的话到这就应该结束了,但是因为我们的代码中有重新readObject的方法,所以我们还需要继续跟进
然后继续向下,存在desc.isExternalizable()的判断是不是Externalizable 接口,如果是,则调用 readExternalData 方法去执行反序列化类中的 readExternal,如果不是,则调用 readSerialData 方法去执行类中的 readObject 方法。
继续向下跟进,因为我们使用的是java.io.Serializable,所以进入到else语句的readserialData方法
然后进入readSerialData,在 readSerialData 方法中,首先通过类描述符获得了序列化对象的数据布局。通过布局的 hasReadObjectMethod 方法判断对象是否有重写 readObject 方法,如果有,则使用 invokeReadObject 方法调用对象中的 readObject 。
继续跟进invokeReadObject,发现使用了invoke方式将输入流作为参数,将其生成一个新对象,从而实现命令执行
参考su18师傅(Java 反序列化漏洞(一) - 前置知识 & URLDNS | 素十八)
通过对反序列化漏洞形成的原理分析,我们可以有个基本思路,就是直接找readObject的方法,再在其内部寻找一些可以利用的漏洞函数,下一篇文章就针对HashMap进行利用。