SSTI – Basic server-side template injection

I love hacking. Recently, YesWeHack released a video on YouTube of Brumens’s talk about advanced SSTI techniques from the 2024 Ekoparty conference. This has inspired me to circle back on this topic, redo the Portswigger labs and document. I know SSTI is there, but, to be honest, I usually don’t really check for it unless I am doing a CTF and there’s a hint that might be the path.

YouTube Video: https://www.youtube.com/watch?v=FVm6wYc1S6A

Let’s get rolling with a series of blog posts solving all of the Portswigger Web Academy labs on the topic.

  1. >>SSTI – Basic server-side template injection<<
  2. SSTI – Basic server-side template injection (code context)
  3. SSTI – Server-side template injection using documentation
  4. SSTI – Server-side template injection in an unknown language with a documented exploit
  5. SSTI – Server-side template injection with information disclosure via user-supplied objects
  6. SSTI – Server-side template injection in a sandboxed environment
  7. SSTI – Server-side template injection with a custom exploit

NOTE – Links will light up as posts become available.

Fuzzing resources:

What is SSTI

SSTI is server side template injection. There are a ton of posts that go deeper into exactly what SSTI is (or ask your favorite LLM). Essentially, Server-Side Template Injection (SSTI) occurs when user input is unsafely processed by a web application’s template engine, allowing attackers to execute arbitrary code on the server.

If a vulnerable web app processes input like {{7*7}}, it evaluates the expression instead of treating it as text, returning 49. If further exploited, attackers can execute system commands, access sensitive data, or take control of the server.

More great resources are available here: SSTI Resources

What’s really cool about SSTI is that we’re not just focused on business logic vulnerabilities. Sure, it’s great to find APIs you can abuse for the purposes of breaking the business logic and accomplishing things like BOLAs (Broken Object Level Authorizations), however, to me, it seems a lot cooler to just take over the entire server. With SSTI, you’re really looking for an injection vulnerability that leads to RCE (remote code execution) on the server which can lead to things like throwing a remote shell. Get your Netcat ready!

Note – for bug bounty, throwing a remote shell typically isn’t necessary. A simple whoami demonstrates code execution and is plenty scary.

Lab: Basic server-side template injection

Link: https://portswigger.net/web-security/server-side-template-injection/exploiting/lab-server-side-template-injection-basic

Recently, I have been using Caido as my primary web pen-testing tool. Burp is definitely still in my arsenal, however, I do like the slick and bit more simplistic interface Caido presents. The devs have been working hard on bringing great tooling into Caido and it does seem lighter weight and a tad quicker (totally just my feel – data to back that up).

Get Caido here: https://caido.io/

Step 1: Recon

Let’s see if we can figure out where the SSTI vulnerability exists within this lab. Loading the lab, we get the standard shop interface:

screen shot of lab home page

Clicking View Details button on the first item and I get the following:

screen shot of lab showing inventory out of stock

However, I can see the URL has changed:

screen shot of url bar showing the message

So, do we have template injection where the template literally reflects what’s passed through the message query parameter?

Step 2: Fuzz

One of the things I do like about Caido is that it seems easier to manage scope. As soon as I launch Chromium via Caido:

screen shot of Caido with arrow towards launching browser

All of the Google noise starts raining in and filling the HTTP History log:

screen shot of google garbage flowing into the history log

I haven’t even plugged the URL for the lab into the browser and there’s just so much noise heading back to the mothership. With that, it’s super easy to create out of scope items and apply them to our work here so that we can get rid of the noise:

screen shot of adding google noise to out of scope

And then tune the scope:

screen shot of google out of scope noise wildcards

And, now there’s no noise in the history. This is pretty nice:

screen shot of clean http history

Ok, now let’s plug the lab URL into our Chromium browser and see what we see.

screen shot of Caido and hiding images and styling from history

Make sure these are checked to remove a images and styling from the history for this lab. You will not always want to have these checked, but for this lab we probably won’t be dealing with CSS or images.

