September 18, 2026
$9,000 for a Cookie That Shouldn’t Have Been an Object: Java Deserialization to RCE
I’ve stopped counting how many targets I’ve fingerprinted as Java without finding anything usable. This one almost joined that pile. What…

By T4nv1
6 min read
I've stopped counting how many targets I've fingerprinted as Java without finding anything usable. This one almost joined that pile. What changed my mind was a cookie value that looked wrong in a way I couldn't immediately explain, and the two hours I spent figuring out why turned into one of the better paydays I've had this year.
The target — I'll call it Halcyon here, per their program's anonymization requirement — is a logistics and supply-chain platform running a Spring-based backend. Nothing about the initial recon screamed Java specifically; it was a JSESSIONID cookie and a handful of X-Powered-By headers that pointed there, standard fingerprinting, nothing clever yet.
The Cookie That Didn't Look Like a Session Token
Most of my attention on this target went to a secondary cookie set alongside the session — one called rememberMe, present after checking the "keep me signed in" box at login. Session cookies are usually either an opaque random string or a signed JWT. This one was neither. Decoded from base64, it began with the bytes rO0AB.
That prefix matters more than it looks. rO0 decodes to the hex sequence AC ED 00 05 — the magic bytes that open every standard Java serialized object stream. Seeing that in a cookie is one of the more reliable "stop everything and look closer" signals in web app testing, because it tells you the application isn't just storing a token — it's storing an entire serialized Java object, and handing it back to the client with the expectation that the client will hand it back unmodified later. Which means the server is going to deserialize whatever comes back in that cookie on the next request. Whether that's dangerous depends entirely on what classes are available on the classpath at deserialization time — and by 2024, most Java web apps built on Spring carry at least a few libraries in their dependency tree with publicly known deserialization gadget chains, whether or not anyone building the app ever intended to expose that risk.
Why Deserialization Is Different From Every Other Injection Class
It's worth being precise about the mechanism here, because it's genuinely unlike SQL injection or XSS in an important way: you're not injecting a malicious value into an existing operation. You're supplying an entire object graph, and the deserialization process itself — reconstructing that graph, calling constructors, invoking readObject() methods as each object comes back to life — is the attack surface. If any class reachable from that process has a readObject(), finalize(), or similar method that does something dangerous with attacker-controlled fields (writes a file, runs a command, calls a scripting engine), an attacker doesn't need to exploit an application bug at all. They need to know that dangerous class is present somewhere in the dependency tree, and know how to chain several such classes together into something usable — a gadget chain.
This is exactly the kind of vulnerability that public tooling exists specifically to test, because building gadget chains by hand against an unknown classpath is impractical. I reached for a well-known open-source payload generator built for exactly this purpose, which ships pre-built gadget chains for the dependency combinations most commonly found in real-world Java applications — several variants targeting Commons Collections, Spring's own beans framework, Groovy, and a handful of others.
Confirming the Primitive, Blindly
I had no source access and no confirmation that any specific gadget chain would work against Halcyon's classpath. The responsible way to test this without risking an unstable payload landing badly on production is to start with a completely inert probe — a deserialization payload built purely to prove the chain executes at all, using an out-of-band network callback rather than anything that touches the filesystem or spawns a persistent process.
CommonsCollections6 gadget chain, payload command:
nslookup <unique-subdomain>.<my-collaborator-domain>CommonsCollections6 gadget chain, payload command:
nslookup <unique-subdomain>.<my-collaborator-domain>I generated the serialized payload with that command embedded, base64-encoded it, and replaced the rememberMe cookie value with it on a fresh authenticated request:
Cookie: JSESSIONID=<valid session>; rememberMe=rO0ABXNyAC...[gadget chain payload]...Cookie: JSESSIONID=<valid session>; rememberMe=rO0ABXNyAC...[gadget chain payload]...Then I waited on my DNS collaborator listener. If nothing came back, that particular gadget chain simply wasn't present on the classpath — a normal, unremarkable outcome that just meant trying the next one. On the third variant I tried, a DNS lookup landed within about four seconds of sending the request, originating from infrastructure that resolved back to Halcyon's hosting provider. That's about as clean a confirmation as blind RCE testing gets: a command I embedded in a serialized object executed on their server, and the only evidence I needed to collect was that a DNS query I controlled the destination for actually fired.
Why a DNS Callback Was Enough, and Why I Stopped There
I want to be direct about scope here, because deserialization RCE is one of the vulnerability classes where the gap between "proving it" and "actually compromising the server" is dangerously thin, and it's entirely up to the researcher to hold that line themselves.
A successful DNS callback from a command I embedded is unambiguous proof of arbitrary command execution — there's no more convincing or less convincing way to demonstrate it. I did not follow up with a reverse shell, did not attempt to read any files, did not try to establish persistence, and did not repeat the technique against any other endpoint or user session. I also didn't test how the same gadget chain might behave against other parts of the application that might share the same deserialization pattern, even though that's a plausible next step, because doing so meaningfully increases the risk of an unintended crash or side effect on infrastructure I don't own. One confirmed callback is exactly as convincing to a triager as ten would be, and it carries none of the risk.
Writing a Report Worth Escalating Immediately
Deserialization RCE reports get read fast once triage understands what they're looking at, but that also raises the bar for precision — vague or exaggerated claims in this category get picked apart quickly by anyone with real Java experience on the other end, and a shaky report can actually slow down a finding that deserves urgency.
Title: Insecure deserialization in rememberMe cookie handling allows unauthenticated-adjacent remote code execution via public gadget chain
Root cause: The application deserializes the raw value of the rememberMe cookie using Java's native object deserialization without any type filtering, allowlisting, or signature verification prior to reconstructing the object graph, and a class implementing a known-exploitable gadget chain is present on the application's classpath.
Reproduction: The exact gadget chain variant used, the full base64-encoded payload, the DNS collaborator interaction log with timestamps, and an explicit note on which gadget chain variants I tried that did not work, since that negative information is genuinely useful to whoever fixes this — it tells them roughly which dependency is the exploitable one without me needing filesystem access to confirm it directly.
Impact: Full remote code execution on the application server, exploitable by anyone able to set a cookie value on an authenticated or authentication-adjacent request, without needing any other vulnerability or prior foothold.
Fix recommendations, given in order of how quickly each can realistically ship:
- Immediately stop deserializing the
rememberMevalue as a raw Java object; if the underlying "remember me" library defaults to this behavior, most modern versions support switching to a signed, opaque token format that never triggers object deserialization on untrusted input at all - As a stopgap while a full fix is developed, implement a deserialization allowlist filter (Java's built-in
ObjectInputFilter, available since Java 9, or an equivalent library-level filter) that rejects any class not explicitly expected during this deserialization step - Audit the full dependency tree for known-exploitable gadget chain libraries and update or remove unused ones, since the vulnerable class doesn't need to be used anywhere in the application's own code to be dangerous — it only needs to be present on the classpath
- Treat this as a pattern to search for platform-wide, not just in this one cookie, since any other feature that deserializes client-supplied data the same way carries the identical risk
What Happened After
Halcyon's security team escalated this within hours of triage confirming it, which is what you'd expect from a finding in this category. They shipped an emergency stopgap — switching the deserialization filter to reject anything outside an explicit allowlist — within 48 hours, followed by a proper migration of the "remember me" mechanism to a signed opaque token over the following few weeks. The report closed at $9,000, rated critical, and their write-back mentioned an organization-wide audit of every other feature using native Java serialization for anything touching client input, which is exactly the scope this kind of finding deserves — a single vulnerable cookie was never really the whole story, it was just the one instance that happened to be reachable from outside.
What This Bug Class Rewards
Deserialization vulnerabilities don't reward clever payload crafting so much as they reward pattern recognition and patience with tooling that does the heavy lifting for you. A few things worth carrying forward if you want to hunt these seriously:
Learn to recognize the magic bytes. rO0 in base64, or AC ED 00 05 in raw hex, appearing anywhere a client can influence — cookies, hidden form fields, API request bodies, even file uploads — is worth decoding and investigating every single time, regardless of how unlikely the context seems.
Start blind, with out-of-band confirmation, before you ever consider anything more invasive. The DNS callback technique used here generalizes to almost any suspected RCE primitive and lets you prove impact with zero risk of actually damaging anything.
Treat gadget-chain tooling as a starting point, not a guarantee. Most real-world classpaths won't match any pre-built chain, and that's a normal, expected outcome — the value of trying several isn't that one is guaranteed to work, it's that testing costs you almost nothing when it doesn't.
Report negative results alongside positive ones. Telling a security team which gadget chains you tried that didn't fire is often more useful to their remediation than they'd expect, and it demonstrates a level of care that tends to earn faster trust from triage teams on future reports.
What I keep coming back to with this one is how little of the actual vulnerability lived in code anyone at Halcyon wrote. The dangerous part was a dependency, sitting quietly in a classpath, doing nothing wrong on its own — right up until a cookie handed it exactly the object graph it needed to misbehave.