5.3.7. Deserialization

5.3.7.1. Introduction

Serialization is to convert objects into byte streams, which are convenient for saving in memory, files, and databases; deserialization is the reverse process, which is restored from byte streams to objects. It is generally used in scenarios such as remote invocation, transferring objects to a remote server over a network, storing objects in a database, or waiting for reuse locally. ObjectOutputStream Class methods in Java writeObject() can implement serialization, and ObjectInputStream class readObject() methods are used for deserialization. If you want to implement deserialization of a class, you implement the Serializable interface .

A deserialization vulnerability exists when a remote service accepts untrusted data and deserializes it and an exploitable class exists in the current environment.

5.3.7.1.1. Sequence data structures

  • 0xaced Magic Head / STREAM_MAGIC

  • 0x0005 version number / STREAM_VERSION / refer to java.io.ObjectStreamConstant

  • 0x73 Object Type Identifier

  • 0x72 class descriptor identifier

5.3.7.1.2. Serialization Process

  • When the ObjectOutputStream instance is initialized, the magic header and version number are written to bout (BlockDataOutputStream type)

  • Call ObjectOutputStream.writeObject() to start writing object data
    • ObjectStreamClass.lookup() encapsulates the class description to be serialized (returns ObjectStreamClass type), obtains the class name, custom serialVersionUID, serializable fields (returns ObjectStreamField type) and constructor, as well as writeObject, readObject methods, etc.

    • writeOrdinaryObject() writes object data
      • write object type identifier

      • writeClassDesc()enters branch writeNonProxyDesc() to write class description data
        • write class descriptor identifier

        • write class name

        • write SUID (when SUID is empty, it will be calculated and assigned)

        • calculate and write serialized attribute flags

        • write field info data

        • write Block Data end flag

        • write parent class description data

      • writeSerialData()writes the serialized data of the object
        • If the class customizes writeObject(), call this method to write the object, otherwise call defaultWriteFields() to write the field data of the object (if it is a non-primitive type, recursively process the sub-object)

5.3.7.1.3. Deserialization Process

  • When the ObjectInputStream instance is initialized, read the magic header and version number for verification

  • Call ObjectInputStream.readObject() to start reading object data
    • Read Object Type ID

    • readOrdinaryObject() reads the data object
      • readClassDesc() reads class description data
        • Read the class descriptor identifier and enter the branch readNonProxyDesc()

        • read class name

        • read SUID

        • Read and parse serialized attribute flags

        • Read field information data

        • resolveClass() gets the Class object of the class to be deserialized according to the class name, and throws ClassNotFoundException if the acquisition fails

        • skipCustomData() loops to read bytes until the end of Block Data is marked

        • Read parent class description data

        • In initNonProxy(), it is judged whether the SUID and class name (excluding the package name) of the object and the local object are the same. If they are different, an InvalidClassException will be thrown

      • ObjectStreamClass.newInstance() gets and calls the no-argument constructor of the nearest non-Serializable parent class to the object (if it does not exist, returns null) to create an object instance

      • readSerialData()reads the serialized data of an object
        • If the class defines readObject(), call this method to read the object, otherwise call defaultReadFields() to read and fill the field data of the object

5.3.7.2. Vulnerability Exploitation

5.3.7.2.1. Dangerous base libraries

  • com.mchange:c3p0 0.9.5.2

  • com.mchange:mchange-commons-java 0.2.11

  • commons-beanutils 1.9.2

  • commons-collections 3.1

  • commons-fileupload 1.3.1

  • commons-io 2.4

  • commons-logging 1.2

  • org.apache.commons:commons-collections 4.0

  • org.beanshell:bsh 2.0b5

  • org.codehaus.groovy:groovy 2.3.9

  • org.slf4j:slf4j-api 1.7.21

  • org.springframework:spring-aop 4.1.4.RELEASE

5.3.7.2.2. Echo Mode

  • Echo via middleware feature

  • echo by throwing an exception

  • echo via OOB

  • echo by writing static files

5.3.7.3. Vulnerability fixes and protection

5.3.7.3.1. Hook resolveClass

When readObject() using deserialization, the resolveClass method will be called to read the deserialized class name. You can check the deserialized class by hooking this method. A demo is as follows:

@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
    if (!desc.getName().equals(SerialObject.class.getName())) {
        throw new InvalidClassException(
                "Unauthorized deserialization attempt",
                desc.getName());
    }
    return super.resolveClass(desc);
}

The above demo only allows serialization. SerialObject In this way, you can set a whitelist that allows serialization to prevent deserialization vulnerabilities from being exploited. SerialKiller/Jackson/Weblogic etc all use this method to defend.

5.3.7.3.2. ValidatingObjectInputStream

Apache Commons IO Serialization ValidatingObjectInputStream class provides a accept method through which the deserialization class white/blacklist control can be implemented. A demo is as follows:

private static Object deserialize(byte[] buffer) throws IOException, ClassNotFoundException , ConfigurationException {
    Object obj;
    ByteArrayInputStream bais = new ByteArrayInputStream(buffer);
    ValidatingObjectInputStream ois = new ValidatingObjectInputStream(bais);
    ois.accept(SerialObject.class);
    obj = ois.readObject();
    return obj;
}

5.3.7.3.3. ObjectInputFilter(JEP290)

Java 9 provides new features that support serialized data filtering. You can inherit the java.io.ObjectInputFilter class override checkInput method to implement custom filters, and use the ObjectInputStream object ‘s setObjectInputFilter set filter to implement deserialization class white/blacklist control. This mechanism itself is a new feature for Java 9, but then the official suddenly decided to introduce the enhancement mechanism downwards, supporting JDK 6, 7, and 8 respectively. This mechanism mainly describes the following mechanisms:

  • Provides a mechanism to restrict deserialization classes, whitelist or blacklist

  • Limit the depth and complexity of deserialization

  • Provides a mechanism for validating classes for RMI remote invocation objects

  • Define a configurable filtering mechanism, for example, you can define a filter in the form of a configuration properties file