Java序列化漏洞:RCE 远程执行代码-Serializable

你好,我是 shengjk1,多年大厂经验,努力构建 通俗易懂的、好玩的编程语言教程。 欢迎关注!你会有如下收益:

  1. 了解大厂经验
  2. 拥有和大厂相匹配的技术等

希望看什么,评论或者私信告诉我!

@TOC

一、背景

二、JDK 原生的序列化方式

ExternalizableSerializable 都是 Java 中用于对象序列化的机制,但它们在控制粒度、性能、使用复杂度等方面有显著区别。下面是两者的详细对比:


一、基本定义

特性 Serializable Externalizable
所在包 java.io.Serializable java.io.Externalizable
类型 标记接口(Marker Interface) (无方法) 功能性接口 (需实现 writeExternal()readExternal()
继承关系 Externalizable extends Serializable Serializable 的子接口

二、核心区别对比表

对比维度 Serializable Externalizable
序列化控制 自动序列化所有非 transient/static 字段 完全手动控制:开发者决定哪些字段写入/读取
构造函数调用 反序列化时 不调用任何构造函数(直接分配内存) 反序列化时 必须调用无参构造函数 ,再调用 readExternal()
性能 较慢(反射 + 元数据开销大) 更快(无反射,直接 I/O 操作)
序列化体积 较大(包含类元信息、字段名等) 更小(只存你需要的数据)
灵活性 低(自动处理,难定制) 高(可加密、压缩、版本兼容等)
易用性 极简(只需实现接口) 复杂(需手写逻辑,易出错)
默认行为 支持 defaultWriteObject() / defaultReadObject() 无默认行为,一切靠自己
父类字段处理 自动递归处理(只要父类也可序列化) 不会自动处理父类字段,需手动写入/读取
安全性 较低(反序列化漏洞高发) 相对可控(可校验输入、过滤恶意数据)
适用场景 快速原型、简单本地持久化 高性能系统、网络传输、自定义格式

三、代码行为对比

1. Serializable 示例(自动)

java 复制代码
class User implements Serializable {
    private String name;
    private int id;
    // 自动序列化 name 和 id
}

2. Externalizable 示例(手动)

java 复制代码
class User implements Externalizable {
    private String name;
    private int id;

    public User() {} // 必须!

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(name); // 手动写
        out.writeInt(id);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        name = in.readUTF(); // 手动读(顺序必须一致!)
        id = in.readInt();
    }
}

⚠️ 如果 User 有父类字段,Externalizable 不会自动保存/恢复它们,而 Serializable 会(前提是父类也实现了 Serializable 或有无参构造器等)。


四、反序列化过程差异

步骤 Serializable Externalizable
1. 创建对象 JVM 直接分配内存,跳过构造函数 先调用 无参构造函数 ,再调用 readExternal()
2. 恢复状态 通过反射设置字段值 通过 readExternal() 手动赋值
3. 完成 对象 ready 对象 ready

五、何时选择哪个?

Serializable 当:

  • 快速实现序列化;
  • 对象结构简单;
  • 不关心性能或体积;
  • 用于本地缓存、临时存储等。

Externalizable 当:

  • 需要极致性能(如游戏、高频交易);
  • 控制序列化格式(如兼容旧协议);
  • 需要加密/压缩序列化数据;
  • 对象包含敏感字段,不想自动暴露;
  • 希望减少网络传输体积。

六、替代方案建议(现代开发)

虽然两者都是 JDK 内置方案,但在实际生产中,更推荐使用第三方序列化框架

场景 推荐方案
Web API / 配置 JSON(Jackson / Gson)
微服务 / RPC Protobuf / Thrift
高性能 Java 应用 Kryo / FST
大数据(Spark/Flink) Kryo

这些方案通常比 Externalizable 更高效、更安全、更易维护。


总结一句话:

Serializable 是"全自动挡",Externalizable 是"手动挡"------前者省事,后者精准可控。

三、JDK 原生的序列化漏洞RCE

Java 复制代码
import java.io.*;
import java.util.Base64;

public class SerializeDemo {

	// 1. 必须实现 Serializable 接口
	static class User implements Serializable {
		// 建议显式声明 serialVersionUID(否则每次代码改动都会变,导致反序列化失败)
		private static final long serialVersionUID = 1L;

		private           String username;
		private transient String password;  // transient 字段不会被序列化(敏感信息推荐)
		private           int    age;

		public User(String username, String password, int age) {
			this.username = username;
			this.password = password;
			this.age = age;
		}


