Serialization exploits in JVM

Posted by Marcin on Friday, April 5, 2024

Intro

The Java Virtual Machine (JVM) provides a mechanism for persisting Java objects, known as serialization. When introducing, it was a great step forward, as developers stopped reinventing the wheel and writing the same (still complex) boilerplate code. As we later learned it also opened a pandora box. I will try to describe the problem in the article below.

Problem

This process of serialization involves converting the state of an object into a byte stream, which can then be reverted back into a copy of the object. That way we can persist any object, store it, then load whenever we want.

However, this feature can be exploited, leading to what are known as “serialization vulnerabilities”. What are these? Turns out, serializable interface allows to read any serialized object even an evil one and tries to load it into a class we control. Scary right?

By default, loading of any serialized object involves executing custom code which is totally in control of the evil serialized payload.

How is this possible?

The weak parts in the implementation of the Serializable interface that make these exploits possible are:

  • Lack of validation: The Serializable interface does not provide a built-in mechanism for validating the serialized data when loaded. This means that an attacker can craft a serialized object with malicious data.

  • Execution during deserialization: During the deserialization process, the readObject method is called, which can result in the execution of code. If an attacker can control the data that is being deserialized, they can cause arbitrary code to be executed. The below class is so called gadget that can execute an action once deserialized.

    class Gadget implements Serializable {
    
        private Runnable command
    
        Gadget(Command command) {
            this.command = command
        }
    
        private final void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
            input.defaultReadObject()
            command.run()
        }
    }
    

    Let’s stop here and understand what is this readObject method doing here.

Custom data loading

When you read java docs about Serializable interface you will find:

Classes that require special handling during the serialization and deserialization process must implement special methods with these exact signatures.

One of them is:

private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;

And below:

The readObject method is responsible for reading from the stream and restoring the classes fields.

Oh wait… that means we are able to override default way how objects are deserialized. If the serialized object has this method definition, JVM will execute it. It might fail to deserialize, still the code is already running.

Proof-of-concept code

Main idea

Now that we know how deserializing exploits are possible, lets see implementation. The gadgets-poc project is my proof-of-concept (PoC) that demonstrates such an exploit. It’s presenting how single command execution can be achieved when deserializing unsafe object.

The TroubleHelper is an util class and is a key part of this PoC. It contains two methods: persistGadget and load.

class TroubleHelper {

    static void persistGadget(String fileName, String commandValue) {
        Command command = new Command(commandValue)
        Gadget gadgetObject = new Gadget(command)
        FileOutputStream fileOut = new FileOutputStream(fileName)
        ObjectOutputStream out = new ObjectOutputStream(fileOut)
        out.writeObject(gadgetObject)
        out.close()
        fileOut.close()
    }

    static PerfectlyValidObject load(String fileName) {
        FileInputStream fileIn = new FileInputStream(fileName)
        ObjectInputStream input = new ObjectInputStream(fileIn)
        PerfectlyValidObject validObject = (PerfectlyValidObject) input.readObject()
        return validObject
    }
}

The persistGadget method creates an evil Gadget object with a Runnable Command object of our choose (commandValue can be console command), which is then crafted, serialized and written to a file. This code is something attacker might use to prepare an evil payload in his side.

On the other hand the load method representing our code, that we often use to deserialize an object. That one that should be a valid. After all we explicitly cast it to PerfectlyValidObject, right?

As we learned before that doesn’t matter. JVM will execute readObject method from Gadget class.

Let’s see that in action. Our appliction main method executes the following code:

// Persisting Gadget executing unix command: cat /etc/passwd command
TroubleHelper.persistGadget("perfectly-valid-object.ser", "cat /etc/passwd")
// Deserializing Gadget content as PerfectlyValidObject object
PerfectlyValidObject perfectlyValidObject = TroubleHelper.load("perfectly-valid-object.ser")

Let’s run the code and see what happens:

./gradlew jar

java -jar build/libs/gadgets-poc-0.0.1-SNAPSHOT.jar

Results in an exception:

Caused by: org.codehaus.groovy.runtime.typehandling.GroovyCastException: Cannot cast object 'com.codeappeal.soserial.Gadget@50194e8d' with class 'com.codeappeal.soserial.Gadget' to class 'com.codeappeal.soserial.PerfectlyValidObject'

seems like we are safe? Not really.

As if we scroll above we can see that the Runnable command:

cat /etc/passwd

has already been executed, and it printed contents of our file to the console!

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync

Turns out deserializing of the Gadget object failed but the command was executed. This is common scenario in such exploits.

Info

Check your logs for Cannot cast object exception. It might be a sign of serialization exploit.

‘Not really a threat’ ? or ‘Way too complex to implement in real world’?

Libraries often contain code (gadgets) that can be used to perform actions that are useful for an attacker, such as executing commands, changing file permissions, etc. An attacker can chain these gadgets together in a serialized object to perform a series of actions when the object is deserialized. These gadgets are often found in libraries that the target application is using. An attacker can craft a serialized object that uses these gadgets to execute arbitrary code when it is deserialized.

If you think crafting those gadget chain can be complex, you’re right. But there are tools for automating that.
I will cover that topic in the next part of this article.

Source code

Described PoC source code is hosted here


comments powered by Disqus