Step 196. Deserialization Vulnerabilities — The Moment Data Becomes Code

Step 196. Deserialization Vulnerabilities — The Moment Data Becomes Code

Level 3 — Real-World CTF & Advanced Attack Skills | Difficulty ★★★★☆ | Estimated time: 3 hours

Prerequisites: you’ve finished Step 195 (XXE). You know basic Python class and method syntax.

  • What you need: Python 3 (uses only the standard library pickle and os — nothing to install).
  • ⚠️ All exercises in this chapter are for your own lab and legal platforms only. Applying them to unauthorized systems is a crime.
  • Legal practice grounds: today’s pickle experiments run only harmless commands (writing one text file) inside your working folder. The PHP/Java examples proceed as concept explanations and screen examples.

For a program to save an object to a file or send it over the network, it must flatten the in-memory object into a sequence of bytes. This is serialization. Reassembling the object from bytes is deserialization. The problem is that "reassembling" is not a simple data copy. In many serialization formats, assembly comes with code execution — because special methods on the object get invoked automatically.

Today you’ll reproduce this principle right before your eyes with Python’s pickle. The moment a single pickle.loads() line runs, you’ll watch a command hidden inside data fire — proven by "a file that didn’t exist coming into existence." Then from PHP’s O: strings to Java’s rO0 signature, you’ll organize the shared structure of this attack, whose skeleton is the same across languages even when the surface differs.


1. Learning Objectives

By the end of this chapter, you will be able to:

  • Explain what serialization and deserialization are, and why deserialization leads to code execution
  • Measure how a malicious object with __reduce__ fires during pickle.loads
  • Distinguish the signatures of PHP (O:4:"User"...), Java (rO0AB...), and pickle (x80x04)
  • Explain the concepts of magic methods (__destruct, readObject) and gadget chains
  • Apply the correct defense against deserialization vulnerabilities ("never loads untrusted data")

2. Background Knowledge — Today’s Tools and Concepts

Today’s Tools at a Glance

Category Details
Language/environment Python 3 standard library (pickle) — local lab; PHP/Java as screen examples
Today’s commands pickle.dumps(), pickle.loads(), __reduce__(), reading Base64 signatures
Concepts needed Serialization/deserialization, magic methods, gadget chains, object injection
Today’s deliverables pickle code-execution reproduction script + per-language signature table + defense principles

2-1. Serialization — Flattening Objects into Bytes

An in-memory object is a structure of tangled variables and references. To write it to a file or send it over a network, you must flatten it into a one-dimensional sequence of bytes. Python has pickle, PHP has serialize(), Java has ObjectOutputStream for this job. Cookies, sessions, caches, message queues — wherever an object crosses a boundary, serialization is there.

Deserialization is the reverse. But "reassembling an object" is not merely filling in values. Written into the data itself are which class of object to create and which restoration procedure to follow. This is the attack’s door.

2-2. Why Data Becomes Code — Magic Methods

During deserialization, special methods fixed by each language run automatically.

  • Python pickle: __reduce__ defines "how to assemble this object," and if you write a function and arguments there, that function gets called during assembly
  • PHP: unserialize() creates the object, and at script shutdown __destruct() is invoked automatically. If a class has file-deletion code in this method, merely assembling an object of that class deletes the file
  • Java: readObject() is called during deserialization, and known paths exist where methods of library-provided classes chain like dominoes, finally reaching command execution

These dominoes are called a gadget chain. The attacker plants no new code; they weave methods of classes already on the server into an "assembly order" and turn that into a weapon. Java’s ysoserial tool is a famous collection of gadget chains — the library combination itself becomes the weapon.

2-3. Signatures — Recognizing the Format from the Data

Attackers and defenders both first judge "is this data a serialized object?" Each format looks different.

Format Signature Example shape
PHP serialize O:number:"ClassName" O:4:"User":2:{s:4:"name";...}
Java serialization (Base64) starts with rO0 rO0ABXNy... (raw byte magic: AC ED)
Python pickle protocol bytes like x80x04 b'x80x04x95...'

