The Commons Collections gadget chain explained

The Commons Collections gadget chain explained

Written by

in

The commons collections gadget chain is the most famous example of how insecure deserialization turns into remote code execution. The trick is not a bug in your code. It is a set of ordinary classes that already sit on your classpath, wired together so that rebuilding one attacker supplied object graph ends in a command running on your server. This post explains the shape of the chain, why those classes are dangerous in combination, and how to shut it down.

Why the commons collections gadget chain matters

Apache Commons Collections is a widely used utility library. Plenty of Java services pull it in without ever using the classes at the heart of this attack. That is the core lesson. A gadget chain does not need a vulnerable method you wrote. It needs a deserializer that accepts untrusted bytes and a collection of reusable classes whose normal behavior can be strung into something harmful.

If you are new to the idea of rebuilding objects from bytes, start with our primer on what insecure deserialization is, then read the cluster hub on what a deserialization gadget chain is. This post is the concrete case those two describe in the abstract.

The ingredients already on the classpath

The classic chain leans on a few building blocks that Commons Collections ships. The names matter less than what they do.

  • Transformer is an interface with one job: take an object in, return an object out.
  • ConstantTransformer ignores its input and always returns a fixed object you chose.
  • InvokerTransformer calls a method by name, with arguments you supply, through reflection.
  • ChainedTransformer runs a list of transformers in order, feeding each output into the next.

On their own these are harmless helpers. Reflection is a normal part of Java. Calling a method by name is a feature, not a flaw. The problem starts when an attacker gets to build the list.

How the pieces chain into command execution

Because InvokerTransformer calls any method by name, a list of transformers can walk from a harmless starting object all the way to Runtime.exec. Conceptually the chain looks like this:

// Conceptual shape, not a working payload
ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getMethod",
        new Class[]{ String.class, Class[].class },
        new Object[]{ "getRuntime", new Class[0] }),
    new InvokerTransformer("invoke",
        new Class[]{ Object.class, Object[].class },
        new Object[]{ null, new Object[0] }),
    new InvokerTransformer("exec",
        new Class[]{ String.class },
        new Object[]{ "command here" })
});

Read it top to bottom. Start with the Runtime class. Use reflection to fetch its getRuntime method. Invoke that method to get the live runtime object. Then call exec on it. Each step is a normal reflective call. Together they reach a command.

The missing trigger: who calls the chain

A chain that never runs is just data. The second half of the attack is finding a class whose own deserialization quietly invokes the transformer. Some map and collection types apply a transformer to their keys or values when they are rebuilt or when a key is read. An attacker wraps the chain inside one of those, so the act of deserializing the object graph fires the chain with no help from your code.

You did not write a single line that executes an attacker command. The library did, on your behalf, the moment your app called readObject on bytes it should never have trusted.

This is why the class is called a gadget chain. Each gadget is a real, shipped method with a legitimate purpose. The attacker only decides the order. The entry point is any place your app reads a serialized Java object from a source it does not control: a request body, a cache entry, a message queue, a cookie, a file upload. For the mechanics of arranging gadgets like this, see pop chains and property oriented programming, and for how researchers locate a usable sequence, see finding deserialization gadget chains.

The same pattern shows up elsewhere

Commons Collections is the headline example, but the idea generalizes to any deserializer that reconstructs arbitrary types from untrusted input. A YAML loader can do it through type tags, which we cover in the SnakeYAML deserialization writeup. An XML deserializer can do it through type coercion, covered in the XStream deserialization post. And an RMI endpoint hands an attacker a remote, unauthenticated way to deliver any of these chains, covered in Java RMI and JRMP deserialization. The gadgets differ. The root cause is the same: untrusted bytes become live objects with no limit on which classes get built.

How to stop it

You cannot remove reflection from Java, and you often cannot remove Commons Collections from a large dependency tree. So defend the entry point, not the gadgets.

  • Do not deserialize untrusted input with native Java serialization. If you control both ends, prefer a data format like JSON with explicit, typed parsing and no automatic type resolution.
  • Add a serialization filter. Modern Java supports an allowlist of classes that are permitted to deserialize through ObjectInputFilter. Set it to accept only the handful of types your app actually expects, and reject everything else.
  • Remove the reachable trigger where you can. If you do not need native deserialization on a given endpoint, delete the code path entirely. An endpoint that never calls readObject on attacker bytes has no chain to fire.
  • Keep dependencies current and minimal. Fewer libraries mean fewer available gadgets, and patched versions remove some known trigger classes.

The allowlist is the strongest single control because it attacks the real problem. The danger was never the gadgets. It was letting untrusted bytes decide which classes to build. An allowlist takes that decision back.

Where this sits in the broader picture

This is an injection class bug at heart: untrusted input flows into a sink that was never meant to receive it. For related reading on how input reaches dangerous operations, browse our injection and input category.

Finding one of these before an attacker does means tracing untrusted bytes from the edge of the app all the way to a readObject call, across libraries you did not write. That source to sink reasoning about untrusted input reaching a deserializer is exactly what UnboundCompute is built to do. Read more on our about page.

Frequently asked questions

What is the Commons Collections gadget chain?

It is a sequence of ordinary Apache Commons Collections classes, such as ChainedTransformer and InvokerTransformer, that an attacker orders so that deserializing one crafted object graph ends in a command running on the server. No vulnerable method in your own code is needed, only a deserializer that accepts untrusted bytes and the library on the classpath.

Do I have to use Commons Collections in my code to be at risk?

No. The chain relies on classes that ship with the library, so any service that has Commons Collections on its classpath and deserializes untrusted Java objects can be exposed, even if your own code never references those classes directly.

How does the chain actually reach command execution?

InvokerTransformer calls any method by name through reflection. A list of transformers walks from the Runtime class, fetches getRuntime, invokes it to get the live runtime object, then calls exec. A map or collection type that applies the transformer during deserialization fires the whole sequence automatically.

How do I prevent the Commons Collections gadget chain?

Stop deserializing untrusted input with native Java serialization, add an ObjectInputFilter allowlist that accepts only the few classes your app expects, remove deserialization code paths you do not need, and keep dependencies minimal and current. The allowlist is the strongest control because it stops untrusted bytes from choosing which classes get built.


Put an autonomous researcher on your own systems

UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, and a say in what it looks for. If your team ships software worth pressure testing, apply to the design partner program.