通过手动模拟 readObject() 的执行过程,来简单了解反序列化的调用过程。
readObject() 的真实执行逻辑
在IDEA中静态追踪 ois.readObject(),只能看到ObjectInputStream的源码。
ObjectInputStream ois = new ObjectInputStream(inputStream);
Object obj = ois.readObject();
但是在运行时,JVM会根据字节流信息完成以下动作:
从流中读取类名
从流中读取类描述信息
分配内存,创建对象
如果该类实现了Serializable,并且定义了 private readObject()
如果该类定义了private void readObject(ObjectInputStream in),JVM会通过反射调用它
ois.readObject()在反序列化过程中,会通过反射机制,查找并调用目标类中定义的私有readObject() 方法。
下面这个Demo可以直观看到这一点。
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
public class SimpleObject implements Serializable {
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
in.defaultReadObject();
System.out.println("SimpleObject.readObject()被调用");
}
}
服务端代码也有一个readObject方法,但是永远不会被调用。
import java.io.*;
public class ServerDemo {
public static void main(String[] args) throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(new SimpleObject());
ObjectInputStream ois = new ObjectInputStream(
new ByteArrayInputStream(bos.toByteArray())
);
ois.readObject();
}
//服务端自己的readObject(永远不会被调用)
private void readObject(ObjectInputStream in) {
System.out.println("[Server] readObject被调用了?");
}
}
运行后输出:
SimpleObject.readObject()被调用
这一步已经能说明一个事实:反序列化过程中,JVM会执行被反序列化对象所属类的逻辑,而不是服务端的逻辑。
本地文件模拟真实的反序列化攻击过程
真实攻击中,攻击者并不是直接new一个对象给服务端,而是在远端构造序列化数据—>通过网络传输—>服务端从输入流中反序列化
这里用写文件+读文件来模拟。
先假设服务端classpath中已经存在一个有问题的类(现实中通常来自第三方库)。
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
public class FileGadget implements Serializable {
private String cmd;
public FileGadget(String cmd) {
this.cmd = cmd;
}
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
in.defaultReadObject();
Runtime.getRuntime().exec(cmd);
}
}
这个类本身并不恶意,但它在反序列化时执行了敏感操作。
模拟攻击者在远端构造序列化数据:
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
public class PayloadWriter {
public static void main(String[] args) throws Exception {
FileGadget gadget = new FileGadget("calc");
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("payload.bin")
);
oos.writeObject(gadget);
oos.close();
}
}
模拟服务端从网络数据(这里是文件)中读取并反序列化:
import java.io.FileInputStream;
import java.io.ObjectInputStream;
public class FileServer {
public static void main(String[] args) throws Exception {
ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("payload.bin")
);
ois.readObject();
}
}
运行 FileServer 时,命令被执行。
反序列化漏洞的本质不是执行了攻击者的代码,而是通过精心构造的对象结构和字段值,驱动服务端已有类中的隐式方法调用。