If you spot such shapes in an HTTP cookie or parameter, that server is deserializing somewhere.

2-4. The Only Defense Principle

Defending deserialization vulnerabilities differs in grain from other web vulnerabilities. It cannot be blocked by filtering. There’s one principle: never deserialize untrusted data. Use a pure data format like JSON instead of pickle, and if you must exchange objects, verify integrity with a signature (HMAC) before assembling. Python’s official documentation warns about pickle in exactly these terms: "never unpickle data from an untrusted source."


3. Follow Along

3-1. Normal Serialization — Data’s Round Trip

Create step196_pickle.py and run the first part.

import pickle

data = {"user": "gildong", "level": 3}
blob = pickle.dumps(data)
print("Serialized result (first 60 bytes):", blob[:60])
print("Deserialized:", pickle.loads(blob))

Output (measured 2026-09-09, Python 3.12):

Serialized result (first 60 bytes): b'x80x04x95 x00x00x00x00x00x00x00}x94(x8cx04userx94x8cx07gildongx94x8cx05levelx94Kx03u.'
Deserialized: {'user': 'gildong', 'level': 3}

How to read it: the leading x80x04 is pickle protocol 4’s signature byte. You can see strings like user and gildong inside the byte sequence. So far, it’s a pure data round trip.

3-2. The Malicious Object — The Moment loads Executes a Command

Now build a class with __reduce__. This method returns a blueprint saying "assemble me like this" — a (function, arguments) pair. And when pickle assembles, it actually calls that function.

import os

class Evil:
    def __reduce__(self):
        # Code to run upon deserialization. Nothing but a harmless file write.
        return (os.system, ("echo PWNED_by_pickle > step196_pwned.txt",))

evil_blob = pickle.dumps(Evil())
print("Malicious payload length:", len(evil_blob), "bytes")
print("File exists (before loads):", os.path.exists("step196_pwned.txt"))

pickle.loads(evil_blob)  # the command executes in this single line
print("File exists (after loads):", os.path.exists("step196_pwned.txt"))
print("File contents:", open("step196_pwned.txt").read().strip())

Output (measured 2026-09-09):

Malicious payload length: 75 bytes
File exists (before loads): False
File exists (after loads): True
File contents: PWNED_by_pickle

How to read it: this is the chapter’s key scene. A file that didn’t exist before pickle.loads() exists after it. What we invoked was "read data," yet the blueprint inside the data (__reduce__) called os.system. A 75-byte piece of data was command execution. In the field, that spot holds a reverse shell or file deletion instead of echo.

Why: by pickle’s design, "object assembly = code execution." The warning Python’s documentation attaches to this module has just been measured live. The defense isn’t a filter — it’s "don’t use it."

3-3. PHP’s Shape — Reading a serialize String (Screen Example)

PHP’s serialization output is a human-readable string. Screen example (for conceptual explanation):

O:4:"User":2:{s:4:"name";s:7:"gildong";s:5:"admin";b:1;}

How to read it: O:4:"User" means "an object of class User (4 letters)," :2: means "2 properties," s:4:"name" means "string of 4 letters, name," and b:1 means "boolean true." The attacker rewrites this string’s class name and property values to make the server assemble an object of a different class that exists there. If that class’s __destruct() contains dangerous code, it fires when the object is cleaned up.

3-4. Java’s Shape — The Gadget Chain Concept (Screen Example)

A Java serialized object is binary, with the first two bytes AC ED (hex). Wrapped in Base64 and riding in a cookie, it starts with rO0.

rO0ABXNyACRqYXZhLnV0aWwuQ29sbGVjdGlvbnMk...

How to read it: a long Base64 string starting with rO0 in logs or cookies marks a Java deserialization point. Java has well-researched gadget chains where methods of library classes chain-call inside readObject(), so attackers pick a payload matching "this server’s library combination" with tools like ysoserial. It only works when a vulnerable library combination exists on the server’s classpath — which is why building gadget chains yourself is advanced territory, and today understanding the concept that "libraries become the weapon’s raw material" is enough.

