You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Effective Java Item89:readResolve序列化单例攻击问题咨询

《Effective Java》第89条问题解析:readResolve单例防护的攻击原理

问题源自《Effective Java》第89条:实例控制场景下优先选用枚举类型而非readResolve,核心疑问为为何攻击者可通过构造序列化流修改单例类的实例字段。


复现代码

存在漏洞的可序列化单例类

public class Elvis implements Serializable {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() { }
    private String[] favoriteSongs ={ "Hound Dog", "Heartbreak Hotel" };
    public void printFavorites() {
        System.out.println(Arrays.toString(favoriteSongs));
    }
    private Object readResolve() {
        return INSTANCE;
    }
}

盗用攻击类

public class ElvisStealer implements Serializable {
    static Elvis impersonator;
    private Elvis payload;

    private Object readResolve() {
        // 保存对“未完成解析”的Elvis实例的引用
        impersonator = payload;
        // 返回类型匹配favoriteSongs字段的对象
        return new String[] { "A Fool Such as I" };
    }

    private static final long serialVersionUID =0;
}

测试复现代码

public class ElvisImpersonator {
// 该字节流不可能由真实的Elvis实例序列化生成!
    private static final byte[] serializedForm = {
        (byte)0xac, (byte)0xed, 0x00, 0x05, 0x73, 0x72, 0x00, 0x05,
        0x45, 0x6c, 0x76, 0x69, 0x73, (byte)0x84, (byte)0xe6,
        (byte)0x93, 0x33, (byte)0xc3, (byte)0xf4, (byte)0x8b,
        0x32, 0x02, 0x00, 0x01, 0x4c, 0x00, 0x0d, 0x66, 0x61, 0x76,
        0x6f, 0x72, 0x69, 0x74, 0x65, 0x53, 0x6f, 0x6e, 0x67, 0x73,
        0x74, 0x00, 0x12, 0x4c, 0x6a, 0x61, 0x76, 0x61, 0x2f, 0x6c,
        0x61, 0x6e, 0x67, 0x2f, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74,
        0x3b, 0x78, 0x70, 0x73, 0x72, 0x00, 0x0c, 0x45, 0x6c, 0x76,
        0x69, 0x73, 0x53, 0x74, 0x65, 0x61, 0x6c, 0x65, 0x72, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01,
        0x4c, 0x00, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64,
        0x74, 0x00, 0x07, 0x4c, 0x45, 0x6c, 0x76, 0x69, 0x73, 0x3b,
        0x78, 0x70, 0x71, 0x00, 0x7e, 0x00, 0x02
    };

    public static void main(String[] args) {
        // 初始化ElvisStealer.impersonator,返回真实的Elvis实例(即Elvis.INSTANCE)
        Elvis elvis = (Elvis) deserialize(serializedForm);
        Elvis impersonator = ElvisStealer.impersonator;
        elvis.printFavorites();
        impersonator.printFavorites();
    }
}

程序运行输出

[Hound Dog, Heartbreak Hotel]
[A Fool Such as I]

序列化字节流结构化解析结果

序列化结构示意图


核心技术疑问

  • 为何ElvisStealer的readResolve()方法返回值可修改impersonator实例的favoriteSongs字段?
  • 为何字节序列0x71, 0x00, 0x7e, 0x00, 0x02代表Elvis实例的引用?
  • 为何上述字节序列会被赋值给ElvisStealer实例的payload字段?

问题解答

1. 为什么ElvisStealer的readResolve返回值能修改实例字段?

根本原因是Java反序列化的执行顺序存在可被利用的空窗期:反序列化时,JVM会先按照字节流的内容把目标对象的所有实例字段全部填充完毕,才会调用对象的readResolve()方法,最后用该方法的返回值替换掉刚反序列化出来的对象,赋值给上层引用。
这段恶意字节流根本不是正常序列化Elvis实例生成的:它恶意把Elvis类的favoriteSongs字段值,写成了一个嵌套的ElvisStealer对象。反序列化走到给外层Elvis的favoriteSongs字段赋值这一步时,会先反序列化这个嵌套的ElvisStealer实例:

  • 第一步先给ElvisStealer的payload字段赋值,值就是当前还没走完反序列化流程、刚创建出来的外层Elvis对象的引用
  • 第二步调用ElvisStealer的readResolve()方法:方法先把payload存到静态变量impersonator里,再返回一个内容为{"A Fool Such as I"}的String数组
  • JVM拿到这个返回值,直接把它赋值给外层Elvis的favoriteSongs字段,之后才会调用外层Elvis的readResolve方法,返回全局单例Elvis.INSTANCE,最终外层反序列化拿到的引用确实是正常的单例对象

但这时候静态变量impersonator已经牢牢持有了那个没被替换的、反序列化生成的恶意Elvis实例的引用,这个实例的favoriteSongs已经被替换成了假数组,完全脱离了单例控制,自然能打印出和单例不一样的内容。

2. 为什么字节序列0x71, 0x00, 0x7e, 0x00, 0x02代表Elvis实例引用?

这完全是Java对象序列化协议的标准规定:

  • 开头的0x71是协议定义的TC_REFERENCE标记位,作用是表示当前位置是一个引用,指向本次序列化流中之前已经解析过的对象,用来解决循环引用、重复引用的问题
  • 后面的字节是句柄值,序列化协议规定句柄基准值为0x7E0000,每解析一个新对象句柄值递增1。这里的句柄值是0x7e000002,对应流里第二个被分配句柄的对象——第一个解析的对象就是外层的Elvis实例,句柄号正好是2,所以这个引用就指向那个还没初始化完的Elvis对象。

3. 为什么这个引用会被赋值给ElvisStealer的payload字段?

完全是序列化流的字段解析顺序决定的:

  • 解析完外层Elvis的类元信息后,按类定义顺序,接下来要解析的第一个实例字段就是favoriteSongs,类型是Object,对应的值就是流里嵌套写入的ElvisStealer对象
  • 解析这个ElvisStealer对象的类元信息后,按它的类定义顺序,接下来要解析的第一个(也是唯一一个)实例字段就是payload,类型是Elvis
  • 这时候流里接下来的内容就是刚才说的TC_REFERENCE引用,JVM自然就把这个引用指向的Elvis对象赋值给payload字段了。

整个攻击的核心漏洞点就是:依赖readResolve做单例实例控制时,反序列化生成的临时对象会在字段填充阶段暴露,攻击者完全可以通过构造畸形流偷走这个临时对象的引用,甚至篡改它的字段值,单例约束直接失效。这也是为什么《Effective Java》推荐单例场景优先用枚举类型:JVM从底层保证枚举的反序列化不会生成额外的临时实例,不需要手写readResolve,从根源上堵死了这类攻击。


内容的提问来源于stack exchange,提问作者just_code_dog

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 16:57:23