Java Serialisation - The Gift That Keeps on Taking (Part 2)

March 30, 2022 By Steve Poole

8 minute read time

In the previous post we started to look at the reasons why Java serialisation is required and what the requisite and necessary design points were.

In this entry, we'll examine these particular characteristics and design points to reveal a few unexpected consequences.

As always when looking at software designs that have been around for some time is important to consider the wider context in place at the time. So as we go through these unexpected consequences and later explore how Java serialisation can be exploited, remember that Java was very new, the internet was just beginning and the concept of external cyber attacks was practically unknown. Viruses were a peculiar Windows thing and spread by the most common internet medium - email.  

The original designers of Java serialisation and those that support it now may curse at how it can be abused, but hindsight is always right and at the time what was created was agreed to be a good practical solution.

Unreplaceable

It turns out the the design and implementation of Java serialisation is so practical and useful that not using it is a challenge. With native Java serialisation being so embedded in the design of the Java runtime and being so performant, alternative methods of serialisation are often ignored simply because they are slower and harder to use.

Simple to use

Using Java serialisation is simple. A few lines of Java code and a working serialisation process is achieved. The only other requirement is that the Java class to be serialised implements the java.io.Serializable interface. 

Three lines of Java code will serialise a Java object to a file. In this case the object is of some arbitrary type referenced by the variable hw 

File f=new File(PATHNAME);

ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream(f));

oos.writeObject(hw);

And three lines of code will deserialize an object from a file. 

File f=new File(PATHNAME);

ObjectInputStream ois=new ObjectInputStream(new FileInputStream(f));


Object obj=ois.readObject();


Widespread and performant 

This is so easy that using the native Java serialisation is extremely widespread. A simple search on github for “ObjectInputStream'' found almost three million hits at time of writing. The serialisation process is highly optimised and built into the JVM. For almost all use-cases it just works.  It’s not perhaps then an unexpected or undesired consequence that native Java serialisation is so widely used today Though it certainly is an unhappy modern one. 

As we’re going to show, the widespread use of Java serialisation without the necessary understanding or safety checks is a gift to the many bad actors out there. Many dangerous consequences come from the self-defining nature of a Java serialisation stream. A design intended to deal with Java polymorphism is easily exploited unless the necessary code checks are in place.

Beginners guide to exploitation

The self defining nature of Java serialisation is at the core of serialisation attacks. Let’s see it in action. First here is a simple class:

public class HelloSerialisedWorld implements Serializable {

 private String greeting=null;

 public HelloSerialisedWorld(String salutation) {

 if(salutation==null)
throw new IllegalArgumentException("missing salutation");


 this.greeting=salutation.toUpperCase();

 }

 public void printGreeting() {

 System.out.println(greeting);

 }

}


Serialising an instance of this object is straightforward. Here is a slightly contrived example so that we can see the object being constructed.

File f=new File(PATHNAME);

HelloSerialisedWorld hw=new HelloSerialisedWorld("Hello DevZone");

ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream(f));

oos.writeObject(hw);

Peeking inside the box

The file just written has a structured format that is defined here. The structure has a complex layout (we are serialising a graph of objects after all) so for this discussion we'll simplify the format into a table and skip irrelevant metadata

Our serialised object with a greeting that contains "Hello DevZone" looks like this 

Field
Value
class name
HelloSerialisedWorld
field count
1
field type
L
field name
greeting
field value class name
java.lang.String
field string value
“Hello DevZone”
 

At first glance the table looks very much as expected. Essentially there is 1 field, called "greeting" and its value is of type "String". However with a more detailed examination the essential polymorphic nature of Java and Java serialisation is revealed. The "java.lang.String" entry simply describes the form of the data to follow. It does not have any relation to the type of the field "greeting".

Assuming the bad actors can change this data what they do?

Dangerous consequences

The field type ‘L’ means “Object” hence the presence of the class name field. Since this is a self defining process what happens if a different object type is used?. May be we could add an Integer instead

Field
Value
class name
HelloSerialisedWorld
field count
1
field type
L
field name
greeting
field value class name
java.lang.Integer
field integer value
100

 

This results in an exception during deserialization:

java.lang.ClassCastException: cannot assign instance of
java.lang.Integer to field HelloSerialisedWorld.greeting
of type java.lang.String in instance of HelloSerialisedWorld

Which is exactly the behaviour expected and wanted. However, look closely at the message. It says “cannot assign instance of java.lang.Integer”.

This message shows that an instance of Integer has already been created before being assigned. That may not mean much for an integer but consider what would happen for a more complex class - what code might get driven by the simple act of instantiation?

Cold hard reality

A prized objective of bad actors is to be able to execute code under their control. The fact that ultimately there may be an exception thrown is immaterial if during deserialization they manage to run the code they desire. In fact throwing exceptions can often mask their intention and obfuscate tracks

Java serialisation’s self-defining protocol makes running code particularly straightforward to initiate. The structure of the original class is irrelevant. Once the bad actor can force the instantiation of one object they can force instantiation of complex object graphs. Completely under their control.

Think back to the github search earlier. Java serialisation is used widely. It can be found in many frameworks and is used for sending data between systems in many ways. There is significant scope for exploitation. All it takes is a coding mistake, a misconfigured server etc to provide a path in.

Remember: all the bad actor has to have is the ability to influence the serialisation data.

Next Time

We'll explore more around exploiting serialisation datastreams. How it's possible to compromise systems silently and in different ways: from changing data, running arbitrary code or even crashing systems.  

As this series suggests - Java serialisation is the gift that keeps on taking. 

Tags: open source security, java, serialization, DevZone

Written by Steve Poole

Developer Advocate, Security Champion, DevOps practitioner (whatever that means) Long time Java developer, leader and evangelist. I’ve been working on Java SDKs and JVMs since Java was less than 1. JavaOne Rockstar, JSR leader and representation, Committer on open source projects including ones at Apache, Eclipse and OpenJDK. A seasoned speaker and regular presenter at international conferences on technical and software engineering topics.