Operator On The Wire
← Back to Knowledge Base
OOTW / Chapter II - Local / 01. Applications / 02. Deserialization Attacks

Deserialization Attacks

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:

  1. Identify the serialized input.
  2. Identify the deserializer.
  3. Determine whether the format is data-only or object-capable.
  4. Confirm that attacker-controlled data reaches the sink.
  5. Prove a harmless state change or local command in a lab.
  6. Identify available gadget chains for the target language and dependencies.
  7. 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.