classAnnotations

我们来彻底解释清楚 classAnnotations 是什么。

首先,请区分两个概念:

  1. Java代码中的注解(Annotation):比如 @Override, @RestController。这是你写在源代码里的。
  2. Java序列化协议中的 classAnnotations:这是序列化数据流中的一个数据块,与上面的注解没有直接关系

核心定义

classAnnotations 是 Java 对象序列化后,在二进制数据流中,紧跟在类描述符(ClassDesc)之后的一个可选数据段。它的设计目的是为了让序列化框架能够在序列化一个类时,为这个类“额外附带”一些自定义信息。

你可以把它理解成序列化数据中的一个 “自定义备注字段”

它在序列化数据流中的位置

一个完整的对象在序列化流中的结构大致如下:

1
2
3
4
5
6
7
8
9
10
TC_OBJECT (0x73)
└── TC_CLASSDESC (0x72) # 类描述符开始
├── className # 类名
├── serialVersionUID # 序列化ID
├── classDescFlags # 类描述符标志位
├── fields # 字段信息
└── classAnnotations # ⭐【这就是 classAnnotations】⭐ - 一个可选的“备注区”
└── ... # 里面可以放任何自定义的序列化数据
└── superClassDesc # 父类描述符(同样可能包含自己的classAnnotations)
└── classdata # 对象实例的实际数据值

它是如何被使用的?

它的存在,归功于 ObjectOutputStream 中的一个受保护的方法:

1
2
3
4
5
6
7
public class ObjectOutputStream extends OutputStream {
// ...
protected void annotateClass(Class<?> cl) throws IOException {
// 默认实现是空的,什么都不做。
}
// ...
}

工作机制如下:

  1. 默认情况:当你使用 ObjectOutputStream 序列化一个对象时,annotateClass 方法什么都不做,classAnnotations 区域就是空的。
  2. 扩展情况:你可以创建 ObjectOutputStream 的子类,并重写 annotateClass 方法
  3. 写入备注:在你重写的 annotateClass 方法里,你可以使用 writeObject, writeUTF 等任何方法,向序列化流中写入你想要附加的数据。
  4. 数据归宿:你写入的这些数据,正好就被放在了序列化数据流的 classAnnotations 位置

RMI 是如何利用 classAnnotations 的?

RMI 框架正是上述机制的典型使用者。

  1. 自定义流:RMI 使用 MarshalOutputStream(它是 ObjectOutputStream 的子类)。
  2. 重写方法:它重写了 annotateClass 方法。
  3. 写入 Codebase:当 RMI 客户端序列化一个服务端可能没有的类时,它会在 annotateClass 方法中,将当前的 codebase 值(即那个 URL)写入流中。

对应的反序列化端:

  1. 反序列化时,对应的 ObjectInputStream 的子类(如 MarshalInputStream)会重写 resolveClass 方法。
  2. resolveClass 中,它会去读取序列化流中 classAnnotations 区域里存放的 codebase 信息。
  3. 如果在本地找不到这个类,就利用这个 codebase 去远程加载。

一个极简的代码示例

假设我们想序列化时给每个类附带一个版本号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 自定义 ObjectOutputStream,用于写入 classAnnotations
class MyObjectOutputStream extends ObjectOutputStream {
public MyObjectOutputStream(OutputStream out) throws IOException {
super(out);
}

@Override
protected void annotateClass(Class<?> cl) throws IOException {
// 这就是在向 'classAnnotations' 区域写数据
// 我们为每个序列化的类附带一个版本字符串
writeUTF("v1.0");
}
}

// 使用这个自定义流进行序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (MyObjectOutputStream oos = new MyObjectOutputStream(baos)) {
oos.writeObject(new MyClass()); // 序列化你的对象
}
byte[] serializedData = baos.toByteArray();

现在,serializedData 这个字节数组中,MyClass 的类描述符后面,就会包含一个字符串 "v1.0",它就是 classAnnotations 的内容。

总结

  • classAnnotations 是序列化协议里的一个“字段”,不是源代码注解。
  • 它是一个预留的“扩展槽”,默认为空。
  • 通过继承和重写 annotateClass 方法,可以向这个“扩展槽”里塞入任何自定义的序列化数据(比如 RMI 塞入的 codebase URL)。
  • 攻击的根源:因为 classAnnotations 是客户端发送的序列化数据的一部分,所以攻击者可以伪造它,将 codebase 指向恶意地址,从而利用服务端信任此数据的机制实现攻击。