0x01 写在前面jdk8u20原生反序列化漏洞是一个非常经典的漏洞，也是我分析过最复杂的漏洞之一。在这个漏洞里利用了大量的底层的基础知识，同时也要求读者对反序列化的流程、序列化的数据结构有一定的了解本文结合笔者自身对该漏洞的了解，写下此文，如有描述不当或者错误之处，还望各位师傅指出0x02 jdk8u20 漏洞原理jdk8u20其实是对jdk7u21漏洞的绕过，在《JDK7u21反序列化漏洞分析笔记》 一文的最后我提到了jdk7u21的修复方式：首先来看存在漏洞的最后一个版本（611bcd930ed1）：http://hg.openjdk.java.net/jdk7u/jdk7u/jdk/file/611bcd930ed1/src/share/classes/sun/reflect/annotation/AnnotationInvocationHandler.java查看其 children 版本（0ca6cbe3f350）：http://hg.openjdk.java.net/jdk7u/jdk7u/jdk/file/0ca6cbe3f350/src/share/classes/sun/reflect/annotation/AnnotationInvocationHandler.javacompare一下：// 改之前 AnnotationType annotationType = null; try { annotationType = AnnotationType.getInstance(type); } catch(IllegalArgumentException e) { // Class is no longer an annotation type; all bets are off return; } // 改之后 AnnotationType annotationType = null; try { annotationType = AnnotationType.getInstance(type); } catch(IllegalArgumentException e) { // Class is no longer an annotation type; time to punch out throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream"); }可以发现，在第一次的修复中，官方采用的方法是网上的第二种讨论，即将以前的 return 改成了抛出异常。我们来看第一次修复后的AnnotationInvocationHandler.readObejct()方法：private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); // Check to make sure that types have not evolved incompatibly AnnotationType annotationType = null; try { annotationType = AnnotationType.getInstance(type); } catch(IllegalArgumentException e) { // 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); if (memberType != null) { // i.e. member still exists Object value = memberValue.getValue(); if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) { memberValue.setValue( new AnnotationTypeMismatchExceptionProxy( value.getClass() + "[" + value + "]").setMember( annotationType.members().get(name))); } } } }在AnnotationInvocationHandler类中，其重写了readObejct方法，那么根据 oracle 官方定义的 Java 中可序列化对象流的原则——如果一个类中定义了readObject方法，那么这个方法将会取代默认序列化机制中的方法读取对象的状态，可选的信息可依靠这些方法读取，而必选数据部分要依赖defaultReadObject方法读取；可以看到在该类内部的readObject方法第一行就调用了defaultReadObject()方法，该方法主要用来从字节流中读取对象的字段值，它可以从字节流中按照定义对象的类描述符以及定义的顺序读取字段的名称和类型信息。这些值会通过匹配当前类的字段名称来赋予，如果当前这个对象中的某个字段并没有在字节流中出现，则这些字段会使用类中定义的默认值，如果这个值出现在字节流中，但是并不属于对象，则抛弃该值。在利用defaultReadObject()还原了一部分对象的值后，最近进行AnnotationType.getInstance(type)判断，如果传入的 type 不是AnnotationType类型，那么抛出异常。也就是说，实际上在jdk7u21漏洞中，我们传入的AnnotationInvocationHandler对象在异常被抛出前，已经从序列化数据中被还原出来。换句话说就是我们把恶意的种子种到了运行对象中，但是因为出现异常导致该种子没法生长，只要我们解决了这个异常，那么就可以重新达到我们的目的。这也就是jdk8u20漏洞的原理——逃过异常抛出。那么具体该如何逃过呢？jdk8u20的作者用了一种非常牛逼的方式。再具体介绍这种方式之前，先简单介绍一些与本漏洞相关的基础知识，以便读者更明白本文的分析流程和细节。0x03 基础知识1、Try/catch块的作用写程序不可避免的出现一些错误或者未注意到的异常信息，为了能够处理这些异常信息或错误，并且让程序继续执行下去，开发者通常使用try ... catch语法。把可能发生异常的语句放在try { ... }中，然后使用catch捕获对应的Exception及其子类，这样一来，在 JVM 捕获到异常后，会从上到下匹配catch语句，匹配到某个catch后，执行catch代码块，从而达到继续执行代码的效果。如jdk7u21中利用的正是这个：try { annotationType = AnnotationType.getInstance(type); } catch(IllegalArgumentException e) { // Class is no longer an annotation type; time to punch out throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream"); }当检测的结果不是AbbitatuibType时，匹配到了IllegalArgumentException异常，然后执行了catch中的代码块。但如果try ... catch嵌套，又该如何判定呢？可以看个例子package com.panda.sec; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; public class test { static double TEST_NUMBER = 0; public static void math(int a, int b){ double c; if (a != b) { try { TEST_NUMBER = a*(a+b); c = a / b; } catch (Exception e) { System.out.println("内层出错了"); } } else { c = a * b; } } public static void urlRequest(int a, int b, String url) throws IOException { try { math(a, b); URL realUrl = new URL(url); HttpURLConnection connection = (HttpURLConnection)realUrl.openConnection(); connection.setRequestProperty("accept", "*/*"); connection.connect(); System.out.println("状态码：" + connection.getResponseCode()); } catch (Exception e) { System.out.println("外层出错了"); throw e; } System.out.println(TEST_NUMBER); } public static void main(String[] args) throws IOException { urlRequest(1,0,"https://www.cnpanda.net"); System.out.println("all end"); } }先来看看代码逻辑，首先定义了全局变量TEST_NUMBER=0，然后定义了math和urlRequest两个方法，并且在urlRequest方法里，调用了math方法，最后在main函数中执行urlRequest方法。请读者不看下文的分析，先思考当变量值为以下情况时，这段代码会输出什么？当a=1,b=0，url地址是https://www.cnpanda.net时当a=1,b=0，url地址是https://test.cnpanda.net时当a=1,b=2，url地址是https://www.cnpanda.net时当a=1,b=2，url地址是https://test.cnpanda.net时来看具体运行结果：当a=1,b=0，url地址是https://www.cnpanda.net时：这种情况下，b=0使得a/b中的分母为0，导致内层出错，因此会进入catch块并打印出内层出错了字符串，但是由于内层的catch块并没有把错误抛出，因此继续执行剩余代码逻辑，向https://www.cnpanda.net地址发起http请求，打印状态码为200，由于在math方法中 TEST_NUMBER = a*(a+b)=1*(1+0)=1，因此打印出TEST_NUMBER为1.0，最后打印all end结束代码逻辑。当a=1,b=0，url地址是https://test.cnpanda.net时：这种情况下，b=0使得a/b中的分母为0，导致内层出错，因此会进入catch块并打印出内层出错了字符串，但是由于内层的catch块并没有把错误抛出，因此继续执行剩余代码逻辑，向https://test.cnpanda.net地址发起http请求，但是由于无法解析导致出错，进入catch块，在catch块中打印外层出错了字符串，然后抛出错误，结束代码逻辑。当a=1,b=2，url地址是https://www.cnpanda.net时：这种情况下，b!=0，因此a/b会正常运算，不会进入catch块，继续执行剩余代码逻辑，向https://www.cnpanda.net地址发起http请求，打印状态码为200，由于在math方法中 TEST_NUMBER = a*(a+b)=1*(1+2)=3，因此打印出TEST_NUMBER为3，最后打印all end结束代码逻辑。当a=1,b=2，url地址是https://test.cnpanda.net时：这种情况下，b!=0，因此a/b会正常运算，不会进入catch块，继续执行剩余代码逻辑，向https://test.cnpanda.net地址发起http请求，但是由于无法解析导致出错，进入catch块，在catch块中打印外层出错了字符串，然后抛出错误，结束代码逻辑。从上面的示例可以得出一个结论，在一个存在try ... catch块的方法（有异常抛出）中去调用另一个存在try ... catch块的方法（无异常抛出），如果被调用的方法（无异常抛出）出错，那么会继续执行完调用方法的代码逻辑，但是若调用方法也出错，那么会终止代码运行的进程这是有异常抛出调用无异常抛出，那么如果是无异常抛出调用有异常抛出呢？如下代码：package com.panda.sec; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; public class test { static double TEST_NUMBER = 0; public static void math(int a, int b,String url) throws IOException { double c; try { urlRequest(url); if (a != b) { TEST_NUMBER = a*(a+b); c = a / b; } else { c = a * b; } } catch (Exception e) { System.out.println("外层出错了"); } } public static void urlRequest(String url) throws IOException { try { URL realUrl = new URL(url); HttpURLConnection connection = (HttpURLConnection)realUrl.openConnection(); connection.setRequestProperty("accept", "*/*"); connection.connect(); System.out.println("状态码：" + connection.getResponseCode()); } catch (Exception e) { System.out.println("内层出错了"); throw e; } System.out.println(TEST_NUMBER); } public static void main(String[] args) throws IOException { math(1,0,"https://test.cnpanda.net"); System.out.println("all end"); } }同上面示例一样的代码逻辑（为了方便，做了略微调整，有些代码无意义也没有删除），只是，不同的是，这里在math方法中调用了urlRequest方法。那么如下情况又会输出什么呢？同样的，请读者不看下文的分析，先思考当变量值为以下情况时，这段代码会输出什么？当a=1,b=0，url地址是https://www.cnpanda.net时当a=1,b=0，url地址是https://test.cnpanda.net时当a=1,b=2，url地址是https://www.cnpanda.net时当a=1,b=2，url地址是https://test.cnpanda.net时当a=1,b=0，url地址是https://www.cnpanda.net时这种情况下，url为https://www.cnpanda.net，因此会在内层向该地址发起http请求，并且打印状态码为200，内层执行完毕后，继续执行外层剩余代码逻辑，b=0使得a/b中的分母为0，导致外层出错，因此会进入catch块并打印出外层层出错了字符串，最后打印all end结束代码逻辑。当a=1,b=0，url地址是https://test.cnpanda.net时这种情况下，url为https://test.cnpanda.net，因此会在内层向该地址发起http请求，但是由于无法解析导致出错，进入catch块，在catch块中打印内层出错了字符串，由于内层出错，导致外层也出错，直接进入外层的catch块并打印出外层层出错了字符串，最后打印all end结束代码逻辑。当a=1,b=2，url地址是https://www.cnpanda.net时这种情况下，url为https://www.cnpanda.net，因此会在内层向该地址发起http请求，并且打印状态码为200，内层执行完毕后，继续执行外层剩余代码逻辑，b!=0使得a/b中的分母不为0，外层不会出错，因此执行完外层的逻辑，最后打印all end结束整个代码逻辑。当a=1,b=2，url地址是https://test.cnpanda.net时这种情况下，url为https://test.cnpanda.net，因此会在内层向该地址发起http请求，因此会在内层向该地址发起http请求，但是由于无法解析导致出错，进入catch块，在catch块中打印内层出错了字符串，由于内层出错，导致外层也出错，直接进入外层的catch块并打印出外层层出错了字符串，最后打印all end结束代码逻辑。从上面的示例可以得出一个结论，在一个存在try ... catch块的方法（无异常抛出）中去调用另一个存在try ... catch块的方法（有异常抛出），如果被调用的方法（有异常抛出）出错，那么会导致调用方法出错且不会继续执行完调用方法的代码逻辑，但是不会终止代码运行的进程2、序列化数据的结构序列化数据的结构可以参考：《Object Serialization Stream Protocol/对象序列化流协议》总结 https://www.cnpanda.net/talksafe/892.html或者直接阅读官方文档：https://docs.oracle.com/javase/8/docs/platform/serialization/spec/protocol.html使用SerializationDumper工具可以查看一段序列化数据的结构，如下图所示：可以看到，序列化结构的骨架是由TC_*和各种字段描述符构成，各个TC_*及描述符的意思已经在《Object Serialization Stream Protocol/对象序列化流协议》一文中介绍了，想深入阅读的读者可以去看看。3、序列化中的两个机制引用机制在序列化流程中，对象所属类、对象成员属性等数据都会被使用固定的语法写入到序列化数据，并且会被特定的方法读取；在序列化数据中，存在的对象有null、new objects、classes、arrays、strings、back references等，这些对象在序列化结构中都有对应的描述信息，并且每一个写入字节流的对象都会被赋予引用Handle，并且这个引用Handle可以反向引用该对象（使用TC_REFERENCE结构，引用前面handle的值），引用Handle会从0x7E0000开始进行顺序赋值并且自动自增，一旦字节流发生了重置则该引用Handle会重新从0x7E0000开始。成员抛弃在反序列化中，如果当前这个对象中的某个字段并没有在字节流中出现，则这些字段会使用类中定义的默认值，如果这个值出现在字节流中，但是并不属于对象，则抛弃该值，但是如果这个值是一个对象的话，那么会为这个值分配一个 Handle。4、了解jdk7u21漏洞这个是毋庸置疑要理解的，因为jdk8u20是对jdk7u21漏洞修复的绕过。可以参考我之前写的文章：JDK7u21反序列化漏洞分析笔记：https://xz.aliyun.com/t/97040x04 从一个case说起由于jdk8u20真的比较复杂，因此为了方便理解，我写了一个简单的case，用于帮助读者理解下文。假设存在两个类AnnotationInvocationHandler和BeanContextSupport，具体内容如下：AnnotationInvocationHandler.javapackage com.panda.sec; import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; public class AnnotationInvocationHandler implements Serializable { private static final long serialVersionUID = 10L; private int zero; public AnnotationInvocationHandler(int zero) { this.zero = zero; } public void exec(String cmd) throws IOException { Process shell = Runtime.getRuntime().exec(cmd); } private void readObject(ObjectInputStream input) throws Exception { input.defaultReadObject(); if(this.zero==0){ try{ double result = 1/this.zero; }catch (Exception e) { throw new Exception("Hack !!!"); } }else{ throw new Exception("your number is error!!!"); } } }BeanContextSupport.javapackage com.panda.sec; import java.io.ObjectInputStream; import java.io.Serializable; public class BeanContextSupport implements Serializable { private static final long serialVersionUID = 20L; private void readObject(ObjectInputStream input) throws Exception { input.defaultReadObject(); try { input.readObject(); } catch (Exception e) { return; } } }Question：当传入AnnotationInvocationHandler方法中的zero等于0的时候，如何能在序列化结束时调用AnnotationInvocationHandler.exec()方法达到RCE？我们首先令zero等于0，然后尝试调用AnnotationInvocationHandler.exec()方法看看：import java.io.*; public class Main { public static void payload() throws IOException, ClassNotFoundException { AnnotationInvocationHandler annotationInvocationHandler = new AnnotationInvocationHandler(0); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("payload1")); out.writeObject(annotationInvocationHandler); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("payload1")); AnnotationInvocationHandler str = (AnnotationInvocationHandler)in.readObject(); str.exec("open /System/Applications/Calculator.app"); } public static void main(String[] args) throws IOException, ClassNotFoundException { payload(); } }不出意外，由于zero的值为0，所以使得result的分母为0，导致出现异常，抛出 Exception("Hack !!!")错误。由于在代码中我们生成了序列化文件payload1，所以现在可以利用SerializationDumper工具来看看其数据结构：STREAM_MAGIC - 0xac ed STREAM_VERSION - 0x00 05 Contents TC_OBJECT - 0x73 TC_CLASSDESC - 0x72 className Length - 41 - 0x00 29 Value - com.panda.sec.AnnotationInvocationHandler - 0x636f6d2e70616e64612e7365632e416e6e6f746174696f6e496e766f636174696f6e48616e646c6572 serialVersionUID - 0x00 00 00 00 00 00 00 0a newHandle 0x00 7e 00 00 classDescFlags - 0x02 - SC_SERIALIZABLE fieldCount - 1 - 0x00 01 Fields 0: Int - I - 0x49 fieldName Length - 4 - 0x00 04 Value - zero - 0x7a65726f classAnnotations TC_ENDBLOCKDATA - 0x78 superClassDesc TC_NULL - 0x70 newHandle 0x00 7e 00 01 classdata com.panda.sec.AnnotationInvocationHandler values zero (int)0 - 0x00 00 00 00由于该数据结构比较短，可以来具体介绍一下。STREAM_MAGIC - 0xac ed是魔数，代表了序列化的格式；STREAM_VERSION - 0x00 05表示序列化的版本；Contents表示最终生成的序列的内容；TC_OBJECT - 0x73表示序列化一个新对象的开始标记；TC_CLASSDESC - 0x72表示一个新类的描述信息开始标记；className表示当前对象的类全名信息，下面紧跟着的内容也是className的描述信息；Length - 41 - 0x00 29表示当前对象的类的长度为41；Value - com.panda.sec.AnnotationInvocationHandler - 0x636f6d2e70616e64612e7365632e416e6e6f746174696f6e496e766f636174696f6e48616e646c6572表示当前对象的类的名称为com.panda.sec.AnnotationInvocationHandler，后面的字符串是其十六进制表示；serialVersionUID - 0x00 00 00 00 00 00 00 0a定义了serialVersionUID的值为20；newHandle 0x00 7e 00 00 表示为对象分配一个值为007e0000的handle（因为引用Handle会从0x7E0000开始进行顺序赋值并且自动自增），值得注意的是这里的handle实际上没有被真正的写入文件，如果我们把这里的007e0000加入到序列化数据中，会发生异常，从而终止反序列化进程，之所以会在这里显示出来，是因为serializationDumper的作者为了方便使用者分析序列化数据的结构；classDescFlags - 0x02 - SC_SERIALIZABLE表示类描述信息标记为SC_SERIALIZABLE，代表在序列化的时候使用的是java.io.Serializable（如果使用的是java.io.Externalizable，这里的标记就会变成classDescFlags - 0x04 - SC_EXTERNALIZABLE）； fieldCount - 1 - 0x00 01表示成员属性的数量为1，值得注意的是这里的fieldCount同样是serializationDumper的作者为了方便使用者分析序列化数据的结构而新设置的描述符，在官方序列化规范中是没有fieldCount的；Fields表示接下来的内容是类中所有字段的描述信息，Fields成员属性保存了当前分析的类对应的所有成员属性的元数据信息，它是一个数组结构，每一个元素都对应了成员属性的元数据描述信息，且不会重复； 0表示接下来的内容是第一个字段的描述信息； Int - I - 0x49表示该字段的类型是int型； fieldName表示当前字段的字段名信息，下面紧跟着的内容也是 fieldName的描述信息；Length - 4 - 0x00 04表示当前字段名的长度为4；Value - zero - 0x7a65726f表示当前字段名为zero；classAnnotations表示和类相关的Annotation的描述信息，这里的数据值一般是由ObjectOutputStream的annotateClass()方法写入的，但由于annotateClass()方法默认为空，所以classAnnotations后一般会设置TC_ENDBLOCKDATA标识；（关于annotateClass具体可以看我写的序列化流程分析总结一文） TC_ENDBLOCKDATA - 0x78数据块的结束标记，表示这个对象类型的描述符已经结束了；superClassDesc表示父类的描述符信息，这里为空；TC_NULL - 0x70表示当前对象是一个空引用；newHandle 0x00 7e 00 01表示为对象分配一个值为007e0001的handle，同上面的newHandle一样，这里的handle实际上没有被真正的写入文件；classdata表示下面紧跟着的是类数据中的所有内容；` com.panda.sec.AnnotationInvocationHandler values zero (int)0 - 0x00 00 00 00`表示类数据中的所有内容 以上就是所有的序列化数据的结构，当进行反序列化的时候，会依次从上到下读取序列化内容进行还原数据。现在思考一个问题：如果在上面的序列化数据中插入一部分源代码中没有的数据，那么在反序列化的时候会发生什么？在解决这个问题前，首先再来深入理解一下我们之前提到的引用机制，举个例子比如以下代码进行一次序列化的序列化数据结构：package com.panda.sec; import java.io.*; public class test implements Serializable { private static final long serialVersionUID = 100L; public static int num = 0; private void readObject(ObjectInputStream input) throws Exception { input.defaultReadObject(); System.out.println("hello!"); } public static void main(String[] args) throws IOException { test t = new test(); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("testcase")); out.writeObject(t); out.close(); } }如果上述代码进行两次序列化，那么这个数据结构会变成什么？可以来看看：package com.panda.sec; import java.io.*; public class test implements Serializable { private static final long serialVersionUID = 100L; public static int num = 0; private void readObject(ObjectInputStream input) throws Exception { input.defaultReadObject(); System.out.println("hello!"); } public static void main(String[] args) throws IOException { test t = new test(); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("testcase")); out.writeObject(t); out.writeObject(t); //二次序列化 out.close(); } }对比一下可以发现，在该序列化数据的结构中最后多了TC_REFERENCE - 0x71 Handle - 8257537 - 0x00 7e 00 01这里对应的就是前文基础知识里“序列化中的两个机制”中引用机制里的一段话每一个写入字节流的对象都会被赋予引用Handle，并且这个引用Handle可以反向引用该对象（使用TC_REFERENCE结构，引用前面handle的值），引用Handle会从0x7E0000开始进行顺序赋值并且自动自增，一旦字节流发生了重置则该引用Handle会重新从0x7E0000开始。那么反序列化是如何处理TC_REFERENCE块的呢？我在反序列化流程分析总结 一文中写到这样一个流程：在readObject0方法里有这样的一个判断：是的，在反序列化的流程中，进入了readObject0方法后，会判断读取的字节流中是否有TC_REFERENCE标识，如果有，那么会调用readHandle函数，但是我没有在文中具体说明readHandle函数，可以一起来看看： 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) { // REMIND: what type of exception to throw here? throw new InvalidObjectException( "cannot read back reference as unshared"); } Object obj = handles.lookupObject(passHandle); if (obj == unsharedMarker) { // REMIND: what type of exception to throw here? throw new InvalidObjectException( "cannot read back reference to unshared object"); } return obj; }这个方法会从字节流中读取TC_REFERENCE标记段，它会把读取的引用Handle赋值给passHandle变量，然后传入lookupObject()，在lookupObject()方法中，如果引用的handle不为空、没有关联的ClassNotFoundException（status[handle] != STATUS_EXCEPTION），那么就返回给定handle的引用对象，最后由readHandle方法返回给对象。也就是说，反序列化流程还原到TC_REFERENCE的时候，会尝试还原引用的handle对象。谈完了引用机制现在在来看数据插入的问题，如何能在类AnnotationInvocationHandler的序列化数据中插入一部分源代码中没有的数据？利用objectAnnotation！继续来看个例子：package com.panda.sec; import java.io.*; public class test implements Serializable { private static final long serialVersionUID = 100L; public static int num = 0; private void readObject(ObjectInputStream input) throws Exception { input.defaultReadObject(); System.out.println("hello!"); } private void writeObject(ObjectOutputStream output) throws IOException { output.defaultWriteObject(); output.writeObject("Panda"); output.writeUTF("This is a test data!"); } public static void main(String[] args) throws IOException, ClassNotFoundException { test t = new test(); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("testcase_new")); out.writeObject(t); out.writeObject(t); out.close(); } }在这个示例中，我们重写了writeObject方法，并且在该方法中利用writeObject和writeUTF方法写入了Panda对象以及This is a test data!字符串，该段序列化数据内容如下：为了更直白的看变化，我们可以用compare工具来对比一下：可以看到原先表示类描述信息标记由0x02 - SC_SERIALIZABLE变成了0x03 - SC_WRITE_METHOD | SC_SERIALIZABLE，并且在原有序列化数据结构的最下方还多了由objectAnnotation标识的内容段，这里的内容段会在反序列化的时候被还原为什么会有这种变化？知识点1：如果一个可序列化的类重写了writeObject方法，而且向字节流写入了一些额外的数据，那么会设置SC_WRITE_METHOD标识，这种情况下，一般使用结束符TC_ENDBLOCKDATA来标记这个对象的数据结束；知识点2：如果一个可序列化的类重写了writeObject方法，在该序列化数据的classdata部分，还会多出个objectAnnotation部分，并且如果重写的writeObject()方法内除了调用defaultWriteObject()方法写对象字段数据，还向字节流中写入了自定义数据，那么在objectAnnotation部分会有写入自定义数据对应的结构和值；这样一来是不是就有点明了了？正常情况下，我们没有办法修改可序列化类本身的内容，也就没办法重写这个类中的writeObject方法，也就没法让序列化数据中多出来objectAnnotation内容段可真的没办法吗？当然不是了！序列化数据只是一块二进制的数据而已，只要按照序列化预定的规则来修改其hex数据，那么实际上就是相当于在重写的writeObject方法中添加数据在写入数据前，我们要考虑一件事，谁向谁写入数据？是先序列化AnnotationInvocationHandler类然后向其中插入BeanContextSupport对象，还是先序列化BeanContextSupport类然后向其中插入AnnotationInvocationHandler对象？先思考jdk7u21被修复的原因是什么？是因为在反序列化的过程中有异常抛出，从而导致反序列化的进程被终止了！这让我们不得不联想到我们在基础知识的Try/catch块的作用中做的结论：在一个存在try ... catch块的方法（无异常抛出）中去调用另一个存在try ... catch块的方法（有异常抛出），如果被调用的方法（有异常抛出）出错，那么会导致调用方法出错且不会继续执行完调用方法的代码逻辑，但是不会终止代码运行的进程我们要的就是不要终止我们的反序列化进程，这样我们就可以取得反序列化后的类对象。所以我们需要先序列化BeanContextSupport类（无异常抛出）然后向其中插入AnnotationInvocationHandler对象（有异常抛出）这里还有一点要注意，因为根据成员抛弃机制我们知道，如果序列化流新增的这个值是一个对象的话，那么会为这个值分配一个 Handle，但由于我们是手动插入Handle，所以需要修改引用Handle的值（就是TC_ENDBLOCKDATA块中handle的引用值）为AnnotationInvocationHandler对象的handle地址具体过程如下：第一步，先序列化BeanContextSupport类，然后利用SerializationDumper工具可以得到以下数据结构：STREAM_MAGIC - 0xac ed STREAM_VERSION - 0x00 05 Contents TC_OBJECT - 0x73 TC_CLASSDESC - 0x72 className Length - 32 - 0x00 20 Value - com.panda.sec.BeanContextSupport - 0x636f6d2e70616e64612e7365632e4265616e436f6e74657874537570706f7274 serialVersionUID - 0x00 00 00 00 00 00 00 14 newHandle 0x00 7e 00 00 classDescFlags - 0x02 - SC_SERIALIZABLE fieldCount - 0 - 0x00 00 classAnnotations TC_ENDBLOCKDATA - 0x78 superClassDesc TC_NULL - 0x70 newHandle 0x00 7e 00 01 classdata com.panda.sec.BeanContextSupport values第二步，序列化AnnotationInvocationHandler类，然后利用SerializationDumper工具可以得到以下数据结构： STREAM_MAGIC - 0xac ed STREAM_VERSION - 0x00 05 Contents TC_OBJECT - 0x73 TC_CLASSDESC - 0x72 className Length - 41 - 0x00 29 Value - com.panda.sec.AnnotationInvocationHandler - 0x636f6d2e70616e64612e7365632e416e6e6f746174696f6e496e766f636174696f6e48616e646c6572 serialVersionUID - 0x00 00 00 00 00 00 00 0a newHandle 0x00 7e 00 00 classDescFlags - 0x02 - SC_SERIALIZABLE fieldCount - 1 - 0x00 01 Fields 0: Int - I - 0x49 fieldName Length - 4 - 0x00 04 Value - zero - 0x7a65726f classAnnotations TC_ENDBLOCKDATA - 0x78 superClassDesc TC_NULL - 0x70 newHandle 0x00 7e 00 01 classdata com.panda.sec.AnnotationInvocationHandler values zero (int)0 - 0x00 00 00 00第三步，利用objectAnnotation插入AnnotationInvocationHandler对象：STREAM_MAGIC - 0xac ed STREAM_VERSION - 0x00 05 Contents TC_OBJECT - 0x73 TC_CLASSDESC - 0x72 className Length - 32 - 0x00 20 Value - com.panda.sec.BeanContextSupport - 0x636f6d2e70616e64612e7365632e4265616e436f6e74657874537570706f7274 serialVersionUID - 0x00 00 00 00 00 00 00 14 newHandle 0x00 7e 00 00 classDescFlags - 0x02 - SC_SERIALIZABLE fieldCount - 0 - 0x00 00 classAnnotations TC_ENDBLOCKDATA - 0x78 superClassDesc TC_NULL - 0x70 newHandle 0x00 7e 00 01 classdata com.panda.sec.BeanContextSupport values objectAnnotation // 从这里开始 TC_OBJECT - 0x73 TC_CLASSDESC - 0x72 className Length - 41 - 0x00 29 Value - com.panda.sec.AnnotationInvocationHandler - 0x636f6d2e70616e64612e7365632e416e6e6f746174696f6e496e766f636174696f6e48616e646c6572 serialVersionUID - 0x00 00 00 00 00 00 00 0a newHandle 0x00 7e 00 00 classDescFlags - 0x02 - SC_SERIALIZABLE fieldCount - 1 - 0x00 01 Fields 0: Int - I - 0x49 fieldName Length - 4 - 0x00 04 Value - zero - 0x7a65726f classAnnotations TC_ENDBLOCKDATA - 0x78 superClassDesc TC_NULL - 0x70 newHandle 0x00 7e 00 01 classdata com.panda.sec.AnnotationInvocationHandler values zero (int)0 - 0x00 00 00 00 TC_ENDBLOCKDATA - 0x78 TC_REFERENCE - 0x71 Handle - 8257539 - 0x00 7e 00 03第四步，修改handle值以及对应的classDescFlags值：STREAM_MAGIC - 0xac ed STREAM_VERSION - 0x00 05 Contents TC_OBJECT - 0x73 TC_CLASSDESC - 0x72 className Length - 32 - 0x00 20 Value - com.panda.sec.BeanContextSupport - 0x636f6d2e70616e64612e7365632e4265616e436f6e74657874537570706f7274 serialVersionUID - 0x00 00 00 00 00 00 00 14 newHandle 0x00 7e 00 00 classDescFlags - 0x03 - SC_WRITE_METHOD | SC_SERIALIZABLE fieldCount - 0 - 0x00 00 classAnnotations TC_ENDBLOCKDATA - 0x78 superClassDesc TC_NULL - 0x70 newHandle 0x00 7e 00 01 classdata com.panda.sec.BeanContextSupport values objectAnnotation // 从这里开始 TC_OBJECT - 0x73 TC_CLASSDESC - 0x72 className Length - 41 - 0x00 29 Value - com.panda.sec.AnnotationInvocationHandler - 0x636f6d2e70616e64612e7365632e416e6e6f746174696f6e496e766f636174696f6e48616e646c6572 serialVersionUID - 0x00 00 00 00 00 00 00 0a newHandle 0x00 7e 00 02 classDescFlags - 0x02 - SC_SERIALIZABLE fieldCount - 1 - 0x00 01 Fields 0: Int - I - 0x49 fieldName Length - 4 - 0x00 04 Value - zero - 0x7a65726f classAnnotations TC_ENDBLOCKDATA - 0x78 superClassDesc TC_NULL - 0x70 newHandle 0x00 7e 00 03 classdata com.panda.sec.AnnotationInvocationHandler values zero (int)0 - 0x00 00 00 00 TC_ENDBLOCKDATA - 0x78 TC_REFERENCE - 0x71 Handle - 8257539 - 0x00 7e 00 03注：这里最后的Handle - 8257539 - 0x00 7e 00 03中的8257539是serializationDumper中生成的数值，具体在序列化或反序列化流程中的体现，没有具体深究，该值不影响我们最终序列化数据的生成，该值的生成算法如下： public static void num(){ byte b1 = 0 ; byte b2 = 126; byte b3 = 0; byte b4 = 3; int handle = ( ((b1 << 24) & 0xff000000) + ((b2 << 16) & 0xff0000) + ((b3 << 8) & 0xff00) + ((b4 ) & 0xff) ); System.out.println("Handle - " + handle + " - 0x" + byteToHex(b1) + " " + byteToHex(b2) + " " + byteToHex(b3) + " " + byteToHex(b4)); }其中，b1 b2 b3 b4组合是 00 7e 00 xx，即代表了引用handle的值，然后将这些值经过运算就可以得到最终的8257539值。然后根据这段数据结构，转换成十六进制数据如下：ac ed 00 05 73 72 00 20 636f6d2e70616e64612e7365632e4265616e436f6e74657874537570706f7274 00 00 00 00 00 00 00 14 03 00 00 78 70 73 72 00 29 636f6d2e70616e64612e7365632e416e6e6f746174696f6e496e766f636174696f6e48616e646c6572 00 00 00 00 00 00 00 0a 02 00 01 49 00 04 7a65726f 78 70 00 00 00 00 78 71 00 7e 00 03这里需要注意的是，我们在前文提到了newhandle实际上没有被真正的写入文件，如果我们把这里的007e0000加入到序列化数据中，会发生异常，从而终止反序列化进程，之所以会在这里显示出来，是因为serializationDumper的作者为了方便使用者分析序列化数据的结构；所以我们在构建十六进制数据的过程中要丢弃掉newhandle对应的十六进制数据最后以4个字符为一组，8个组为一行，整理可得：aced 0005 7372 0020 636f 6d2e 7061 6e64 612e 7365 632e 4265 616e 436f 6e74 6578 7453 7570 706f 7274 0000 0000 0000 0014 0300 0078 7073 7200 2963 6f6d 2e70 616e 6461 2e73 6563 2e41 6e6e 6f74 6174 696f 6e49 6e76 6f63 6174 696f 6e48 616e 646c 6572 0000 0000 0000 000a 0200 0149 0004 7a65 726f 7870 0000 0000 7871 007e 0003将这些内容替换掉在从一个case说起最开始部分生成的payload1里的内容然后尝试再次运行以下代码：package com.panda.sec; import java.io.*; public class Main { public static void payload() throws IOException, ClassNotFoundException { // AnnotationInvocationHandler annotationInvocationHandler = new AnnotationInvocationHandler(0); // // ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("payload1")); // out.writeObject(annotationInvocationHandler); // out.writeObject(annotationInvocationHandler); // out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("payload1")); System.out.println(in.readObject().toString()); AnnotationInvocationHandler str = (AnnotationInvocationHandler)in.readObject(); System.out.println(str.toString()); str.exec("open /System/Applications/Calculator.app"); } public static void main(String[] args) throws IOException, ClassNotFoundException { payload(); } }结果如下：成功实现了RCE，并且可以看到，我们第一次反序列还原的对象是com.panda.sec.BeanContextSupport，第二次反序列化还原的对象是com.panda.sec.AnnotationInvocationHandler，也正对应了我们自己手动插入数据的顺序0x05 jdk8u20 漏洞分析我们在jdk8u20漏洞原理部分提到逃过异常抛出是该漏洞的关键所在，又经过上面一个case的分析，现在再来看看如何逃过异常抛出呢？没错，就是在jdk源码中找到一个类似于该case中的BeanContextSupport类，让BeanContextSupport成为外层，去调用jdk源码中的AnnotationInvocationHandler类，这样一来没有异常抛出就能够使反序列化流程不被终止，成功组成新的gadget链，完成一次完美的反序列化漏洞攻击。那么在jdk源码中到底有没有一个类似于该case中的BeanContextSupport类？答案是显而易见的，其实为了方便读者理解此处的内容，我在case中就把这个类的名称给了出来——是的，就是 java.beans.beancontext.BeanContextSuppor类，我们利用的是该类中的readChildren方法，来具体看看： public final void readChildren(ObjectInputStream ois) throws IOException, ClassNotFoundException { int count = serializable; while (count-- > 0) { Object child = null; BeanContextSupport.BCSChild bscc = null; try { child = ois.readObject(); bscc = (BeanContextSupport.BCSChild)ois.readObject(); } catch (IOException ioe) { continue; } catch (ClassNotFoundException cnfe) { continue; } synchronized(child) { BeanContextChild bcc = null; try { bcc = (BeanContextChild)child; } catch (ClassCastException cce) { // do nothing; } if (bcc != null) { try { bcc.setBeanContext(getBeanContextPeer()); bcc.addPropertyChangeListener("beanContext", childPCL); bcc.addVetoableChangeListener("beanContext", childVCL); } catch (PropertyVetoException pve) { continue; } } childDeserializedHook(child, bscc); } } } 可以看到，在该方法的第7行，对传入进来的ObjectInputStream对象调用了readObject方法进行反序列化处理，并且当在反序列化过程中如果出现异常，采用的是continue处理。完美的符合我们的要求。我们在上文中提到ObjectAnnotation这个概念，并且其实可以发现，如果存在ObjectAnnotation结构，那么一般是由TC_ENDBLOCKDATA - 0x78去标记结尾的，但是这里其实存在一个问题，我们知道在jdk7u21修复中是因为IllegalArgumentException异常被捕获后抛出了java.io.InvalidObjectException，虽然这里我们可以利用BeanContextSupport来强制序列化流程继续下去，但是抛出的异常会导致BeanContextSupport的ObjectAnnotation中TC_ENDBLOCKDATA - 0x78结尾标志无法被正常处理，如果我们不手动删除这个TC_ENDBLOCKDATA - 0x78那么会导致后面的结构归在ObjectAnnotation结构中，从而读取错误，反序列化出来的数据不是我们预期数据。所以我们在生成BeanContextSupport的ObjectAnnotation中不能按照正规的序列化结构，需要将标记结尾的结构TC_ENDBLOCKDATA - 0x78删除也正由于我们把TC_ENDBLOCKDATA - 0x78删除了，会导致我们在使用SerializationDumper工具查看jdk8u20的序列化数据结构出错，如下图所示：这里还有一个tips点，就是我们在插入BeanContextSupport对象的时候并不是像case中那样直接插入，而是借用假属性的概念插入。在成员抛弃中我们提到在反序列化中，如果当前这个对象中的某个字段并没有在字节流中出现，则这些字段会使用类中定义的默认值，如果这个值出现在字节流中，但是并不属于对象，则抛弃该值，但是如果这个值是一个对象的话，那么会为这个值分配一个 Handle。所以我们插入一个任意类型为BeanContextSupport的字段就可以在不影响原有的序列化流程的情况下，形成一个gadget链这里可能有点难以理解，多说一点我们知道一般gadget链是一链接着一链紧紧相连，通过写各种类之间的调用，就能够满足整个gadget链的要求，实现整个gadget链的相连。但在jdk8u20中，并非如此，因为LinkedHashSet没法在满足绕过异常抛出的条件下直接调用BeanContextSupport方法，但是BeanContextSupport可以调用AnnotationInvocationHandler方法，这也就导致我们的gadget链在LinkedHashSet下一步断了，那怎么办？只能通过修改序列化数据结构的方式，在LinkedHashSet中强行插入一个BeanContextSupport类型的字段值，由于在java反序列化的流程中，一般都是首先还原对象中字段的值，然后才会还原objectAnnotation结构中的值（即是按照序列化数据结构的顺序），所以它会首先反序列化LinkedHashSet，然后反序列LinkedHashSet字段的值，由于在这个字段值中有一个BeanContextSupport类型的字段，所以反序列化会去还原BeanContextSupport对象，也就是objectAnnotation中的数据在反序列化BeanContextSupport的过程中，会首先反序列化BeanContextSupport的字段值，其中有个值为 Templates.class 的 AnnotationInvocationHandler 类的对象的字段，然后反序列化会去还原AnnotationInvocationHandler对象，成功的关联了下一个链！最后就是同 Jdk7u21 一样的流程，利用动态代理触发Proxy.equals(EvilTemplates.class)，达到恶意类注入实现RCE的最终目的。目前jdk8u20反序列化漏洞 payload 的写法有以下几种方式：原生的，通过数组手动去构建：https://github.com/pwntester/JRE8u20_RCE_Gadget/通过字节码写入的方式构建：https://mp.weixin.qq.com/s/3bJ668GVb39nT0NDVD-3IA、https://xz.aliyun.com/t/8277#toc-5通过python javaSerializationTools模块构建：https://mp.weixin.qq.com/s/SMq6aE5-qV9cINv1-74RgA纵观以上的方法各有各的优劣，有的容易理解，但是构建起来却很麻烦，有的不容易理解，但是构建起来较为方便，其实还有如果读者全文看下来，还有一种更容易理解的方法，就是先把jdk7u21漏洞利用的payload的序列化数据结构生成出来，然后向该数据结构中用类似case中的方法，去手动插入对象，但是这个工作量比较大，故我也没有手动去实现，有兴趣、有时间的朋友可以去尝试利用该方法生成payload（包你酸爽～但是对你理解jdk8u21来说，这种方式是最直白的方式）另外，jdk8u20特殊的一点在于，实际上BeanContextSupport不能算是真正意义上的一条链，链还是jdk7u21中的那条链，只不过BeanContextSupport是用来避免抛出异常用到的媒介。0x06 总结本文对jdk8u20原生反序列化漏洞进行了分析，但和其他分析文章不同的是，本文没有按照常规的分析方法进行分析，而是重点写了一个case，用一个最简单的case去了解jdk8u20最核心的问题点，然后从整体上阐述了jdk8u20反序列化漏洞是怎么一回事，流程上是什么样的站在读者的角度上去考虑，让自己如何用更直白的方式让别人理解你发的内容，我觉得这样的方式可以让我更能理解我所分析的漏洞、记忆我所写的内容，毕竟，每一个分析文章其实对于我来说都是一次整体上的总结0x07 参考https://github.com/pwntester/JRE8u20_RCE_Gadgethttp://wouter.coekaerts.be/2015/annotationinvocationhandlerhttps://paper.seebug.org/456/https://github.com/potats0/javaSerializationToolshttps://mp.weixin.qq.com/s/SMq6aE5-qV9cINv1-74RgAhttps://xz.aliyun.com/t/8277https://mp.weixin.qq.com/s/3bJ668GVb39nT0NDVD-3IA
《Object Serialization Stream Protocol/对象序列化流协议》总结
目录目录0、写在前面1、概述2、stream元素3、Stream 协议版本4、Stream 格式的语法a. 语法规则b.终端符号和常数5、参考0、写在前面本文主要是《Object Serialization Stream Protocol》一文的翻译，然后对序列化格式进行了一些总结1、概述​stream 格式满足以下设计目标：结构紧凑，便于高效阅读；允许仅使用流的结构和格式而不需要深入了解 stream，这种情况不需要调用调用任何类的代码；只允许 stream 对数据进行访问和操作；2、stream元素​表示 stream 中的对象需要的基本结构。对象的每个属性都需要表示：它的类和类中的字段，这些数据会被写入而且之后会被类中特定的方法读取。stream 中对象的表示可以用语法来描述。null objects, new objects, classes, arrays, strings以及对stream 中已有的任何对象的反向引用，都有特殊的表示。写入 stream 的每个对象都分配了一个Handle，用于引用回该对象。从0x7E0000开始按顺序分配Handle。当 stream 被重置时，句柄在0x7E0000处重新启动。类对象由以下对象表示：它的ObjectStreamClass对象​ 非动态代理类的ObjectStreamClass对象由以下表达式表示：可兼容类的SUID一组指示类的各种属性的标志，例如该类是否定义了 writeObject 方法，以及该类是否可序列化、是否是可外部或者是否是枚举类型可序列化字段的数量默认情况下，对于类的字段数组和对象字段来说，字段的类型要作为字符串被包含，并且必须按照 Java 虚拟机规范中的规定，采用“字段描述符”格式（例如，Ljava/lang/Object;）由 annotateClass 方法写入的可选块数据(Data-Block)记录或对象其超类型的 ObjectStreamClass（如果超类不可序列化，则为 null）动态代理类的ObjectStreamClass对象由以下表达式表示：动态代理类实现的接口数动态代理类实现的所有接口的名称，这些接口通过调用Class的getInterfaces方法的返回结果进行排序列出；可选的块数据(Data-Block)记录或由AnnotationProxyClass方法编写的对象超类对应的java.lang.reflect.Proxy的ObjectStreamClass​ 字符串对象的表示由长度信息和MUTF-8编码的字符串内容组成。MUTF-8编码与Java虚拟机和Java.io.DataInput以及DataOutput接口中使用的编码相同；它在表示补充字符和空字符方面与标准UTF-8不同。长度信息的形式取决于MUTF-8编码中字符串的长度。如果给定字符串的MUTF-8编码长度小于65536字节，则该长度将写入表示无符号16位整数的2字节。从Java2平台标准版v1.3开始，如果MUTF-8编码中的字符串长度为65536字节或更多，那么该长度将以表示有符号64位整数的8字节表示。序列化 stream 中字符串前面的类型码用于表明写入字符串的格式。数组由以下内容表示：他们的ObjectStreamClass对象元素的数量。值的顺序。值的类型在数组的类型中是隐式的。例如，字节数组的值是byte类型的。枚举常量由以下表达式表示：常量的基本枚举类型的 ObjectStreamClass 对象常量的名称字符串stream 中的新对象（New objects）由以下表示：所有对象类的派生类信息；对象的每一个可序列化类的数据，从它的顶级父类开始写入。针对 Stream 中的每一个类的信息包含以下内容： -- 一个类中的可序列化字段信息；-- 如果这个类包含writeObject/readObject方法，有可能出现通过writeObject方法写入的可选对象或者基础类型的数据块(Data-Block)记录，跟着使用endDataBlock的代码；​ 类写入的所有原始数据都被缓冲并包装在数据块记录中，无论数据是在 writeObject 方法内写入 stream 还是从 writeObject 方法外部直接写入 Stream。 此数据只能通过相应的 readObject 方法读取或直接从 stream 中读取。writeObject方法写入的对象终止任何以前的数据块记录，并视情况作为常规对象或空引用或反向引用写入。数据块记录允许错误恢复丢弃任何可选数据。当从类内调用时，stream 可以丢弃任何数据或对象，直到endBlockData。3、Stream 协议版本​ 有必要对JDK1.2中的序列化 Stream 格式进行更改，该格式与JDK1.1的所有次要版本都不向后兼容。为了提供需要向后兼容的情况，Oracle 添加了一个功能，这个功能用来指示在编写序列化流时要使用哪个协议版本。ObjectOutputStream中的useProtocolVersion方法会接收一个参数以表示写入的可序列化字节流的协议版本。流协议版本如下：ObjectStreamConstants.PROTOCOL_VERSION_1:表示初始流格式。ObjectStreamConstants.PROTOCOL_VERSION_2：表示新的外部数据格式。基本数据以块数据模式写入，并以TC_ENDBLOCKDATA终止。数据块边界已标准化。以数据块模式写入的基元数据被规范化为不超过1024字节块。此更改的好处是加强了 Stream 中序列化数据格式的规范。这种变化是完全前后兼容的。JDK1.2默认为写入PROTOCOL_VERSION_2JDK1.1默认为写入PROTOCOL_VERSION_1JDK1.1.7及更高版本可以读取这两个版本JDK1.1.7之前的版本只能读取PROTOCOL_VERSION_14、Stream 格式的语法下表包含流格式的语法。非终结符号以斜体显示。终结符号拥有固定的宽度。非终结符号的定义之后带了一个：。这个定义之后每一行会有一个或者多个替代符号。下表描述了符号：NotationMeaning(datatype)此令牌具有指定的数据类型，例如 bytetoken[n]令牌的预定义出现次数，即数组x0001用十六进制表示的文字值，十六进制数字的数量反映了值的大小<xxx>从 Stream 中读取的值，用于指示数组的长度。注意，符号（utf）用于指定使用2字节长度信息写入的字符串，而（long utf）用于指定使用8字节长度信息写入的字符串。a. 语法规则序列化 Stream 由满足 Stream 规则的任何 Stream 表示【官方文档仅有字段值分类而无具体含义，这里的具体含义来自参考中的序列化草案】stream: **` magic` `version` `contents`** 整个数据流的格式，直接分成三部分，magic 表示魔数STREAM_MAGIC标记，version 表示序列化的版本STREAM_VERSION，contents表示最终生成的序列的内容；contents: **`content`** **` contents content`** 这一部分表示生成的二进制序列的内容部分，这些内容有可能是独立的内容【content】，也可能是多个内容的一个集合【contents】；content: **`object`** **` blockdata`** 二进制序列独立的内容【content】有可能包含对象定义的数据【object】，也有可能包含数据块格式的数据【blockdata】，上边格式也有能blockdata在前，object在后；object: **` newObject`** **` newClass`** **` newArray`** **` newString`** **` newEnum`** **` newClassDesc`** **` prevObject`** **` nullReference`** **` exception`** ==` TC_RESET`== ​ 该部分内容表示对象中包含的字节流数据，这部分数据中的元素相互间没有顺序，仅仅表示该对象中可能存在标记表示的数据；newObject表示新对象类型， newClass表示Class类型的对象，newArray表示数组对象，newString表示字符串对象，newEnum表示枚举常量，newClassDesc表示对象的类描述信息，preObject表示前边出现过的对象，nullReference表示空引用，exception表示异常对象，==TC_RESET==表示重置标记【固定值】。newClass: ==`TC_CLASS`== **`classDesc` `newHandle`** 该部分内容表示一个新的Class类型的对象，==TC_CLASS==表示类型标记，classDesc表示类描述信息，newHandle表示新的引用；classDesc: ` newClassDesc` ` nullReference` ` (ClassDesc)prevObject` // 要求为ClassDesc类型的对象 该部分表示一个对象的类描述符，newClassDesc表示新出现一个类描述符，nullReference表示空引用，prevObject表示前边出现过的对象； superClassDesc: **`classDesc`** 这部分表示父类的描述符信息，它的内容是一个classDesc，也就是上边类描述信息； newClassDesc: ==`TC_CLASSDESC`== **`className` `serialVersionUID` `newHandle` `classDescInfo`** ==`TC_PROXYCLASSDESC`== **`newHandle` `proxyClassDescInfo`** ​ 这个部分演示了类描述符中描述的两种类描述符信息：一般类描述信息，动态代理类描述信息，clsssName表示类名，serialVersionUID表示该类中定义的serialVersionUID对应的值，newHandle表示一个新的引用，classDescInfo表示类描述符本身的相关信息，proxyClassDescInfo表示动态代理类描述符本身相关的信息；classDescInfo: **` classDescFlags` `fields` `classAnnotation` `superClassDesc `** ​ 这一部分内容是详细的类描述信息，classDescFlags为类描述信息标记，fields表示类中所有字段的描述信息，classAnnotation表示和类相关的Annotation的描述信息，superClassDesc表示该类的父类的描述信息。className: *(utf)* 类全名，以UTF-8的格式保存的字符串对应的二进制序列，描述了当前对象的类全名；serialVersionUID: *(long)* ​ 对应类定义中的字段serialVersionUID的信息；classDescFlags: *(byte)* // 在终端符号和常量中的定义 ​ 类描述符标记，一个字节的数据，用于定义终止符和常量；proxyClassDescInfo: *(int)<count> proxyInterfaceName[count] classAnnotation superClassDesc* 动态代理类的相关描述信息，<count>表示该动态代理类实现的接口总数，类型为int类型。proxyInterfaceName[count]表示所有当前动态代理类实现的接口信息，classAnnotation表示该动态代理类对应的Annotation的描述信息，superClassDesc表示当前动态代理类的父类的类描述信息；proxyInterfaceName: *(utf)* ​ 动态代理类的代理接口的名称，一个UTF-8格式的字符串对应的二进制序列；fields: *(short)<count> fieldDesc[count]* <count>表示该类中的字段【成员属性】的总数，数据类型为short类型。fieldDesc[count]表示一个类中所有字段的详细描述信息，字段的数量和前边的count是一致的；fieldDesc: **`primitiveDesc`** ​ objectDesc 这个标记表示字段的描述信息，字段描述信息包括两部分信息内容，primitiveDesc表示基础类型数据的描述信息，objectDesc表示对象类型数据的描述信息；primitiveDesc: **`prim_typecode fieldName`** 基础类型的字段的相关描述信息，prim_typecode表示字段的类型标识，字段类型标识表示当前字段的类型，fieldName表示字段的名称，为一个字段名称组成的字符串的二进制序列；objectDesc: **`obj_typecode` `fieldName` `className1`** ​ 对象类型的字段的描述信息，obj_typecode表示字段的类型标识，该标识描述了对象字段对应的类信息，fieldName表示字段的名称，为一个字段名称组成的字符串的二进制序列，className1表示该成员属性的类型签名；fieldName: *(utf)* ​ 表字段名称字符串，该字符串由二进制序列组成且经过了 UTF-8 编码className1: *(String)object* // 包含字段类型的字符串，字段描述符格式 该对象对应的类的类全名，为一个String类型的对象描述信息；classAnnotation: **`endBlockData`** **`contents endBlockData`** // annotateClass 编写的内容 ​ 该对象所属类中的Annotation的描述信息，endBlockData为存储对象的数据块【Data-Block】的结束标记，为终止符，contents表示该类中多个内容的一个集合【contents】；prim_typecode: `B` // byte `C` // char `D` // double `F` // float `I` // integer `J` // long `S` // short `Z` // boolean 基础类型的字段的类型标识，标识了字段所属的基础数据类型，其代码表示的类型含义如定义中的注释部分的内容；obj_typecode: `[` // array `L` // object 对象类型的字段的类型标识，标识了字段所属的对象类型，其代码表示类型含义如注释部分的内容；newArray: ==`TC_ARRAY`== **`classDesc` `newHandle (int)<size> values[size]`** ​ 创建一个新的数组的描述符，==TC_ARRAY==表示接下来的序列是一个数组，它是数组序列的开始标记，classDesc是当前这个数组的类描述符，newHandle表示针对当前数组对象的引用，<size>表示该数组的长度，长度数字为int类型，values[size]表示当前数组每一个元素的值部分的内容；newObject: ==`TC_OBJECT`==**` classDesc newHandle classdata[]`** // 每个类的数据 ​ 创建一个新的对象的描述符信息，==TC_OBJECT==表示接下来的序列是一个新对象，它是对象的开始标记，classDesc是当前这个对象的类描述符，newHandle表示针对当前对象的引用，classdata[]这个对象对应的每一个Class的相关数据信息；classdata: **`nowrclass`** // SC_SERIALIZABLE & classDescFlag && !(SC_WRITE_METHOD & classDescFlags) **`wrclass objectAnnotation`** // SC_SERIALIZABLE &classDescFlag&&SC_WRITE_METHOD&classDescFlags **` externalContents`** // SC_EXTERNALIZABLE & classDescFlag && !(SC_BLOCKDATA & classDescFlags **`objectAnnotation`** // SC_EXTERNALIZABLE & classDescFlag&& SC_BLOCKDATA & classDescFlags ​ 这一部分数据描述的是类数据中所有内容，下边有针对各种不同的类数据相关说明；nowrclass: **`values`** // 类描述符顺序的字段 一个类中可序列化的字段的数据值，这些数据值的顺序遵循类描述符中定义的顺序；wrclass: **`nowrclass`** 这部分数据的内容和上述的nowrclass部分的内容是一样的，表一个类中可序列化的字段的数据值；objectAnnotation: **`endBlockData`** **`contents endBlockData`** // 由`writeObject`或`writeExternal PROTOCOL_VERSION_2`编写的内容。 这部分数据的内容和classAnnotation的数据结构是一致的；表该对象所属类中的Annotation的描述信息，endBlockData为存储对象的数据块【Data-Block】的结束标记，为终止符，contents表示该类中多个内容的一个集合【contents】；blockdata: **`blockdatashort`** **`blockdatalong`** 在Java序列化中，数据块存储分为两种：一种是长度为short的默认数据块方式，另外一种是长度为int的数据块方式，这种方式可存储容量大的数据；blockdatashort: ==`TC_BLOCKDATA`== **`(unsigned byte)<size> (byte)[size]`** ​ 描述了长度为short的默认数据块的结构；blockdatalong: ==` TC_BLOCKDATALONG`== **` (int)<size> (byte)[size]`** 描述了长度为int类型的数据块的结构；endBlockData : ==`TC_ENDBLOCKDATA`== 表示数据块的结束标记，一般用于描述当前的数据块结束了或者这个对象类型的描述符已经结束了；externalContent: // 只能由readExternal解析 *( bytes)* // 原始数据 `object` 这个部分描述的是外部化的相关内容，(bytes)部分的数据只能被readExternal方法读取，而且里面一般包含的数据类型是基础类型数据，object表示对象数据类型；externalContents: // 在PROTOCOL_VERSION_1中由writeExternal编写的外部内容。 **`externalContent`** ​ externalContents externalContent 这部分内容是上述的外部化内容的一个集合，一般这一部分只包含了使用writeExternal方法以PROTOCOL_VERSION_1的版本写入字节流的数据；newString: ==`TC_STRING`== **`newHandle`** *(utf)* ==`TC_LONGSTRING`== **`newHandle`** *(long-utf)* 表示一个字符串类型的数据，而字符串数据同样有两种类型：STRING和LONGSTRING；newEnum: ==`TC_ENUM`== **`classDesc` `newHandle` `enumConstantName`** ​ 表示一个Enum类型的数据，==TC_ENUM==为枚举类型的标识，表示接下来的序列类型是枚举类型，classDesc为一个枚举类型的类描述符，newHandle为该枚举对象的引用，enumConstantName的值为调用枚举类型中的name()方法返回的枚举类型的值对应的字符串字面量；enumConstantName: `(String)object` 枚举常量的字符串名称字面量，本身为一个字符串； prevObject ==`TC_REFERENCE`== `(int)handle` ​ 表示已经写入到字节流中的对象的一个对象的引用，==TC_REFERENCE==是引用标识，表接下来的数据类型是Java引用类型。nullReference ==`TC_NULL`== 就一个字节长度的数据，就表示null值，一般这个值表示的是对象的空引用；exception: ==`TC_EXCEPTION`== **`reset` `(Throwable)object` ` reset `** 针对异常信息的描述，==TC_EXCEPTION==为异常信息的标记，标识接下来的序列是一个异常对象；magic: **`STREAM_MAGIC`** ​ 魔数；version `STREAM_VERSION` ​ 序列化的版本信息，本文中使用的默认值是05；values: // 当前对象的ClassDec描述了大小和类型 针对当前对象的classDesc对应的类描述信息提供描述类型的大小；newHandle: // 序列中的下一个数字分配给被序列化或反序列化的对象 序列中的下一个数值将赋值给一个可序列化或者可执行反序列化的对象引用；reset: // 将丢弃已知对象集，以便异常的对象不会与以前发送的对象或异常之后可能发送的对象重叠 一个已知对象的集合将会被放弃，重置该字节流；b.终端符号和常数java.io.ObjectStreamConstants中的以下符号定义了 stream 中预期的终端值和常量值。 final static short STREAM_MAGIC = (short)0xaced; final static short STREAM_VERSION = 5; final static byte TC_NULL = (byte)0x70; final static byte TC_REFERENCE = (byte)0x71; final static byte TC_CLASSDESC = (byte)0x72; final static byte TC_OBJECT = (byte)0x73; final static byte TC_STRING = (byte)0x74; final static byte TC_ARRAY = (byte)0x75; final static byte TC_CLASS = (byte)0x76; final static byte TC_BLOCKDATA = (byte)0x77; final static byte TC_ENDBLOCKDATA = (byte)0x78; final static byte TC_RESET = (byte)0x79; final static byte TC_BLOCKDATALONG = (byte)0x7A; final static byte TC_EXCEPTION = (byte)0x7B; final static byte TC_LONGSTRING = (byte) 0x7C; final static byte TC_PROXYCLASSDESC = (byte) 0x7D; final static byte TC_ENUM = (byte) 0x7E; final static int baseWireHandle = 0x7E0000;标志字节 classDescFlags 可能包括以下值: final static byte SC_WRITE_METHOD = 0x01; //if SC_SERIALIZABLE final static byte SC_BLOCK_DATA = 0x08; //if SC_EXTERNALIZABLE final static byte SC_SERIALIZABLE = 0x02; final static byte SC_EXTERNALIZABLE = 0x04; final static byte SC_ENUM = 0x10;如果写入流的可序列化类具有writeObject方法，并且若该方法已将其他数据写入 stream ，则会设置标志SC_WRITE_METHOD。在这种情况下，TC_ENDBLOCKDATA标记总是希望终止该类的数据。如果使用SC_BLOCKDATA将Externalizable类写入 stream，则设置标志SC_BLOCKDATA。默认情况下，在JDK 1.2中将Externalizable对象写入stream的协议。JDK1.1中写入STREAM_PROTOCOL_1如果编写 stream 的类扩展了java.io.SERIALIZABLE而不是java.io.Externalizable，那么会设置标志 SC_SERIALIZABLE，读取 stream 的类也必须扩展java.io.SERIALIZABLE，并使用默认的序列化机制。如果编写 stream 扩展java.io.EXTERNALIZABLE的类，读取数据的类也必须扩展EXTERNALIZABLE，并且如果使用其writeExternal和readExternal方法读取数据，那么会设置标记SC_EXTERNALIZABLE。如果写入 stream 的类是枚举类型，则会设置标志SC_ENUM。接收方的对应类也必须是枚举类型。最后根据上述内容，可以总结得到下图：5、参考官方文档：https://docs.oracle.com/javase/8/docs/platform/serialization/spec/protocol.htmlJava序列化草案：https://blog.csdn.net/silentbalanceyh/article/details/8183849