Deserialization attacks happen when an application rebuilds objects from attacker-controlled serialized data.
The primitive is object reconstruction. The application expects harmless saved state, but the deserializer may create objects, call methods, resolve types, load classes, or execute special hooks as part of rebuilding that state.
Serialization is not dangerous by itself. The danger appears when untrusted data is treated as a trusted object graph.
Core Idea
Serialization turns an object into bytes or text.
Deserialization turns those bytes or text back into an object.
object -> serialized data -> object
Common formats include:
JSON
YAML
XML
pickle
Java serialization
.NET BinaryFormatter
PHP serialize
Ruby Marshal
Node.js serialized objects
Some formats are data-only when used safely. JSON is usually data-only. Other formats can carry type information and object construction behavior. Those are much more dangerous.
The key question is:
Does this parser only rebuild data, or can it rebuild behavior?
Why It Becomes Dangerous
Many languages support special methods that run during serialization or deserialization.
Examples:
Python pickle: __reduce__, __setstate__
PHP: __wakeup, __destruct
Java: readObject, readResolve
.NET: ISerializable, ObjectDataProvider gadget chains
Ruby: marshal_load
If attacker-controlled serialized data can choose a dangerous type or gadget chain, deserialization can lead to:
- File read
- File write
- Authentication bypass
- Object injection
- Server-side request forgery
- Command execution
- Privilege escalation
- Application state corruption
Not every deserialization bug is command execution. Sometimes the impact is changing an object field, changing a role, skipping validation, or forcing the application into an unsafe state.
Sources And Sinks
Common sources:
- Local cache files
- Session files
- Remember-me cookies
- Job queue messages
- Message broker payloads
- RPC parameters
- Import/export files
- Plugin manifests
- Saved workflow state
- Desktop application project files
- Backup/restore archives
Common dangerous sinks:
pickle.loads(data)
yaml.load(data)
new ObjectInputStream(input).readObject()
new BinaryFormatter().Deserialize(stream)
unserialize($_COOKIE["state"])
Safe-looking code can still be dangerous if the input is attacker-controlled.
Data Format vs Object Format
Data formats describe values.
{"username":"operator","role":"user"}
Object formats may describe types and reconstruction behavior.
create this class
call this reconstruction method
set these fields
resolve this reference
For untrusted input, prefer data formats with explicit schema validation.
Good boundary:
untrusted bytes -> parse as simple data -> validate schema -> construct safe application object manually
Bad boundary:
untrusted bytes -> deserialize directly into live objects
Gadget Chains
A gadget is an existing class or method that performs useful behavior during deserialization.
An exploit usually does not need to inject new code. It uses classes already present in the application or its dependencies.
The pattern is:
attacker-controlled serialized object
-> deserializer rebuilds object graph
-> special method or property access runs
-> existing class performs dangerous behavior
This is why dependency versions matter. Adding a library can add new gadget chains even if application code did not change.
Common tooling:
ysoserial Java gadget payloads
ysoserial.net .NET gadget payloads
phpggc PHP gadget payloads
marshalsec Java deserialization research tooling
Tools are useful, but the concept is more important: untrusted object graphs are the problem.
Platform Notes
Python
pickle is not safe for untrusted data.
Dangerous:
pickle.loads(user_controlled_bytes)
Safer:
json.loads(user_controlled_text)
validate schema
construct object manually
Java
Java native serialization has a long history of gadget-chain attacks.
Dangerous:
ObjectInputStream.readObject()
Risk increases when large dependency sets are present, especially old enterprise libraries.
Use serialization filters, avoid native Java serialization for untrusted data, and prefer explicit data formats.
.NET
BinaryFormatter is unsafe for untrusted data and should not be used.
Dangerous:
BinaryFormatter.Deserialize(stream)
Legacy applications may also expose ViewState, LosFormatter, NetDataContractSerializer, or other serialization surfaces. The exact exploit path depends on framework, signing, encryption, machine keys, and available gadget chains.
PHP
unserialize() on untrusted input can create PHP object injection.
Dangerous:
unserialize($_COOKIE["state"])
Impact depends on available classes and magic methods such as __wakeup() and __destruct().
YAML
YAML can be safe or unsafe depending on the loader.
Dangerous:
yaml.load(data)
Safer:
yaml.safe_load(data)
Enumeration
Look for:
- Magic bytes or format markers
- Base64 blobs in cookies or files
- Java serialized streams beginning with
ac ed 00 05 - Python pickle opcodes or base64-encoded pickle blobs
- PHP serialized strings such as
O:,a:,s:,i: - .NET ViewState fields such as
__VIEWSTATE - BinaryFormatter payloads in files, messages, or request bodies
- Functions named
deserialize,unserialize,loads,load,readObject,BinaryFormatter,ObjectInputStream
File and traffic clues:
.ser
.pickle
.pkl
.bin
.dat
.session
.state
.viewstate
Application clues:
- Import/export features
- Saved project files
- Local cache files
- Remember-me functionality
- Message queues
- Background jobs
- RPC services
- Plugin systems
Exploit Workflow
Use this workflow:
- Identify the serialized input.
- Identify the deserializer.
- Determine whether the format is data-only or object-capable.
- Confirm that attacker-controlled data reaches the sink.
- Prove a harmless state change or local command in a lab.
- Identify available gadget chains for the target language and dependencies.
- Keep the proof scoped to the authorization boundary.
For local labs, use harmless commands such as:
id
whoami
hostname
The goal is to prove the primitive, not to build malware.
Remediation
Patch deserialization bugs by removing trust from the object graph:
- Do not deserialize untrusted data with object-capable deserializers.
- Prefer JSON or another data-only format with schema validation.
- Manually construct application objects from validated data.
- Use allowlists for expected types when object deserialization is unavoidable.
- Enable platform-level serialization filters.
- Sign and authenticate serialized data when it must cross trust boundaries.
- Do not treat signing as a replacement for safe parsing.
- Keep dependencies patched to reduce gadget availability.
- Avoid storing sensitive authorization state only client-side.
- Log deserialization failures and unexpected type names.
Safe pattern:
data = json.loads(untrusted_text)
if not isinstance(data, dict):
raise ValueError("invalid state")
username = data.get("username")
role = data.get("role")
if role not in {"user", "operator"}:
raise ValueError("invalid role")
state = SessionState(username=username, role=role)
The parser reads data. The application decides which object to create.