Java代码审计系列(六) 反序列化

  • A+
所属分类:代码审计
01 概述

首先我们介绍下序列化和反序列化的概念:

  • 序列化:把Java对象转换为字节序列的过程。
  • 反序列化:把字节序列恢复为Java对象的过程。

对象的序列化主要有两种用途:

  • 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;(持久化对象)
  • 在网络上传送对象的字节序列。(网络传输对象)

任何一门成熟的语言,在网络上传递信息,都会使用到一些格式化的数据结构,比如

YAML

JSON

XML

像阿里的FastJson就是简单化处理JSON格式的数据的。

02 序列化前提

在Java中,如果一个对象要想实现序列化,必须要实现下面两个接口之一:

  • Serializable 接口

  • Externalizable 接口

Java 通过对象输入输出流来实现序列化和反序列化:

  • java.io.ObjectOutputStream 类的 writeObject() 方法可以实现序列化;
  • java.io.ObjectInputStream 类的 readObject() 方法用于实现反序列化。

序列化和反序列化示例:

public class SerializeDemo01 {
    enum Sex {
        MALE,
        FEMALE
    }


    static class Person implements Serializable {
        private static final long serialVersionUID = 1L;
        private String name = null;
        private Integer age = null;
        private Sex sex;

        public Person() { }

        public Person(String name, Integer age, Sex sex) {
            this.name = name;
            this.age = age;
            this.sex = sex;
        }

