从零构建Java反序列化漏洞:手写简易版Gadget链

当你第一次听说Java反序列化漏洞时,是否对那些神奇的"Gadget链"感到困惑?为什么简单的对象反序列化就能触发命令执行?本文将带你从最基础的Java序列化机制开始,逐步构建一个能弹出计算器的简易漏洞链。不同于直接使用ysoserial工具,我们会从创造者的角度,用不到200行代码还原漏洞本质。适合已经掌握Java基础语法,想深入理解安全原理的开发者。

1. Java序列化机制基础

Java序列化就像把一个对象"拍扁"成字节流,而反序列化则是将这些字节"还原"成活的对象。这个机制在日常开发中常用于网络传输或持久化存储。让我们先看一个最简单的可序列化类:

import java.io.Serializable;

public class Person implements Serializable {
    private String name;
    
    public Person(String name) {
        this.name = name;
    }
    
    // 标准的getter/setter省略...
}

要使类可序列化,只需实现 Serializable 标记接口。序列化和反序列化的基本操作如下:

// 序列化
Person alice = new Person("Alice");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("data.bin"));
oos.writeObject(alice);
oos.close();

// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("data.bin"));
Person person = (Person) ois.readObject();
ois.close();

关键点 :当对象被反序列化时,JVM会调用该类的 readObject() 方法。如果类中没有自定义这个方法,就会使用默认实现。但如果我们重写它...

2. 危险的readObject重写

让我们修改Person类,加入自定义的 readObject 方法:

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
    ois.defaultReadObject(); // 先调用默认反序列化
    System.out.println("[!] 反序列化触发: " + this.name); 
}

现在当我们反序列化这个对象时,控制台会打印出警告信息。这本身无害,但设想如果这里的代码不是打印日志,而是执行系统命令...

3. 构造第一条Gadget链

真正的漏洞利用需要将多个类的操作"链式"组合起来。让我们创建一个包含Runtime.exec的简单链:

public class ExploitObject implements Serializable {
    private String command;
    
    public ExploitObject(String cmd) {
        this.command = cmd;
    }
    
    private void readObject(ObjectInputStream ois) throws Exception {
        ois.defaultReadObject();
        Runtime.getRuntime().exec(this.command);
    }
}

测试这个漏洞链:

// 生成恶意序列化数据
ExploitObject exploit = new ExploitObject("calc");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("exploit.bin"));
oos.writeObject(exploit);
oos.close();

// 受害者反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("exploit.bin"));
ois.readObject(); // 计算器弹出!

这已经是一个完整的漏洞利用,但现实中很少有这么直接的案例。更常见的是通过多个类的组合间接触发命令执行。

4. 多级Gadget链构造

让我们构建一个更接近真实场景的两级调用链。首先定义两个类:

public class Gadget1 implements Serializable {
    private Runnable action;
    
    public void setAction(Runnable action) {
        this.action = action;
    }
    
    private void readObject(ObjectInputStream ois) throws Exception {
        ois.defaultReadObject();
        this.action.run(); // 关键点:反序列化时自动执行
    }
}

public class Gadget2 implements Serializable, Runnable {
    private String command;
    
    public Gadget2(String cmd) {
        this.command = cmd;
    }
    
    @Override
    public void run() {
        try {
            Runtime.getRuntime().exec(this.command);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

利用链的组装方式:

Gadget2 g2 = new Gadget2("calc");
Gadget1 g1 = new Gadget1();
g1.setAction(g2);

// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("gadget.bin"));
oos.writeObject(g1);
oos.close();

// 反序列化触发
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("gadget.bin"));
ois.readObject(); // 依然弹出计算器

这种间接调用模式正是ysoserial中各种payload的核心思路。通过精心设计的对象关系,让反序列化过程像多米诺骨牌一样触发一连串操作。

5. 防御措施与最佳实践

理解了攻击原理后,我们才能更好地防御。以下是几种常见防护方案:

白名单验证

public class SafeObjectInputStream extends ObjectInputStream {
    private static final Set<String> ALLOWED_CLASSES = 
        Set.of("java.lang.String", "com.example.SafeClass");
        
    protected SafeObjectInputStream(InputStream in) throws IOException {
        super(in);
    }
    
    protected Class<?> resolveClass(ObjectStreamClass desc) 
        throws IOException, ClassNotFoundException {
            if (!ALLOWED_CLASSES.contains(desc.getName())) {
                throw new InvalidClassException("Unauthorized deserialization attempt");
            }
            return super.resolveClass(desc);
    }
}

其他防御手段

  • 使用第三方安全库如Apache Commons IO的ValidatingObjectInputStream
  • 对序列化数据添加数字签名
  • 完全禁用不受信任源的序列化功能

6. 从原理看真实漏洞

理解了基础原理后,再看Shiro等框架的反序列化漏洞就更容易理解了。以Shiro RememberMe为例:

  1. 攻击者构造恶意序列化对象
  2. 使用已知AES密钥加密后作为Cookie发送
  3. Shiro解密后自动反序列化触发漏洞

整个过程与我们手写的Demo本质相同,只是多了加密层和框架自动处理的环节。

更多推荐