Advanced

Insecure Deserialization: From Serialized Blob to Remote Code Execution

How insecure deserialization becomes RCE: serialized blobs, magic methods, gadget chains, ysoserial, phar, pickle, and how to prevent it. With free practice.

Published

Insecure Deserialization: From Serialized Blob to Remote Code Execution

Serialization turns an in-memory object into a byte stream so it can be stored or sent, and deserialization rebuilds it. The bug appears when an application deserializes attacker-controlled data, because the act of rebuilding an object can trigger code (constructors, magic methods, type handlers) before the app ever validates it. That turns a seemingly inert data blob into one of the most reliable paths to remote code execution, and it is widespread because developers treat serialized data as trusted. Want to feel it rather than read about it? Sign up free and try it on a live target.

Serialization basics

The mental model in one sentence: serialized data can carry behavior, not just values. A serialized object records its class and its properties, so rebuilding it instantiates that class and populates it, which can run lifecycle code. Apps serialize constantly: session state, caches, message queues, and API payloads. The first spoke covers the formats and why the bytes are dangerous: serialization basics.

Why deserializing untrusted data is dangerous

A safe parser reads data into a fixed structure and nothing else. A deserializer reconstructs an object of whatever type the data names, and runs that type's lifecycle hooks, before your code validates anything. So the dangerous work happens during the parse itself, not after. If an attacker controls the bytes, they control which object is built and with what properties, and the hooks do the rest.

How to identify deserialization

You exploit what you can recognize. Serialized blobs have distinctive signatures (PHP O:, Java rO0AB, .NET __VIEWSTATE or AAEAAAD, pickle opcodes) and ride in predictable carriers (cookies, hidden fields, query and body params, headers, API payloads). Flipping a byte and watching for a parse error confirms the server deserializes your input. See identifying deserialization.

Per-language exploitation

The mechanics differ by ecosystem, so each has its own spoke:

Magic methods and gadget chains

You rarely need your own class. A gadget chain strings together existing classes' lifecycle methods so that deserializing a crafted object reaches a dangerous sink. You attack the libraries, not the app. See magic methods and gadget chains.

PHAR deserialization

In PHP you can trigger deserialization with no explicit unserialize() call: on PHP 7.x the phar:// wrapper deserializes a PHAR archive's metadata whenever a file operation touches it. PHP 8.0 restricted this to an explicit Phar::getMetadata() call, so on modern PHP it is a legacy or getMetadata-reachable technique. See PHAR deserialization.

Beyond RCE: memory corruption

When the deserializer is native code, malformed input can cause type confusion or buffer issues, which matters even in memory-safe languages that call native extensions. See deserialization to memory corruption.

Exploiting with tools

ysoserial (Java), ysoserial.net and viewgen (.NET), and hand-built pickles turn gadget construction into one command. See exploiting with tools.

A worked example

Say a PHP app stores a serialized session object in a cookie. You decode it and see a familiar signature:

O:4:"User":2:{s:4:"name";s:3:"bob";s:7:"isAdmin";b:0;}

That is a serialized User object. First you confirm you can tamper it: flip isAdmin to true and re-encode.

O:4:"User":2:{s:4:"name";s:3:"bob";s:7:"isAdmin";b:1;}

If the app trusts the rebuilt object, you are now admin. But the bigger prize is a gadget. Suppose the codebase also loads a Logger class whose __destruct() writes data to logfile. You never instantiate it normally, but you can forge it in the cookie:

O:6:"Logger":2:{s:7:"logfile";s:23:"/var/www/html/shell.php";s:4:"data";s:26:"<?php system($_GET[0]); ?>";}

When the request ends, PHP destroys the forged object, __destruct() runs, and it writes a webshell into the web root. That is the arc: fingerprint the blob, confirm you can tamper it, then forge an object whose lifecycle method reaches a sink.

Prevention

  • Do not deserialize untrusted data. This is the only complete fix.
  • Prefer simple formats (JSON, Protobuf) with no polymorphic type handling. Disable Jackson default typing, JSON.NET TypeNameHandling, and yaml.load. Never pickle external input.
  • Integrity-protect any serialized data you round-trip to clients: sign with an HMAC or encrypt, and verify before parsing.
  • Allow-list permitted classes (Java ObjectInputFilter, resolver overrides).
  • Patch gadget-prone libraries and remove unused ones from the classpath.
  • Least privilege and isolation so a successful RCE is contained.

The full detail is in preventing insecure deserialization.

Where it connects

Deserialization is one of several routes from a web request to code execution. Server-side template injection reaches RCE through a template engine, and OS command injection through a shell. And file upload feeds PHAR deserialization directly, since a polyglot image that is secretly a PHAR becomes the trigger.

Where to go next

Work through the cluster in order, or jump to what you need. Start with serialization basics, then identifying deserialization and PHP object injection. When you want every payload in one place, the cheat sheet has them. And when you are ready to break something real, sign up free and start hacking.