用一个 case 去理解 jdk8u20 原生反序列化漏洞

本文最后更新于 2021.07.12,总计 37643 字 ,阅读本文大概需要 38 ~ 168 分钟
本文已超过 69天 没有更新。如果文章内容或图片资源失效,请留言反馈,我会及时处理,谢谢!

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.java

compare一下:

compare.png

// 改之前
        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,然后定义了mathurlRequest两个方法,并且在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时:

1.jpg

这种情况下,b=0使得a/b中的分母为0,导致内层出错,因此会进入catch块并打印出内层出错了字符串,但是由于内层的catch块并没有把错误抛出,因此继续执行剩余代码逻辑,向https://www.cnpanda.net地址发起http请求,打印状态码为200,由于在math方法中 TEST_NUMBER = a*(a+b)=1*(1+0)=1,因此打印出TEST_NUMBER1.0,最后打印all end结束代码逻辑。

a=1,b=0,url地址是https://test.cnpanda.net时:

2.jpg

这种情况下,b=0使得a/b中的分母为0,导致内层出错,因此会进入catch块并打印出内层出错了字符串,但是由于内层的catch块并没有把错误抛出,因此继续执行剩余代码逻辑,向https://test.cnpanda.net地址发起http请求,但是由于无法解析导致出错,进入catch块,在catch块中打印外层出错了字符串,然后抛出错误,结束代码逻辑。

a=1,b=2,url地址是https://www.cnpanda.net时:

3.jpg

这种情况下,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时:

4.jpg

这种情况下,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

5.jpg

这种情况下,urlhttps://www.cnpanda.net,因此会在内层向该地址发起http请求,并且打印状态码为200,内层执行完毕后,继续执行外层剩余代码逻辑,b=0使得a/b中的分母为0,导致外层出错,因此会进入catch块并打印出外层层出错了字符串,最后打印all end结束代码逻辑。

a=1,b=0,url地址是https://test.cnpanda.net

6.jpg

这种情况下,urlhttps://test.cnpanda.net,因此会在内层向该地址发起http请求,但是由于无法解析导致出错,进入catch块,在catch块中打印内层出错了字符串,由于内层出错,导致外层也出错,直接进入外层的catch块并打印出外层层出错了字符串,最后打印all end结束代码逻辑。

a=1,b=2,url地址是https://www.cnpanda.net

7.jpg

这种情况下,urlhttps://www.cnpanda.net,因此会在内层向该地址发起http请求,并且打印状态码为200,内层执行完毕后,继续执行外层剩余代码逻辑,b!=0使得a/b中的分母不为0,外层不会出错,因此执行完外层的逻辑,最后打印all end结束整个代码逻辑。

a=1,b=2,url地址是https://test.cnpanda.net

8.jpg

这种情况下,urlhttps://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工具可以查看一段序列化数据的结构,如下图所示:

9.jpg

可以看到,序列化结构的骨架是由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/9704

0x04 从一个case说起

由于jdk8u20真的比较复杂,因此为了方便理解,我写了一个简单的case,用于帮助读者理解下文。

假设存在两个类AnnotationInvocationHandlerBeanContextSupport,具体内容如下:

AnnotationInvocationHandler.java

package 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.java

package 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 !!!")错误。

10.jpg

由于在代码中我们生成了序列化文件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 表示为对象分配一个值为007e0000handle(因为引用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的描述信息,这里的数据值一般是由ObjectOutputStreamannotateClass()方法写入的,但由于annotateClass()方法默认为空,所以classAnnotations后一般会设置TC_ENDBLOCKDATA标识;(关于annotateClass具体可以看我写的序列化流程分析总结一文)

TC_ENDBLOCKDATA - 0x78数据块的结束标记,表示这个对象类型的描述符已经结束了;

superClassDesc表示父类的描述符信息,这里为空;

TC_NULL - 0x70表示当前对象是一个空引用;

newHandle 0x00 7e 00 01表示为对象分配一个值为007e0001handle,同上面的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();
    }
}

11.jpg

如果上述代码进行两次序列化,那么这个数据结构会变成什么?

可以来看看:

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();
    }
}

12.jpg

对比一下可以发现,在该序列化数据的结构中最后多了

TC_REFERENCE - 0x71
    Handle - 8257537 - 0x00 7e 00 01

这里对应的就是前文基础知识里“序列化中的两个机制”中引用机制里的一段话

