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();
		}
	}
}

四、总结

相关推荐
bcbnb4 小时前
如何解析iOS崩溃日志:从获取到符号化分析
后端
许泽宇的技术分享5 小时前
当AI学会“说人话“:Azure语音合成技术的魔法世界
后端·python·flask
用户69371750013845 小时前
4.Kotlin 流程控制:强大的 when 表达式:取代 Switch
android·后端·kotlin
用户69371750013845 小时前
5.Kotlin 流程控制:循环的艺术:for 循环与区间 (Range)
android·后端·kotlin
vx_bisheyuange5 小时前
基于SpringBoot的宠物商城网站的设计与实现
spring boot·后端·宠物
bcbnb5 小时前
全面解析网络抓包工具使用:Wireshark和TCPDUMP教程
后端
leonardee5 小时前
Spring Security安全框架原理与实战
java·后端
回家路上绕了弯5 小时前
包冲突排查指南:从发现到解决的全流程实战
分布式·后端
爱分享的鱼鱼5 小时前
部署Vue+Java Web应用到云服务器完整指南
前端·后端·全栈
麦麦麦造6 小时前
比 pip 快 100 倍!更现代的 python 包管理工具,替代 pip、venv、poetry!
后端·python