screen shot of send to automate

Let’s fuzz this request so send to Automate (equivalent to Burp Intruder)

First, let’s set the spot we want to fuzz. Highlighter and click the plus to add:

screen shot of adding a placeholder for fuzzing

Then we need to set the payload. You can either import a payload as a file and then utilize or you can paste in a simple list. I am going to use this list (copy and paste in as simple): https://raw.githubusercontent.com/swisskyrepo/PayloadsAllTheThings/refs/heads/master/Server%20Side%20Template%20Injection/Intruder/ssti.fuzz

Now, the problem with running a list like this is that we really don’t know what we’re looking for within the response. So, let’s prepend a flag that will be easy to watch for in the response. That way we can easily search for that special string within the responses. Now, hit run

screen shot of showing {{4*4}} did not execute

We can see here that the string reflected is the string that was passed in from the payload list. Arrowing through the responses, I find one that looks promising very early:

screen shot of showing code execution - 49 returned instead of 7*7

What was in payload position #4?

<%= 7 * 7 %>

There are different types of template engines. Different payloads are pointed at triggering code execution within those different engines. This specific payload most likely triggered code execution from Embedded Ruby (ERB).

We have RCE!

Step 3: Explore

Let’s see if we can get additional information about the system. Right mouse click on the request that triggered the RCE and hit send to Replay (equivalent to Burp Repeater).

Quick note on Caido – there are Plugins that extend the functionality of Caido just like Burp has its extensions. One of the Plugins I load immediately for Caido is Convert Tools.

screen shot of Caido plugins adding in Convert Tools

This tool adds in different options and boxes within the UI that enable different types of encoding conversions on the fly. Within Replay, we get this nifty box in the lower left that is handy for changing the string within the payload without having to decode and then recode:

screen shot of Caido and manipulating the payload to return the /etc/passwd file

This is pretty sweet. The little editor window let me paste in a payload from PayloadsAllTheThings which truly proves we have RCE by retrieving the /etc/passwd file.

Step 4: Solve the lab

Lab instructions: This lab is vulnerable to server-side template injection due to the unsafe construction of an ERB template.

To solve the lab, review the ERB documentation to find out how to execute arbitrary code, then delete the morale.txt file from Carlos’s home directory.

I guess if we had read the instructions we would have seen that the underlying implementation was utilizing ERB and we could have skipped the fuzzing step. All good. Regardless, we now need a payload that is going to delete the morale.txt file from Carlos’s home directory. Since we retrieved /etc/passwd, the path is probably going to be /home/carlos/morale.txt. However, let’s walk it a step at a time.

screen shot of payload to show run a dir on root directory

returns:

screen shot of root directory listing including /home directory

So,

screen shot of payload for running a dir on /home

returns:

screen shot of showing dir listing for /home and seeing carlos

So,

screen shot of payload for running a dir on /home/carlos

returns:

screen shot of response showing the morale.txt file

So,

screen shot of payload for deleting the file
screen shot of response for deleting the file

and

screen shot of showing the lab being solved

Done!

Summary

Server-Side Template Injection (SSTI) is a powerful attack vector that allows execution of arbitrary code by injecting payloads into vulnerable template engines. Above, we walked through the first Portswigger lab exploiting SSTI specifically with the ERB template engine, demonstrating how seemingly harmless user input can lead to full system compromise. Good stuff!

Key Takeaways:

  • SSTI = Code Execution – If a template engine is injectable, it’s a very powerful vuln that can lead to a high payout.
  • Know Your Payloads – Different engines have different syntax—identify the target and adapt. Use the payloads from the community and then craft your own over time.
  • Post-Exploitation Matters – Once inside, leverage access for deeper attacks. Show true RCE and then turn it in!

SSTI isn’t just about injecting expressions – often its about owning the entire box.

Happy Hacking!

Leave a Reply