The /api/v1/dependencies endpoint at RubyGems returned Ruby objects serialized with Marshal instead of JSON. A standard HTTP scanner would never flag that behavior. Report H1 #1119120 demonstrated the endpoint was exploitable for RCE on developer machines.
Deserialization vulnerabilities in APIs are systematically overlooked in black-box security reviews. Not because they are rare: 40+ critical CVEs across 6 ecosystems over 10 years prove otherwise. The problem is structural. Every mainstream guide starts from a known vulnerable endpoint. Format fingerprinting at the HTTP layer closes that gap without requiring source code access.
The Detection Gap Every Guide Skips
The 13 PortSwigger deserialization labs all start the same way: "locate the serialized cookie in this request." No lab teaches how to find the endpoint. HackTricks assumes you have already identified the vulnerable parameter before generating the first payload.
The OWASP API Security Top 10 2023 classifies insecure deserialization under API8 (Security Misconfiguration), without providing any detection guidance. Snyk operates entirely through SAST analysis: useless when you have no source code access. The gap is structural: none of the five leading security resources on the topic offers a discovery methodology for black-box engagements.
The result shows up in HackerOne reports themselves. Several explicitly mention that the vulnerable endpoint was "discovered accidentally while testing another component." That is not luck: it is the absence of a detection methodology.
Why APIs Are More Exposed Than Web Apps
Web apps expose serialization in visible channels: cookies, hidden form fields, query parameters. A developer inspecting browser traffic will notice a cookie prefixed with rO0. APIs do the opposite.
APIs route serialized data through content negotiation, custom media types, and machine-to-machine channels that standard HTTP scanners treat as legitimate opaque traffic. There is no human inspection layer. Binary format raises no visual alerts.
H1 #1119120 illustrates the pattern precisely: the /api/v1/dependencies endpoint at RubyGems returned Marshal.dump instead of JSON. Bundler, the official RubyGems client, called Marshal.load on the response without sanitization. An attacker controlling a malicious gem server ran arbitrary code on every developer machine that ran bundle install.
H1 #2127968 (CVE-2023-40195) followed the same pattern in the Apache Airflow Spark Provider. The attacker configured the address of a malicious Spark server through the Airflow API configuration interface. The Airflow client deserialized the response from that server, reaching RCE on the host. H1 #221294 confirms that Starbucks suffered Java deserialization via RCE on a JBoss endpoint at card.starbucks.in, with no web form involved. In every case: M2M channels chosen for efficiency, not security.
Format Fingerprinting: 5 Signatures That Expose Deserialization in HTTP
Five signatures identify serialized payloads in HTTP traffic before any exploit payload is generated. All are detectable with standard reconnaissance tools.
Java ObjectInputStream: magic bytes AC ED 00 05 in binary; prefix rO0 in Base64. The Content-Type: application/x-java-serialized-object header confirms the format, but its absence does not rule it out. Many Java endpoints omit the header even when returning serialized objects.
PHP unserialize(): prefixes O:, a:, s: in request parameters and response bodies. O: indicates a serialized object; a: indicates an array. These prefixes appear literally in the HTTP request or response text.
Python pickle: bytes \x80\x04 or \x80\x05 in protocols 4 and 5. Job queues and API responses returning complex Python objects are the most common vectors in this case.
Ruby Marshal: header \x04\x08 in any binary response body from a Ruby API. That fingerprint would be enough to identify the endpoint as a candidate for exploitation.
GadgetProbe: DNS callback technique for Java classpath enumeration. GadgetProbe sends payloads that trigger DNS resolution only if a target class is loaded in the classpath. This reveals which gadget libraries are present without triggering RCE, allowing you to select the correct gadget chain before generating any exploit.
Where to look: binary request bodies, Base64-encoded cookies, responses with Content-Type: application/octet-stream. A simple grep on response bodies during reconnaissance covers most cases:
# Detecta assinatura Java Base64 em respostas capturadas
grep -rl 'rO0' ./burp-export/
# Detecta bytes mágicos Java em binários
xxd response.bin | grep 'aced 0005'
# Detecta serialização PHP em parâmetros
grep -E 'O:[0-9]+:|a:[0-9]+:|s:[0-9]+:' ./responses/
Gadget Chains Explain Why Patching a CVE Changes Nothing
A gadget chain is a sequence of existing classes that an attacker links through deserialization to reach code execution. No new code is injected: the classes are already in the application classpath.
A study published at ICSE 2023 (arxiv:2303.07593) analyzed 86 publicly documented Java gadget chains. The GCMiner tool proposed by the study found 56 additional unique chains that earlier tools did not detect. Each chain represents a path that exists whenever the corresponding library is in the classpath, independent of any patch applied to the application.
Oracle WebLogic sustained a continuous stream of Java deserialization CVEs for more than five years. Each patch closed one gadget path and exposed another. ysoserial was released in 2015 and remains effective because the root problem is ObjectInputStream itself, not any specific gadget that can be removed.
CVE-2025-49113 confirms the pattern persists across languages and decades. Roundcube received CVSS 9.9 in June 2025 for PHP object deserialization in the _from parameter of upload.php. The attack class was formally documented more than 10 years ago. Patching a CVE removes a known payload sequence. It does not remove gadget chains from the classpath.
That makes the fingerprinting methodology permanently relevant: no patch closes all gadget chains as long as ObjectInputStream exists.
Three HackerOne Reports That Prove the API Pattern
H1 #1119120 (RubyGems) is the most straightforward case: an API endpoint returning Marshal.dump instead of JSON, with the official client calling Marshal.load without sanitization. Format fingerprinting would have identified the endpoint without any source code access.
H1 #2127968 (Apache Airflow Spark, CVE-2023-40195, 2023) demonstrates the vector through API configuration. The attacker sets a malicious Spark server address through the Airflow API interface. The Airflow client deserializes the response from that server and executes code on the host. The endpoint was authenticated and treated as implicitly trusted.
H1 #2334460 (Apache Airflow XComs, 2024) shows that configuration controls are insufficient. The enable_xcom_pickling=False flag did not prevent deserialization of XCom data delivered through the API. The trust assumption in the API channel bypassed the configuration control. The pattern across all three reports is identical: M2M channel treated as internally trusted, payload validation skipped.
A Black-Box Detection Workflow in 30 Minutes
Step 1: catalog endpoints and Content-Type headers. Flag any non-standard media type: application/x-java-serialized-object, application/octet-stream, application/x-msgpack. These headers indicate the server does not assume the client is a browser.
Step 2: inspect binary bodies for magic bytes. Run grep on request and response bodies looking for rO0, AC ED, \x04\x08, \x80\x04. Base64 blobs starting with rO0 or /rO0 confirm Java ObjectInputStream.
Step 3: GadgetProbe on suspected Java endpoints. Set up DNS callback infrastructure through Burp Collaborator or interactsh. Run GadgetProbe against the endpoint to enumerate which gadget libraries are present in the classpath without triggering RCE.
Step 4: generate payloads matching the confirmed format. ysoserial for Java, phpggc for PHP, pickletools for Python. Validate in a controlled environment before sending against the target.
Format fingerprinting has limitations: channels with mutual TLS make passive inspection impractical without access to internal proxies, WAFs with deep inspection may normalize binary payloads before they reach the endpoint, and applications that serialize only in internal responses do not appear in external scans. In those situations, GadgetProbe with DNS callbacks via interactsh replaces passive inspection as the initial step.
intel.mago.team (disclosure: editorial team tool) automates Steps 1 and 2 across the full API surface mapped during reconnaissance. Format fingerprinting runs on every API security scan, identifying deserialization candidates without requiring manual review of each endpoint.
The endpoint returning rO0 appears in the documentation as "internal use only." Fingerprinting finds what the documentation does not list.
Top comments (0)