		private void writeObject(java.io.ObjectOutputStream out) throws IOException {
			out.defaultWriteObject();
			System.out.println("writeObject 被调用了!!!");

		}

//  关键是在这里,如果服务端接收一个序列化后的对象,反序列化的时候就有可能
		private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
			System.out.println("readObject 我被调用了!!!");
			in.defaultReadObject();
			// Runtime.getRuntime().exec("calc");                    // Windows
			Runtime.getRuntime().exec("open -a Calculator.app"); // macOS
			// Runtime.getRuntime().exec("gedit");                   // Linux
		}


		@Override
		public String toString() {
			return "User{username='" + username + "', password='" + password + "', age=" + age + '}';
		}
	}

	public static void main(String[] args) throws Exception {
		User user = new User("admin", "123456", 18);

		// ================ 序列化到字节数组(最常用)===============
		byte[] bytes = serialize(user);

		// 方便传输/存储,通常会转成 Base64 字符串
		String base64 = Base64.getEncoder().encodeToString(bytes);
		System.out.println("Base64 序列化结果:");
		System.out.println(base64);

		// ================ 反序列化 ================
		User deserializedUser = (User) deserialize(bytes);
		System.out.println("反序列化后对象:" + deserializedUser);
	}

	// 序列化方法(返回 byte[])
	public static byte[] serialize(Object obj) throws IOException {
		try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
			 ObjectOutputStream oos = new ObjectOutputStream(baos)) {
			System.out.println("================= 序列化开始 ================");
			oos.writeObject(obj);
			oos.flush();
			return baos.toByteArray();
		}
	}

	// 反序列化方法(这就是高危入口!)
	public static Object deserialize(byte[] data) throws IOException, ClassNotFoundException {
		try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
			 ObjectInputStream ois = new ObjectInputStream(bais)) {
			System.out.println("================= 反序列化开始 ================");
			return ois.readObject();   // ← 这里就是所有 Java 反序列化漏洞的根源
		}
	}

	// 方法1:JVM 启动参数全局过滤(推荐用于无法改代码的老项目)
// -Djdk.serialFilter=*!com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;*!java.lang.Runtime;*!java.lang.ProcessBuilder;maxdepth=20;maxrefs=10000


	// 方法3:每次创建 ObjectInputStream 时单独设置(最灵活)
	public static Object safeDeserialize(byte[] data) throws Exception {
		try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
			 ObjectInputStream ois = new ObjectInputStream(bais) {
				 @Override
				 protected Class<?> resolveClass(ObjectStreamClass desc)
						 throws IOException, ClassNotFoundException {

					 String name = desc.getName();

					 // 白名单(推荐!只允许你自己的类)
					 if (!name.startsWith("com.yourcompany.yourapp.") &&
							 !name.startsWith("[Lcom.yourcompany.yourapp.") &&  // 数组
							 !name.equals("java.lang.String") &&
							 !name.startsWith("java.util.") &&
							 !name.startsWith("java.lang.")) {
						 throw new InvalidClassException("Unauthorized class", name);
					 }

					 // 或者用黑名单(不推荐,容易被绕过)
					 if (name.contains("TemplatesImpl") ||
							 name.contains("AnnotationInvocationHandler") ||
							 name.contains("Runtime") ||
							 name.contains("ProcessBuilder")) {
						 throw new InvalidClassException("Blocked dangerous class", name);
					 }

					 return super.resolveClass(desc);
				 }
			 }) {
			return ois.readObject();
		}
	}
}

四、总结

相关推荐
..过云雨4 分钟前
17-2.【Linux系统编程】线程同步详解 - 条件变量的理解及应用
linux·c++·人工智能·后端
南山乐只35 分钟前
【Spring AI 开发指南】ChatClient 基础、原理与实战案例
人工智能·后端·spring ai
努力的小雨2 小时前
从“Agent 元年”到 AI IDE 元年——2025 我与 Vibe Coding 的那些事儿
后端·程序员
源码获取_wx:Fegn08952 小时前
基于springboot + vue小区人脸识别门禁系统
java·开发语言·vue.js·spring boot·后端·spring
wuxuanok3 小时前
Go——Swagger API文档访问500
开发语言·后端·golang
用户21411832636023 小时前
白嫖Google Antigravity!Claude Opus 4.5免费用,告别token焦虑
后端
爬山算法3 小时前
Hibernate(15)Hibernate中如何定义一个实体的主键?
java·后端·hibernate
用户26851612107565 小时前
常见的 Git 分支命名策略和实践
后端
程序员小假5 小时前
我们来说一下 MySQL 的慢查询日志
java·后端