Gadget chain discovery is the part of deserialization research that sounds like magic and is really just disciplined search. Nobody stumbles onto a working chain by reading one file. They start from a method that does something dangerous and reason backward until they reach a point an attacker can actually trigger. This post explains how that backward search works, why it is a source to sink reachability problem over the whole classpath, and why it is so hard to do by hand.
Gadget chain discovery starts at the sink, not the input
The intuitive way to look for a bug is to start at user input and follow it forward. For gadget chains that is the slow direction, because the input reaches a generic deserialize call that could build almost anything. Effective gadget chain discovery runs the other way. You begin at a sink, a method that runs a command, loads a class by name, writes a file, or makes a network call, and you ask a single question: what calls this, and can any of those callers be reached from a method that fires automatically during deserialization?
The sink is the anchor because there are relatively few truly dangerous methods, while there are countless harmless ones. Starting from the scarce end of the problem keeps the search small.
Finding a chain is not reading code forward from input. It is standing at the dangerous method and walking backward until you reach a door an attacker can open.
Walking backward from the sink
Once you have a sink, the search is a sequence of backward steps, each one a call edge.
- Find the sink. A method such as one that executes a command string or instantiates a class from a name.
- Find its callers. Which methods call the sink, and can they pass attacker influenced arguments to it?
- Keep climbing. For each caller, find its callers, always keeping the link that an attacker controlled field decides the next call.
- Reach an entry method. Stop when you arrive at a method the runtime invokes by itself during deserialization, like the hooks in magic methods in deserialization attacks.
If that backward path connects an automatic entry method to a sink, and each hop passes control through a field the attacker can set, you have a candidate chain. The handoff through fields is what makes it property oriented programming rather than ordinary call tracing.
Why this is a classpath wide reachability problem
The callers you are climbing through are not in your code. They are in libraries, and in libraries those libraries depend on. A chain can start in one package, pass through a second, and end in a third, with no single developer ever having seen all three together. So the real search space is every class loaded at runtime, not the application source. This is the same point the hub makes about deserialization gadget chains: the gadgets live on the classpath, so discovery has to reason over the whole classpath.
Conceptually you are building a call graph and asking a reachability question on it:
sink: Runtime.exec(cmd)
<- Transformer.transform(input) // passes input to exec
<- LazyMap.get(key) // triggers transform
<- AnnotationHandler.invoke() // calls get on a controlled map
<- readObject() // automatic entry point
// if a path exists from readObject() down to exec(), a chain exists
That is the mechanics behind well known Java chains, including the Commons Collections gadget chain. The researcher did not invent new classes. They found an existing path from an automatic method to a dangerous one.
Why it is hard by hand
Three things make manual gadget chain discovery slow.
- Scale. A medium app loads thousands of classes. Reading every method to find the ones that call a sink is not realistic.
- Indirection. The call from a gadget to the next is usually through an interface or a field whose real type is decided by the attacker. A plain reader cannot tell which concrete method runs without modeling what that field can point to.
- Version drift. A chain that works in one library version breaks in the next when a method is renamed or a field removed. Discovery has to be redone against the exact versions shipped.
A text search finds the word exec. It cannot tell you whether any automatic entry method can reach that exec through attacker controlled fields, which is the only thing that matters. That gap, between finding a pattern and proving a path, is a theme across our injection and input category and in the primer on insecure deserialization.
What discovery actually produces
A real result is not a warning that a deserializer exists. It is a concrete path: this automatic entry method, through these intermediate calls, passing this attacker set field at each step, reaches this dangerous sink. That path is what lets you prove the bug instead of guessing at it, and it is what lets you write a precise fix, whether that is an allowlist at the deserializer or removing the library that supplies a link gadget.
Where an autonomous researcher fits
This backward, classpath wide reachability search is close to the core of what UnboundCompute does. It learns how an application fits together, reasons from a dangerous sink back toward the untrusted input that could reach it, and only reports a finding once it has proven the path with concrete evidence rather than a pattern match. An untrusted input to deserializer flow, traced end to end and verified, is exactly the kind of bug that reasoning is built to surface. In early testing, a frontier model drove that full methodology on its own and identified and verified real injection and access control issues in test applications it had not seen before, which we read as an encouraging early signal rather than a benchmark.
Gadget chain discovery rewards understanding over scanning, because the answer is a path, not a keyword. You can read more about how we approach that kind of reasoning on our about page.
Frequently asked questions
How is gadget chain discovery actually done?
You start at a dangerous sink, a method that runs a command, loads a class, or writes a file, and reason backward through its callers. The search stops when you reach a method the runtime invokes automatically during deserialization, with attacker controlled fields steering each hop.
Why start at the sink instead of the user input?
Input reaches a generic deserialize call that could build almost anything, so following it forward explodes the search. There are few truly dangerous sinks and many harmless methods, so starting from the scarce end keeps the backward search small and focused.
Why is gadget chain discovery a classpath wide problem?
The intermediate gadgets live in libraries and their dependencies, not your own source. A chain can cross several packages that no single developer ever saw together, so discovery has to reason over every class loaded at runtime, not just the application code.
Why is finding a chain hard to do by hand?
Scale, because an app loads thousands of classes; indirection, because each gadget calls the next through a field whose real type the attacker chooses; and version drift, because a chain breaks when a library changes. A text search finds a keyword but cannot prove a reachable path.
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.