每一个写入字节流的对象都会被赋予引用Handle,并且这个引用Handle可以反向引用该对象(使用TC_REFERENCE结构,引用前面handle的值),引用Handle会从0x7E0000开始进行顺序赋值并且自动自增,一旦字节流发生了重置则该引用Handle会重新从0x7E0000开始。

那么反序列化是如何处理TC_REFERENCE块的呢?

我在反序列化流程分析总结 一文中写到这样一个流程:

readObject0方法里有这样的一个判断:

13.jpg

是的,在反序列化的流程中,进入了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不为空、没有关联的ClassNotFoundExceptionstatus[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方法,并且在该方法中利用writeObjectwriteUTF方法写入了Panda对象以及This is a test data!字符串,该段序列化数据内容如下:

14.jpg

为了更直白的看变化,我们可以用compare工具来对比一下:

15.jpg

可以看到原先表示类描述信息标记由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中的8257539serializationDumper中生成的数值,具体在序列化或反序列化流程中的体现,没有具体深究,该值不影响我们最终序列化数据的生成,该值的生成算法如下:

    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里的内容

16.jpg

然后尝试再次运行以下代码:

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();
    }
}

结果如下:

17.jpg

成功实现了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来强制序列化流程继续下去,但是抛出的异常会导致BeanContextSupportObjectAnnotationTC_ENDBLOCKDATA - 0x78结尾标志无法被正常处理,如果我们不手动删除这个TC_ENDBLOCKDATA - 0x78那么会导致后面的结构归在ObjectAnnotation结构中,从而读取错误,反序列化出来的数据不是我们预期数据。所以我们在生成BeanContextSupportObjectAnnotation中不能按照正规的序列化结构,需要将标记结尾的结构TC_ENDBLOCKDATA - 0x78删除

也正由于我们把TC_ENDBLOCKDATA - 0x78删除了,会导致我们在使用SerializationDumper工具查看jdk8u20的序列化数据结构出错,如下图所示:

18.jpg

这里还有一个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.classAnnotationInvocationHandler 类的对象的字段,然后反序列化会去还原AnnotationInvocationHandler对象,成功的关联了下一个链!

最后就是同 Jdk7u21 一样的流程,利用动态代理触发Proxy.equals(EvilTemplates.class),达到恶意类注入实现RCE的最终目的。

目前jdk8u20反序列化漏洞 payload 的写法有以下几种方式:

纵观以上的方法各有各的优劣,有的容易理解,但是构建起来却很麻烦,有的不容易理解,但是构建起来较为方便,其实还有如果读者全文看下来,还有一种更容易理解的方法,就是先把jdk7u21漏洞利用的payload的序列化数据结构生成出来,然后向该数据结构中用类似case中的方法,去手动插入对象,但是这个工作量比较大,故我也没有手动去实现,有兴趣、有时间的朋友可以去尝试利用该方法生成payload(包你酸爽~但是对你理解jdk8u21来说,这种方式是最直白的方式)

另外,jdk8u20特殊的一点在于,实际上BeanContextSupport不能算是真正意义上的一条链,链还是jdk7u21中的那条链,只不过BeanContextSupport是用来避免抛出异常用到的媒介。

0x06 总结

本文对jdk8u20原生反序列化漏洞进行了分析,但和其他分析文章不同的是,本文没有按照常规的分析方法进行分析,而是重点写了一个case,用一个最简单的case去了解jdk8u20最核心的问题点,然后从整体上阐述了jdk8u20反序列化漏洞是怎么一回事,流程上是什么样的

站在读者的角度上去考虑,让自己如何用更直白的方式让别人理解你发的内容,我觉得这样的方式可以让我更能理解我所分析的漏洞、记忆我所写的内容,毕竟,每一个分析文章其实对于我来说都是一次整体上的总结

0x07 参考

https://github.com/pwntester/JRE8u20_RCE_Gadget

http://wouter.coekaerts.be/2015/annotationinvocationhandler

https://paper.seebug.org/456/

https://github.com/potats0/javaSerializationTools

https://mp.weixin.qq.com/s/SMq6aE5-qV9cINv1-74RgA

https://xz.aliyun.com/t/8277

https://mp.weixin.qq.com/s/3bJ668GVb39nT0NDVD-3IA

「感谢老板送来的软糖/蛋糕/布丁/牛奶/冰阔乐!」

panda

(๑>ڡ<)☆谢谢老板~

使用微信扫描二维码打赏

版权属于:

panda | 热爱安全的理想少年

本文链接:

https://www.cnpanda.net/sec/974.html(转载时请注明本文出处及文章链接)

添加新评论

暂无评论