从“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本质相同,只是多了加密层和框架自动处理的环节。
