Safeguard
Application Security

Java deserialization gadget chains explained

One 2015 talk and a tool called ysoserial turned ordinary Java libraries into remote code execution chains — here's how gadget chains work and how to stop them.

Safeguard Research Team
Research
6 min read

In January 2015, at AppSecCali, researchers Chris Frohoff and Gabriel Lawrence gave a talk called "Marshalling Pickles" that changed how the industry thinks about Java security. They showed that Java's built-in serialization format — the byte stream produced by writeObject and consumed by ObjectInputStream.readObject() — could be weaponized without any memory-corruption bug at all. The trick was chaining together the readObject(), equals(), and hashCode() methods of completely ordinary, already-trusted classes from libraries like Apache Commons Collections and Spring, so that deserializing a crafted byte stream triggered a cascade ending in arbitrary code execution. They released a tool, ysoserial, that generates these payloads on demand. Later that year, Stephen Breen of FoxGlove Security showed the same Commons Collections chain gave remote code execution against WebLogic, WebSphere, JBoss, and Jenkins — because Commons Collections shipped bundled inside nearly every one of them. The resulting CVEs, including CVE-2015-4852 against Oracle WebLogic and CVE-2015-7501 against JBoss, took years to fully remediate industry-wide, because the vulnerability wasn't in one library — it was in the deserialization model itself. This post explains how gadget chains work, why they're so hard to patch permanently, and what actually stops them.

What makes Java's native serialization dangerous in the first place?

Java's ObjectInputStream.readObject() reconstructs an object graph from a byte stream by instantiating classes and calling their readObject() methods, and it does this before any application logic gets a chance to validate what's being built. The stream itself specifies which classes to instantiate — the caller doesn't choose them in advance. If an attacker can supply that byte stream, they aren't limited to sending you data; they're choosing which classes on your classpath get constructed and which of their methods run during construction. Frohoff and Lawrence's insight was that readObject() implementations already existed all over common libraries that performed reflection, proxying, or transformation as ordinary side effects. None of those methods was a bug on its own — Commons Collections' InvokerTransformer, for instance, was designed to invoke a named method via reflection, which is exactly its documented job. The vulnerability only exists in the combination: untrusted input reaching readObject() plus a "gadget-capable" class sitting on the classpath.

How does a gadget chain actually turn into code execution?

A gadget chain links several classes so that deserializing the first one triggers a method call on the next, and so on, until the final link executes attacker-chosen code. In the ysoserial CommonsCollections chain, a java.util.PriorityQueue or similar container is deserialized holding a Commons Collections TransformerFunction chain; when the container calls compare() or hashCode() on its elements during deserialization, it triggers a ChainedTransformer, which invokes InvokerTransformer.transform() with attacker-supplied method names and arguments — ultimately calling Runtime.exec() with a command the attacker chose. No single class did anything wrong in isolation. This is why ysoserial ships dozens of named payload chains (CommonsCollections1 through CommonsCollections6, Spring1, Groovy1, and others as of the project's public GitHub repository, frohoff/ysoserial) — each one targets whatever gadget-capable libraries happen to already be on a given application's classpath.

Why didn't patching Commons Collections fix the problem?

Removing the dangerous method from one library only closes one door, and dozens of other libraries — Spring, Groovy, Hibernate, C3P0, and more — have supplied their own gadget chains over the years, each cataloged in ysoserial. The Apache Commons project addressed this by hardening InvokerTransformer in later Commons Collections releases so it's disabled by default (documented in Apache's own public statement on the widespread Commons Collections issue), but that's a mitigation for one gadget source, not a fix for the deserialization model. An application that deserializes untrusted data is exposed to every gadget-capable class present anywhere on its full transitive classpath, including libraries the development team never directly chose. That's what made the FoxGlove Security findings so alarming in 2015: WebLogic, WebSphere, and JBoss were all vulnerable via the same Commons Collections chain despite having entirely different codebases, simply because they all bundled the same vulnerable dependency and all exposed a network-reachable deserialization endpoint.

What is look-ahead deserialization filtering and how does it stop gadget chains?

Look-ahead (also called allow-list) deserialization filtering inspects each class an incoming byte stream is about to instantiate before that instantiation happens, and rejects anything not explicitly permitted. Oracle formalized this as JEP 290, delivered in Java 9 and backported to 8u121, 7u131, and 6u141, which added the java.io.ObjectInputFilter interface. A filter set on an ObjectInputStream can inspect the class name, array length, graph depth, and reference count of every object in the stream as it's being read, and reject the stream outright if anything falls outside an allowed set — turning the model from "block known-bad gadget classes" into "permit only known-good classes." Oracle's own serialization filtering documentation recommends configuring filters per-stream via ObjectInputFilter.Config.createFilter rather than relying solely on a single JVM-wide global filter, since a global filter must be broad enough to cover every deserialization call site in the JVM and can't reflect what a specific endpoint actually needs to accept.

What should teams do instead of trying to patch every gadget class?

The most durable fix is avoiding native Java serialization for untrusted input entirely, replacing it with a schema-constrained format like JSON or Protocol Buffers where the receiving code defines the exact shape of acceptable data up front rather than trusting the stream to describe itself. Where legacy systems still depend on Java serialization — often because a message queue, RMI interface, or session-replication mechanism was built around it years before JEP 290 existed — the practical mitigation is combining allow-list ObjectInputFilters at every deserialization entry point with dependency hygiene that tracks which gadget-capable libraries (Commons Collections, Spring, Groovy, and others cataloged by ysoserial) are actually reachable from code that calls readObject(). Treating "is Commons Collections on the classpath" as the risk question misses the point; the risk question is "does any code path let untrusted bytes reach a readObject() call while a gadget-capable class is present," which is a reachability problem as much as a patching problem, and one that a dependency inventory alone can't answer without knowing which deserialization call sites are actually attacker-reachable.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.