        @Override
        public String toString() {
            return "Person{" + "name='" + name + ''' + ", age=" + age + ", sex=" + sex + '}';
        }
    }

    /**
     * 序列化
     */

    private static void serialize(String filename) throws IOException {
        File f = new File(filename); // 定义保存路径
        OutputStream out = new FileOutputStream(f); // 文件输出流
        ObjectOutputStream oos = new ObjectOutputStream(out); // 对象输出流
        oos.writeObject(new Person("Jack"30, Sex.MALE)); // 保存对象
        oos.close();
        out.close();
    }

    /**
     * 反序列化
     */

    private static void deserialize(String filename) throws IOException, ClassNotFoundException {
        File f = new File(filename); // 定义保存路径
        InputStream in = new FileInputStream(f); // 文件输入流
        ObjectInputStream ois = new ObjectInputStream(in); // 对象输入流
        Object obj = ois.readObject(); // 读取对象
        ois.close();
        in.close();
        System.out.println(obj);
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        final String filename = "~/Donwloads/s.dat";
        serialize(filename);
        deserialize(filename);
    }
}
Java代码审计系列(六) 反序列化

其中代码最关键的地方就是

ObjectInputStream ois = new ObjectInputStream(in); // 对象输入流
Object obj = ois.readObject(); // 读取对象

这两行其实就包括了整个反序列化的流程。

我们来看下生成的s.dat文件,用vim打开之后输入:%!xxd就可以查看文件的十六进制模式了

Java代码审计系列(六) 反序列化

一段数据如果以aced开头,那么他就是这一段java序列化的16进制

我们可以用到一个工具SerializationDumper 来查看我们序列化生成的字节流 https://github.com/NickstaDB/SerializationDumper

我们把十六进制提取出来命令运行后

Java代码审计系列(六) 反序列化
java -jar  /Users/fengxuan/Downloads/SerializationDumper-v1.13.jar aced000573720023636f6d2e6576616c7368656c6c2e5365724170706c69636174696f6e24506572736f6e00000000000000010200034c00036167657400134c6a6176612f6c616e672f496e74656765723b4c00046e616d657400124c6a6176612f6c616e672f537472696e673b4c00037365787400224c636f6d2f6576616c7368656c6c2f5365724170706c69636174696f6e245365783b7870737200116a6176612e6c616e672e496e746567657212e2a0a4f781873802000149000576616c7565787200106a6176612e6c616e672e4e756d62657286ac951d0b94e08b02000078700000001e7400044a61636b7e720020636f6d2e6576616c7368656c6c2e5365724170706c69636174696f6e2453657800000000000000001200007872000e6a6176612e6c616e672e456e756d000000000000000012000078707400044d414c450a

得到一下数据


STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
  TC_OBJECT - 0x73
    TC_CLASSDESC - 0x72
      className
        Length - 35 - 0x00 23
        Value - com.evalshell.SerApplication$Person - 0x636f6d2e6576616c7368656c6c2e5365724170706c69636174696f6e24506572736f6e
      serialVersionUID - 0x00 00 00 00 00 00 00 01
      newHandle 0x00 7e 00 00
      classDescFlags - 0x02 - SC_SERIALIZABLE
      fieldCount - 3 - 0x00 03
      Fields
        0:
          Object - L - 0x4c
          fieldName
            Length - 3 - 0x00 03
            Value - age - 0x616765
          className1
            TC_STRING - 0x74
              newHandle 0x00 7e 00 01
              Length - 19 - 0x00 13
              Value - Ljava/lang/Integer; - 0x4c6a6176612f6c616e672f496e74656765723b
        1:
          Object - L - 0x4c
          fieldName
            Length - 4 - 0x00 04
            Value - name - 0x6e616d65
          className1
            TC_STRING - 0x74
              newHandle 0x00 7e 00 02
              Length - 18 - 0x00 12
              Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b
        2:
          Object - L - 0x4c
          fieldName
            Length - 3 - 0x00 03
            Value - sex - 0x736578
          className1
            TC_STRING - 0x74
              newHandle 0x00 7e 00 03
              Length - 34 - 0x00 22
              Value - Lcom/evalshell/SerApplication$Sex; - 0x4c636f6d2f6576616c7368656c6c2f5365724170706c69636174696f6e245365783b
      classAnnotations
        TC_ENDBLOCKDATA - 0x78
      superClassDesc
        TC_NULL - 0x70
    newHandle 0x00 7e 00 04
    classdata
      com.evalshell.SerApplication$Person
        values
          age
            (object)
              TC_OBJECT - 0x73
                TC_CLASSDESC - 0x72
                  className
                    Length - 17 - 0x00 11
                    Value - java.lang.Integer - 0x6a6176612e6c616e672e496e7465676572
                  serialVersionUID - 0x12 e2 a0 a4 f7 81 87 38
                  newHandle 0x00 7e 00 05
                  classDescFlags - 0x02 - SC_SERIALIZABLE
                  fieldCount - 1 - 0x00 01
                  Fields
                    0:
                      Int - I - 0x49
                      fieldName
                        Length - 5 - 0x00 05
                        Value - value - 0x76616c7565
                  classAnnotations
                    TC_ENDBLOCKDATA - 0x78
                  superClassDesc
                    TC_CLASSDESC - 0x72
                      className
                        Length - 16 - 0x00 10
                        Value - java.lang.Number - 0x6a6176612e6c616e672e4e756d626572
                      serialVersionUID - 0x86 ac 95 1d 0b 94 e0 8b
                      newHandle 0x00 7e 00 06
                      classDescFlags - 0x02 - SC_SERIALIZABLE
                      fieldCount - 0 - 0x00 00
                      classAnnotations
                        TC_ENDBLOCKDATA - 0x78
                      superClassDesc
                        TC_NULL - 0x70
                newHandle 0x00 7e 00 07
                classdata
                  java.lang.Number
                    values
                  java.lang.Integer
                    values
                      value
                        (int)30 - 0x00 00 00 1e
          name
            (object)
              TC_STRING - 0x74
                newHandle 0x00 7e 00 08
                Length - 4 - 0x00 04
                Value - Jack - 0x4a61636b
          sex
            (object)
              TC_ENUM - 0x7e
                TC_CLASSDESC - 0x72
                  className
                    Length - 32 - 0x00 20
                    Value - com.evalshell.SerApplication$Sex - 0x636f6d2e6576616c7368656c6c2e5365724170706c69636174696f6e24536578
                  serialVersionUID - 0x00 00 00 00 00 00 00 00
                  newHandle 0x00 7e 00 09
                  classDescFlags - 0x12 - SC_SERIALIZABLE
                  fieldCount - 0 - 0x00 00
                  classAnnotations
                    TC_ENDBLOCKDATA - 0x78
                  superClassDesc
                    TC_CLASSDESC - 0x72
                      className
                        Length - 14 - 0x00 0e
                        Value - java.lang.Enum - 0x6a6176612e6c616e672e456e756d
                      serialVersionUID - 0x00 00 00 00 00 00 00 00
                      newHandle 0x00 7e 00 0a
                      classDescFlags - 0x12 - SC_SERIALIZABLE
                      fieldCount - 0 - 0x00 00
                      classAnnotations
                        TC_ENDBLOCKDATA - 0x78
                      superClassDesc
                        TC_NULL - 0x70
                newHandle 0x00 7e 00 0b
                TC_STRING - 0x74
                  newHandle 0x00 7e 00 0c
                  Length - 4 - 0x00 04
                  Value - MALE - 0x4d414c45
  Invalid content element type 0x0a

这里有两个关键的标识

TC_OBJECT:标记后面的数据为Object对象

TC_CLASSDESC:类描述符标识,表示一个类中的所有信息

当我们实例化ObjectInputStream后,首先调用的是ObjectInputStream的构造方法。

ObjectInputStreamObjectOutputStream类一样有两个构造方法 —— 一个为public的单参数构造方法,一个为protected的无参构造方法

同样地,当我们实例化ObjectInputStream并传入new FileInputStream("panda.out")参数后,调用的是ObjectInputStream中的public单参数构造方法,该方法内容如下:

Java代码审计系列(六) 反序列化

ObjectOutputStream的构造方法一样——在该构造函数的开始,首先会调用verifySubclass方法处理缓存信息,要求该类(或子类)进行验证——验证是否可以在不违反安全约束的情况下构造此实例。

然后和ObjectOutputStream不同的是,在ObjectOutputStream中我们初始化的对象是bouthandlessubs以及enableOverride,但是在ObjectInputStream中,我们初始化的对象变成了binhandlesvlist以及enableOverride


 /** filter stream for handling block data conversion */
    private final BlockDataInputStream bin;
    /** validation callback list */
    private final ValidationList vlist;
 /** wire handle -> obj/exception map */
    private final HandleTable handles;
 /** if true, invoke readObjectOverride() instead of readObject() */
    private final boolean enableOverride;

03 漏洞基本原理

我们用下面这个例子来说明序列化漏洞的产生。

public class test{
    public static void main(String args[]) throws Exception{
        //定义myObj对象
        MyObject myObj = new MyObject();
        myObj.name = "hi";
        //创建一个包含对象进行反序列化信息的”object”数据文件
        FileOutputStream fos = new FileOutputStream("object");
        ObjectOutputStream os = new ObjectOutputStream(fos);
        //writeObject()方法将myObj对象写入object文件
        os.writeObject(myObj);
        os.close();
        //从文件中反序列化obj对象
        FileInputStream fis = new FileInputStream("object");
        ObjectInputStream ois = new ObjectInputStream(fis);
        //恢复对象
        MyObject objectFromDisk = (MyObject)ois.readObject();
        System.out.println(objectFromDisk.name);
        ois.close();
    }
}

class MyObject implements Serializable{
    public String name;
    //重写readObject()方法
    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
        //执行默认的readObject()方法
        in.defaultReadObject();
        //执行打开计算器程序命令
        Runtime.getRuntime().exec("open /System/Applications/Calculator.app");
    }
}
Java代码审计系列(六) 反序列化

简单一句话概括就是:一个类在实现Serializable 后重写了 readObject 这个方法,在这里写上了一些危险的代码,就会导致反序列化漏洞的产生。

有的同学忽然惊呼:这不是和php反序列化一样吗?php是在__wakeup魔法方法里,而这里是readObject方法。

我想说的是:对 就是一样,很多同学被一些文章转发的CommonsCollections 这个利用链吓住了,其实正常情况下就是看readObject这个方法有没有危险方法或者利用点的。

04 ysoserial

说到反序列化的利用,就不得不提到ysoserial 链接地址:https://github.com/frohoff/ysoserial, 简单来说,就是利用自己选择的利用链,生成反序列化数据,通过这些数据发送给服务器,从而执行预定的命令。

java -jar ysoserial.jar CommonsCollections1 calc.exe | xxd

如上,ysoserial的利用链就是执行一个系统命令,把生成好的exp发送到对方机器上,这条命令就会执行。

05 总结

如何发现 Java 反序列化漏洞

  1. 从流量中发现序列化的痕迹,关键字:ac ed 00 05rO0AB (见图1)
  2. Java RMI 的传输 100% 基于反序列化。
  3. 查看源码,看下哪些类实现了Serializable接口
  4. 观察反序列化的类中是否重写readObject() ,是否有可利用的地方


本文始发于微信公众号(风炫安全):Java代码审计系列(六) 反序列化

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: