Detecting that an endpoint accepts serialized objects is reachability - confirming a gadget chain without running a real command is where severity is decided.
Deserialization bugs earn their critical rating because the dangerous work happens before type-checking finishes. A gadget chain does not need any application-specific code - it stitches together methods already sitting on the classpath, invoked as the object is reconstructed. Detecting that an endpoint accepts serialized input is reachability. Proving the chain assembles, without ever running a real command, is where the severity is actually decided.
Safe code validates a value and then acts on it. A vulnerable deserializer acts while rebuilding the object graph - calling readObject, property setters and side-effecting constructors - and only afterward discovers it built something it should not have. By then the gadget has already fired. That inversion is why the class is critical rather than merely risky.
The oracle discipline is the same as everywhere else: prove capability, run nothing dangerous. Instead of a real command or a spawned subprocess, the confirming payload uses a benign out-of-band marker - a unique, attributable callback that fires only if the sink was reached. It demonstrates the chain assembled and executed a harmless lookup, not a shell. Out-of-band confirmation is off by default and used only with authorization.
# gadget chain wired to a HARMLESS out-of-band lookup, not a command sink: reflection gadget on the app's classpath action: resolve ap-62615533.oast.probe # unique, benign, attributable # confirmation, entirely out of band: callback log ──► DNS/HTTP hit for ap-62615533.oast.probe # the chain assembled and reached execution. No shell, no file, no command.
Java's magic bytes (0xAC 0xED 0x00 0x05) mark a standard serialized object, but that is one format among many. Jackson with default typing, Kryo, Python pickle, PHP serialize and Ruby Marshal each present a different fingerprint and a different sink, so a single-format detector reports clean on most real deployments.
| Format | Fingerprint | Typical sink |
|---|---|---|
| Java native | 0xACED0005 magic bytes | readObject on the classpath |
| Jackson | JSON with type hints / default typing | Polymorphic type resolution |
| Python pickle | Opcode stream (proto markers) | __reduce__ during load |
| PHP | O:len:"Class" serialize format | __wakeup / __destruct |
| Ruby | Marshal version prefix | Object reconstruction hooks |
Gadgets are never fully enumerable, so chasing individual chains is a losing game. The durable fix constrains the deserializer itself: Java 9+ serialization filters (JEP 290, ObjectInputFilter) allowlist expected types before a chain can assemble; Jackson should disable default typing and use explicit types; pickle and Marshal should not touch untrusted input at all.
Prove capability with a benign callback, rate it on the sink, and remediate at the one place that can actually hold the line - the type filter - not the endless list of gadgets downstream of it.