← Back to Blog

Deserialization Attacks in Java: From Gadget Chains to Runtime Blocking

Java code snippet showing ObjectInputStream.readObject() call with a red warning overlay, representing the deserialization vulnerability entry point

Java deserialization vulnerabilities were publicly documented by Gabriel Lawrence and Chris Frohoff at AppSec California in January 2015. Their tool, ysoserial, generated exploit payloads for commonly used Java libraries. A year later, Stephen Breen identified that WebLogic, WebSphere, JBoss, Jenkins, and OpenNMS were all vulnerable. The class of vulnerability should have been eliminated from new deployments within a year. Instead, deserialization exploits appeared in major incidents through 2023 and remain in active exploitation today.

The persistence of this vulnerability tells you something important about the gap between knowing a class of vulnerability exists and actually eliminating it from complex enterprise Java applications.

Why Java Deserialization Is Uniquely Dangerous

Java's ObjectInputStream.readObject() method reconstructs a Java object from a byte stream. During reconstruction, the JVM executes code — specifically, the readObject() methods of any classes referenced in the serialized data. If an attacker can control the byte stream that reaches ObjectInputStream, they can cause the JVM to execute arbitrary Java code.

The key insight is that the attacker does not need the malicious class to be present in the application's own code. They need it to be present anywhere on the JVM classpath. Java applications, particularly enterprise applications built on application servers, have extremely large classpaths — dozens of library JARs, each potentially containing classes with exploitable readObject() methods. These exploitable classes are called gadget classes, and chains of them connected through Java's object graph traversal are gadget chains.

Apache Commons Collections, Spring Framework, Apache Commons BeanUtils, JDK's own runtime classes — all have contained gadget chains at various points. ysoserial currently includes 32 distinct gadget chains against widely used Java libraries. The attack surface is the entirety of the Java ecosystem on the target's classpath.

Where Deserialization Endpoints Appear

Java serialization was used for legitimate purposes throughout the Java ecosystem before the vulnerability class was well understood. Java RMI (Remote Method Invocation) serializes method arguments and return values. JMX (Java Management Extensions) serializes management objects. HTTP session clustering in application servers serializes session objects. Message queue consumers in JMS deserialize message payloads. Custom binary protocols in legacy enterprise systems often used Java serialization as the encoding format.

The challenge for remediation is that many of these endpoints are not exposed directly — they are internal, part of middleware, or embedded in application server functionality. The WebLogic T3 protocol deserialization vulnerabilities (CVE-2019-2725, CVE-2020-2883, and several others) exploited deserialization in the WebLogic clustering protocol, which was enabled by default and exposed on the admin port. Application developers did not write the vulnerable code and often did not know the endpoint existed.

The Allowlist Defense and Its Limitations

The standard recommended mitigation is to implement a deserialization allowlist using a custom ObjectInputStream subclass that overrides resolveClass() to throw an exception for any class not on an explicit allowlist. Java 9 introduced the Serialization Filtering API as a standard way to implement this.

The limitation of allowlists is that they require knowing exactly which classes the application legitimately deserializes. For custom application code that controls both the serializing and deserializing side, this is manageable. For middleware deserialization — session clustering, JMX, RMI — the legitimate class set is often large, complex, and documented nowhere. Teams that attempt to build allowlists for these endpoints frequently find that the list grows until it includes enough of the gadget classes to remain exploitable anyway.

Allowlist approaches are also brittle to application updates. New library versions may deserialize new classes. Dependency updates can silently break allowlists, causing application errors that teams diagnose without realizing the cause is a deserialization security control.

Detecting Gadget Chain Execution at Runtime

A gadget chain executes because of the methods it invokes during deserialization. The end goal of most Java deserialization exploits is one of three things: execute an OS command (Runtime.exec or ProcessBuilder.start), load a remote class (URLClassLoader.loadClass), or write a file to disk. These operations have very specific method signatures in the JVM.

Raven.io's Java agent instruments the method invocation layer for these sensitive operations. When Runtime.exec() is called during deserialization — specifically, from a call stack that originated in ObjectInputStream.readObject() — the agent blocks the method invocation before the OS command executes. The call stack trace is recorded and sent as a structured event to the SIEM.

This approach catches gadget chain execution regardless of which gadget classes are used. A newly discovered gadget chain in a library the agent has never seen before will still trigger a block because the exploit must ultimately invoke one of the monitored sensitive operations to cause damage. The agent does not need a signature for the specific gadget chain — it monitors the execution result, not the path.

Monitoring for Known Deserialization Attacks in the Log

Before the agent blocks an exploit, the exploit attempt itself is visible in application logs if your Java application server logs class resolution events. JVM deserialization attempts that fail due to ClassNotFoundException generate warning messages in server logs. A sudden burst of ClassNotFoundException messages for unusual class names (org.apache.commons.collections.functors.InvokerTransformer, com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl) is a reliable indicator of active deserialization exploitation attempts.

If you have a SIEM with Java application server logs ingested, a search for these class names in error logs identifies active exploitation scans. This does not prevent exploitation, but it provides detection visibility before you have a runtime layer in place.

Removing Gadget Classes from the Classpath

The most permanent fix for gadget chains is removing the gadget library from the classpath if it is not actually used by the application. Apache Commons Collections is a common gadget source. Applications that include it as a transitive dependency (pulled in by another library) often do not directly call any Commons Collections API. Removing the JAR from the classpath eliminates those gadget chains entirely.

Use a dependency analysis tool (Maven Dependency Plugin's analyze goal, or Gradle's dependencyInsight task) to identify which libraries are actually used versus just present on the classpath. For each library that contains known gadget chains, verify whether it is used directly or only as a transitive dependency, and evaluate whether it can be removed or replaced with a different library that achieves the same function without the gadget exposure.

In our experience deploying Raven.io across production Java stacks, approximately 35% of applications have at least one library containing known ysoserial gadget chains that could be removed from the classpath without functional impact. That is a permanent, code-level fix that complements runtime detection rather than depending on it.

Migration Away from Java Serialization

For new code and for application code under active development, the long-term answer is replacing Java serialization with a safer encoding format. Protocol Buffers, JSON, MessagePack, and Avro are all viable alternatives for data interchange. None of them execute code during deserialization. A Java object serialized to Protocol Buffers and deserialized back carries no code execution risk.

Migration for legacy systems is difficult because Java serialization is embedded in application server protocols that cannot be easily changed. But for application-level serialization — session data, cache entries, message queue payloads — migration is feasible and should be a medium-term remediation item for any organization with a mature Java application portfolio.

Runtime protection bridges the gap during that migration. As long as there are deserialization endpoints in your stack — whether you know about them or not — behavioral blocking at the sensitive operation level is the control that prevents exploitation when gadget chains are available.

Java Deserialization Detection

Raven.io's Java agent instruments Runtime.exec, URLClassLoader, and ProcessBuilder at the method invocation level. Gadget chain execution triggers a block before the OS command fires. Deploy in observation mode to see which deserialization events your application generates.

Start a Free Pilot