Description
PHP Object Injection occurs when an application passes user-controlled data to PHP's unserialize() function without validation. Serialised PHP objects encode both data and class names. When unserialize() processes the input, PHP instantiates the named class and, critically, invokes any __wakeup() or __destruct() magic methods defined on it automatically, without any explicit call from the application.
CWE-502 — Deserialization of Untrusted Data covers this vulnerability class. The attack does not require the attacker to inject a new class — it requires only that a class already present in the application's loaded codebase (including all Composer dependencies) defines magic methods that can be chained to produce a harmful outcome. These chains of existing code reused for exploitation are called Property-Oriented Programming (POP) chains.
Under A08:2021 — Software and Data Integrity Failures, the OWASP Top 10 specifically references insecure deserialization as a critical concern. PHP applications using unserialize() on cookie values, session data retrieved from untrusted storage, or any user-supplied string are vulnerable.
How It Works
Consider a simple application that stores user preferences in a cookie using serialisation:
// Storing preferences
$prefs = new UserPrefs();
$prefs->theme = 'dark';
setcookie('prefs', base64_encode(serialize($prefs)));
// Restoring preferences
$prefs = unserialize(base64_decode($_COOKIE['prefs']));
An attacker constructs a malicious serialised object targeting a class in the loaded codebase — for example, a file deletion gadget using a logger class:
// Attacker-crafted payload targeting a FileLogger class with __destruct
class FileLogger {
public $logFile = '/var/www/html/shell.php';
public $logData = '<?php system($_GET["cmd"]); ?>';
}
$payload = serialize(new FileLogger());
// O:10:"FileLogger":2:{s:7:"logFile";s:26:"/var/www/html/shell.php";s:7:"logData";s:30:"<?php system($_GET["cmd"]); ?>";}
When the application calls unserialize() on the base64-encoded version of this payload, PHP instantiates FileLogger and, when the object is garbage collected, calls __destruct() which writes the attacker's web shell to disk.
Real-world exploitation leverages tools like phpggc (PHP Generic Gadget Chains) to automatically generate POP chain payloads targeting popular frameworks:
# Generate a Laravel RCE payload via phpggc
phpggc Laravel/RCE1 system 'id' -b
# Outputs a base64-encoded serialised payload
Impact
- Remote code execution — POP chains targeting
__call(),__toString(), or__destruct()methods in common libraries can achieve arbitrary OS command execution. - Arbitrary file write — gadgets targeting file system operations can write web shells or modify application configuration files.
- SQL injection bypass — object injection can manipulate ORM query objects, introducing SQL injection through an otherwise parameterised data layer.
- Authentication bypass — manipulating deserialised user or session objects can grant elevated privileges or impersonate other users.
- Full application compromise — combining RCE with file write enables persistent backdoor installation and complete application takeover.
Detection
- Identify
unserialize()call sites — search the application source for every call tounserialize()and trace the data reaching it back to its source. Any path that includes user-controlled data (cookies, POST bodies, HTTP headers, database values populated from user input) is potentially exploitable. - Inspect cookies and session tokens — base64-decode all cookies and look for PHP serialisation format markers: strings beginning with
O:,a:, ors:are serialised PHP. - Test with
phpggc— enumerate installed Composer packages fromcomposer.lockand checkphpggc -lfor available gadget chains matching those packages. Generate test payloads and observe application behaviour. - Monitor for unexpected class instantiation — enable PHP error logging and send a payload referencing a class that does not exist. An unhandled PHP warning confirms that
unserialize()is processing the input. - Fuzz with malformed serialised data — send corrupted serialised payloads and observe for PHP fatal errors or unexpected application behaviour that confirms deserialization of the input.
Remediation
Replace unserialize() with json_decode(). For the vast majority of use cases (storing preferences, passing data between components), JSON is a safe substitute that carries no code execution risk.
If unserialize() is unavoidable, use the allowed_classes option:
// Restrict deserialization to a specific allowlist of classes
$data = unserialize($input, ['allowed_classes' => ['UserPrefs', 'CartItem']]);
This prevents instantiation of arbitrary classes from gadget chains while still allowing the application's intended data objects.
Implement integrity verification. Sign all serialised data with HMAC-SHA256 before storage and verify the signature before calling unserialize(). An attacker cannot forge a valid signature without the secret key.
Audit Composer dependencies. Regularly check installed packages against the phpggc gadget chain library. Removing unused packages that contain known gadgets reduces the exploitable attack surface.
