Server-Side Template Injection
One probe separates a harmless string from remote code execution. If the server hands back 49, it didn't echo your input — it ran it. That is SSTI, and this page lets you trigger it yourself.
Interactive lab
This is a real (arithmetic-only, sandboxed) template engine — no eval, nothing leaves your browser.
Fire a payload at the vulnerable server, then flip it to patched and send the same thing. The difference is the whole technique.
The core idea
A web app becomes injectable the moment it drops user input into the template source instead of passing it as data to a finished template. The engine can't tell your text from the developer's — so it parses your braces too.
render_template_string("Hi " + name)
render_template("hi.html", name=name)
49 and not {{7*7}}?
In the safe version the braces are just characters in a string argument — printed verbatim. In the vulnerable version they land in the template grammar, so 7*7 is arithmetic the engine performs. From there, 7*7 is just the friendly demo — the same injection point reaches Python/Ruby/Java internals.Technique #1 · detection flow
Straight from the skill file — including the clever part: you don't need the output reflected at all. A changed error is enough to prove the engine is parsing you (that's the "Successful Errors" of the title).
{{7*7}}, ${7*7}, <%= 7*7 %>. Different engines, different braces.{{undefined.x}}. Blind and WAF'd contexts still leak through the exception path.os / system.From 49 to shell
Once 7*7 evaluates, the payload swaps for the engine's object graph. Here's the canonical Jinja2 climb — the same rung the top-ranked research automates.
The braces run arithmetic — proven, low-noise.
Reach config, secrets, and objects in scope through the template's variables.
Walk the object graph to the OS: import a module, spawn a process, read its output.
# Jinja2 (Python) — the demo math swapped for a shell command {{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }} # Same idea, other engines: # Twig (PHP) {{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}} # ERB (Ruby) <%= system("id") %>
id or a controlled read), document it, and report — never pivot, never touch real data.