3-5. The Safe Alternative — The Same Data as JSON

Let’s implement the same need (store and restore an object) in a safe format and compare.

import json

data = {"user": "gildong", "level": 3}
blob = json.dumps(data)
print("JSON:", blob)
print("Restored:", json.loads(blob))

Output (measured 2026-09-09):

JSON: {"user": "gildong", "level": 3}
Restored: {'user': 'gildong', 'level': 3}

How to read it: JSON has no concept of "a function to run at assembly time." Only strings, numbers, arrays, and objects (dictionaries) make the round trip. However an attacker rewrites the contents, there’s simply no place in the data for executable code to ride. If you truly must exchange objects, attach an HMAC signature, verify "did I create this data?" first, and deserialize only what passes.


4. Missions & Exercises

Mission — Reproduce pickle Code Execution and Tell Signatures Apart

  1. Reproduce 3-1–3-2 and capture the screen where "file exists" flips before/after loads
  2. Change the Evil class’s command — e.g., one that records the current time to a file. Confirm again that the execution point is loads
  3. Read the PHP signature string from 3-3 and break the class name, property count, and each property’s type and value into a table
  4. Base64-encode the evil_blob you made in your experiment, and confirm "the clue that this string is pickle" (the leading bytes after decoding)
  5. Place the JSON version from 3-5 next to the pickle version and write one paragraph on "why this attack doesn’t work with JSON"

Exercises

Exercise 1. In a deserialization attack, explain why the attacker can execute commands without "uploading new code" to the server.

Exercise 2. What role do PHP’s __destruct, Java’s readObject, and Python’s __reduce__ each play during deserialization? What do they have in common?

Exercise 3. If an HTTP cookie value starts with rO0ABXN..., what should you suspect, and why?

Exercise 4. Explain why the defense against deserialization vulnerabilities is "don’t use it / verify signatures" rather than "input filtering."


5. Model Answers & Completion Criteria

Mission Model Answer

Items 1–2 are exactly the Section 3 measurements. When changing the command in item 2, the execution point is still the single pickle.loads(evil_blob) line — confirm with prints that nothing happens at pickle.dumps time.

Item 3 breakdown table:

Fragment Meaning Value
O:4:"User" Object of class User (4 letters) class name User
:2: 2 properties
s:4:"name" String of 4 letters, "name" property name
s:7:"gildong" String of 7 letters, "gildong" value
s:5:"admin" / b:1 Boolean property admin = true value

Item 4: decoding the result of base64.b64encode(evil_blob) shows it starts with x80x04. If these bytes appear when you decode Base64, it’s pickle. Item 5 sample paragraph: "JSON expresses only kinds of values (strings, numbers, arrays, objects) and has no means to express ‘a function to run at assembly,’ so however you rewrite the contents, no executable code can ride in the data. pickle writes the assembly procedure itself into the data, so execution follows."

Exercise Answers

Answer 1. Because deserialization follows an "assembly procedure" written inside the data, and that procedure can be set to call code already present on the server (class magic methods, library functions). The attacker doesn’t plant new code — they submit, as data, an order for weaving existing code (a gadget chain).

Answer 2. All three share the trait of being "methods that run automatically, without an explicit developer call, when an object is assembled by deserialization." PHP’s __destruct runs at object destruction, Java’s readObject when reading an object from a stream, and Python’s __reduce__ specifies a function call as the assembly blueprint. The attacker manipulates data to reach these automatic execution points.

Answer 3. You should suspect a Java serialized object. Java’s serialized byte stream starts with the magic bytes AC ED, and Base64-encoding that yields a string starting with rO0. This value sitting in a cookie means the server deserializes that cookie somewhere — a dangerous point where tamperable input flows into deserialization.

Answer 4. A deserialization attack’s payload is "completely syntactically valid" serialized data — because the structure is correct, format checks or string filters can’t distinguish malicious from normal. The malice lies not in the data’s shape but in the assembly’s outcome. So the defense must be either not applying deserialization at all to untrusted input (using pure data formats), or assembling only "data I issued" verified by an HMAC signature.

