Java insecure deserialization 101
Serialization is the process of converting an object into a sequence of bytes. Deserialization is the opposite: it takes a sequence of bytes and converts them back to a data structure.
The serialization/deserialization mechanism is supported by many programming languages, including Java. Java is an OOP (Object-Oriented Programming) language so it’s really helpful to convert objects to byte streams and vice versa. For example this can be useful if we need to store the data somewhere, or transfer the data through the network.
*Image from PortSwigger
In order to serialize a Java object we need to implement the Serializable
interface. This is enough for our object to be serialized (Java will handle the serialization process by itself). The following example shows how we can serialize and a MyClass
object (in this case we’ll save the serialized object to a file).
public class MyClass implements Serializable {
private String value;
public MyClass(String value) {
this.value = value;
}
public static void main(String args[]) throws IOException, ClassNotFoundException {
MyClass obj = new MyClass("Hi there!");
FileOutputStream fileOut = new FileOutputStream("serialized");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(obj);
out.close();
fileOut.close();
}
}
We can then reconstruct the object by deserializing it as shown below (notice that we are calling the readObject
method of the ObjectInputStream
class, which is responsible for the deserialization process):
public static void main(String args[]) throws IOException, ClassNotFoundException {
FileInputStream fileIn = new FileInputStream("serialized");
ObjectInputStream in = new ObjectInputStream(fileIn);
MyClass obj = (MyClass) in.readObject();
System.out.println(obj.value);
}
If we run the previous code we’ll get the value
attribute (“Hi there!") printed out on the console.
To better understand the serialization process, let’s analyze the “serialized” file with an hex editor. We can clearly see that Java “packed” our object (a serialized Java object always starts with the 2 bytes 0xAC and 0xED) and it includes the string “Hi there!” for the value
attribute.
Also notice that at byte 0x3B there is a value of 0x09, which is exactly the length of our string “Hi there!”.
So what happens if we modify the file with some arbitrary string and we try to deserialize back the file to a Java object?
Let’s change the file as shown here:
And if we run the same program we get the string “Hacked!” printed out on the console.
Now, this might not be a critical security issue: we just changed the value of the original string with a different one. However, imagine if an application doesn’t perform any validation when deserializing untrusted user inputs: an attacker could be able to tamper the data with arbitrary values (for example by changing an attribute isAdmin
from false
to true
..) gaining access to unauthorized resources.
Furthermore, if the application doesn’t properly validate the deserialization process, we can get additional issues. For example, what happens if we just set the length of the value
attribute with a value higher than the actual string length? The application will crash with an EOFException
causing a denial-of-service. But things can get even worse.
A simple Vulnerable example class
To better understand insecure deserialization and how it can be used by an attacker to execute arbitrary code, let’s first assume that our application has a Vulnerable
class that is used to execute some commands. This class implements both the Serializable
and the Runnable
interface, as shown below:
public class Vulnerable implements Serializable, Runnable {
private String command;
private Runnable runnable;
public Vulnerable(String command) {
this.command = command;
this.runnable = this;
}
private final void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
runnable.run();
}
@Override
public void run() {
try {
Runtime.getRuntime().exec(command);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Notice that we overrode the run
method (required by the Runnable
interface) and we also implemented the readObject
method (which is called automatically by Java when deserialization takes place). For the readObject
implementation, we called the defaultReadObject
method (which will do the actual deserialization process) then we call the run
method to execute the specified command
.
Now, if an attacker is able to create a serialized Vulnerable
object, he can get RCE. For example the following code will create a payload
file:
public static void main(String args[]) throws IOException, ClassNotFoundException {
Vulnerable obj = new Vulnerable("touch hacked");
FileOutputStream fileOut = new FileOutputStream("payload");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(obj);
out.close();
fileOut.close();
}
Then we can send the payload to the vulnerable application that will try to deserialize the provided payload
file as follow:
FileInputStream fileIn = new FileInputStream("payload");
ObjectInputStream in = new ObjectInputStream(fileIn);
MyClass obj = (MyClass) in.readObject();
The call to the readObject()
method will trigger command execution and a new file “hacked” will be created (at the same location where the application is running). So if the application doesn’t perform any check on the data that must be deserialized, an attacker can exploit this vulnerability and execute arbitrary code. Notice that we are trying to cast the object returned from the readObject
call to an object of type MyClass
. This cast operation will fail (and an exception will be thrown by the application) but this doesn’t prevent the execution of the payload (which happens inside the readObject
method).
Of course, the previous example is vulnerable on purpose and it’s quite unlikely that your application implements such vulnerable class (also, it will require the attacker to provide a malicious serialized object as input for the application).
On the other hand, if you are developing a Java application, it is almost certain that you are going to import many third-party dependencies and their classes will be included in your classpath. So, if an attacker is somehow able to leverage the methods of the classes in libraries imported in your project, he could be able to “chain” them” to perform malicious actions (this principle is similar to the ROP chain attack in binary exploitation).
The question is: is it possible to find such “gadgets” in popular Java libraries ? The answer is yes.
Using gadgets to get RCE
In this context, a gadget is a sequence of classes and methods that can be chained together to exploit the deserialization process (also referred as “gadget chain”) and gain control of the target system.
A gadget chain always starts with some entry-point (also referred as source) and it can be chained with other gadgets until it ends to a sink (for example a class method that allow us to execute code).
*Image from ShiftLeft Blog
Finding a working gadget chain is hard, but several security researchers already discovered gadget chains in common Java libraries. Just to give you an idea, working gadgets have been found in several versions of common-collections
, spring-core
, spring-beans
and other libraries (or combination of libraries). So if your application includes these libraries, it’s possible for an attacker to use gadgets and build a working exploit (if your application performs unsafe deserialization).
An amazing and useful tool for creating payloads using such gadgets has been developed by Chris Frohoff and it’s called ysoserial (“Why So Serial”).
It’s important to notice that the deserialization vulnerabilities arise when the application does not perform any check against unsafe deserialization and having gadgets in your classpath is not a vulnerability by itself (however it can allow an attacker to exploit your application if vulnerable).
Also, I want to mention that even if you are not using serialization/deserialization directly in your application, you might still be vulnerable to this attack. For example your application might rely on some Java marshalling/unmarshalling library (like Jackson, JYAML, SnakeYAML, and many others) that are commonly used to serialize Java objects to JSON/YAML and vice versa. In the past, several critical vulnerabilities have been discovered also in such libraries (CVE-2017-7525, CVE-2020-8441, CVE-2022-1471).
Conclusions
In this article we saw the basics of the serialization/deserialization mechanism in Java and why it’s dangerous to deserialize untrusted data without applying any security check. We saw how common Java libraries imported in our application can be used by attackers to build a gadget chain and perform malicious activities like denial-of-service, breaking access control systems and even remote code execution.
We also saw that deserialization issues can even apply to famous marshalling/unmarshalling libraries and this emphasizes how it’s important to keep our software supply chain up-to-date.