Completion Criteria Checklist

  • [ ] I can explain serialization/deserialization as "object ↔ byte sequence"
  • [ ] I measured that __reduce__ is an assembly blueprint and that pickle executes its function
  • [ ] I reproduced the scene where a file appears before/after pickle.loads
  • [ ] I can distinguish the PHP O:, Java rO0, and pickle x80x04 signatures
  • [ ] I can explain that a gadget chain is "a chained call of existing code"
  • [ ] I can explain why JSON is safe against this attack
  • [ ] Mission: reproduction capture + PHP signature breakdown table + JSON comparison paragraph

6. Common Pitfalls & Fixes

Wall 1. ModuleNotFoundError: No module named 'pickle'

Cause: rare, but a file of yours named pickle.py may be shadowing the standard library.
Fix: don’t create files in your working folder named the same as standard modules like pickle.py or os.py. If names clash, rename and clear __pycache__ too.

Wall 2. I ran loads but no file appeared

Cause 1: the current working directory where os.system‘s command runs may differ from the script’s location. Run from another folder, and the file appears there instead.
Cause 2: redirection (>) is shell syntax and varies by environment. It works fine in Git Bash/Linux but may differ elsewhere.
Fix: print os.getcwd() to check the working directory, and look in that folder for the file.

Wall 3. AttributeError: Can't pickle local object

Symptom-family message:

_pickle.PicklingError: Can't pickle <class ...>: it's not found as ...

Cause: a class defined inside a function or conditional can’t be located by pickle via its module path.
Fix: define classes at the script’s top level (with no indentation).

Wall 4. A pickle made on another Python version won’t open

Cause: pickle is Python-specific and has protocol versions. Interpreters older than the default protocol (around 5 on 3.12) can’t read newer protocols.
Fix: this is yet another reason not to use pickle as a "permanent storage/exchange format." If you need exchange, use JSON. For experiments, run dumps and loads on the same interpreter.

Wall 5. I want to build "a filter that uses pickle safely"

Cause: a technique of subclassing pickle.Unpickler to restrict find_class is known, but bypass cases keep being reported.
Fix: abandon the very idea that filtering can make it safe. As the official documentation warns, not using it on untrusted data is the answer. If you need verification, attach an HMAC signature (the concept from Step 150).


7. Summary

Today’s Concepts

Concept One-line explanation
Serialization Flattening an in-memory object into bytes for storage/transfer
Deserialization Reassembling an object from bytes — assembly can come with code execution
__reduce__ pickle’s assembly blueprint — write (function, (args,)) and that function runs
Magic methods Methods invoked automatically during deserialization (__destruct, readObject)
Gadget chain A weapon made by weaving methods of classes on the server into a call chain
Signatures PHP O:, Java rO0 (AC ED), pickle x80x04 — clues for format identification
ysoserial A Java gadget-chain payload collection — library combinations as weapons
Defense principle Never deserialize untrusted data — JSON or HMAC verification

Today’s Commands & Code

Command/code What it does
pickle.dumps(object) Object → byte sequence
pickle.loads(bytes) Bytes → object assembly (the danger point)
def __reduce__(self): return (function, (arg,)) Define the action to run on deserialization
os.path.exists(...) before/after comparison Proving the code execution moment
json.dumps() / json.loads() Safe pure-data round trip
base64.b64encode(blob) Turning a payload into a transmittable string

The Instinct That Matters More Than Commands

When you look at data, keep an eye that asks "is this deserialized somewhere?" The rO0 in a cookie, the O: in a parameter, the x80 in a binary — these signatures are signposts saying "assembly is happening beyond here." And one defender’s instinct: this vulnerability is a matter of choice, not patching. Adopt "never feed others’ data to loads" as a design principle from the start, and you block at the source attacks that a hundred filters couldn’t stop. The more convenient the format, the more you should check first whether execution comes along for the ride.


Once every box is checked, Step 196 is complete.