Temple — TryHackMe Writeup
Discovered an unusual Werkzeug/Python web server on high port 61337 alongside default Apache on port 80, OpenSSH, Telnet, and FTP.
Traversed nested 403 Forbidden directories (/temporary -> /temporary/dev) to uncover a hidden registration endpoint at /temporary/dev/newacc.
Identified template reflection in the account view. Bypassed a strict character blacklist ('_#&;) using hex escapes (\x5f) and Jinja attribute reflection on the cycler object to execute commands as bill.
Identified a root-owned Logstash service monitoring /etc/logstash/conf.d/*.conf with auto-reload enabled (3s). Injected an exec input plugin into a world-writable sample config via base64 to pop a root shell.
Challenge Overview
Temple is a medium-rated Linux machine on TryHackMe that showcases two distinct web and systems exploitation concepts:
- ▹Application Security: A Flask/Werkzeug web application running on an atypical high port (61337) with a hidden multi-tier administrative route that reflects user input directly into a Jinja2 template under restrictive character filtering.
- ▹System Administration Security: A misconfigured Elastic Logstash service executing as
rootwith dynamic configuration auto-reload enabled on a world-writable pipeline directory.
The attack chain begins with targeted service enumeration, drills down through nested directory structures where HTTP 403 status codes disguise valid functional paths, weaponizes an elegant Jinja2 SSTI filter bypass using attribute introspection without relying on brittle subclass indices, and escalates privileges by abusing Logstash's built-in execution plugins.
1. Reconnaissance
::1.1 Full TCP Port Scan
We begin our engagement with an aggressive, all-ports TCP scan to establish the perimeter surface:
::1.2 Service Fingerprinting
Five ports are open. We run deep script and banner enumeration (-sC -sV) against the detected listeners:
2. Web Enumeration & Directory Discovery
::2.1 Initial Inspection of the Werkzeug Service (Port 61337)
Navigating directly to http://10.114.137.179:61337/ issues an immediate 302 Found redirecting the browser to /login:
The /login page renders a minimalist authentication prompt requesting a username and password. There are no registration links, forgot-password buttons, or client-side comments revealing credentials.
::
To identify hidden routes, we run gobuster against the root of the WSGI server:
We observe two distinct 403 Forbidden responses: /admin and /temporary.
The 403 Trap: Many penetration testers discard 403 Forbidden responses as dead ends. In web application frameworks and reverse-proxy setups, a 403 frequently signifies that directory listing is disabled on an existing container folder, or that access to the parent folder index is forbidden while sub-resources remain accessible. Always fuzz inside 403 paths!
We drill into /temporary/:
Another 403 Forbidden at /temporary/dev. We switch to a broader wordlist (raft-medium-directories-lowercase.txt) and recurse into /temporary/dev/:
::
Navigating to http://10.114.137.179:61337/temporary/dev/newacc returns an account creation interface:
We now have an unauthenticated vector to register arbitrary accounts into the underlying database.
3. Vulnerability Analysis — Jinja2 SSTI
::3.1 Template Injection Probing
We register a test user to observe how user profiles are handled post-authentication:
- ▹Email:
test@lab.local - ▹Username:
{{7*7}} - ▹Password:
password123
The server evaluated the mathematical expression {{7*7}} and returned 49.
This confirms that the application dynamically renders the username using render_template_string(template_string) rather than passing variables through safe template bindings (render_template('account.html', username=username)).
::3.2 Template Engine Fingerprinting Methodology
Whenever template expression evaluation is observed, penetration testers utilize a structured decision tree (originally formalized by PortSwigger Research) to isolate the exact backend engine. The technique systematically tests syntax variations (${...} vs {{...}}), comment behavior, and type coercion (e.g. {{7*'7'}} yielding '7777777' in Python Jinja2 versus 49 in PHP Twig).
The interactive diagnostic visualizer below reproduces this methodology, allowing you to trace the exact detection path taken on Temple, simulate live target probes, or reference canonical RCE vectors:
PortSwigger Template Injection Decision Graph & Engine Fingerprinting
{{7*'7'}} evaluates to '7777777' because Python natively implements string replication via the * operator.
Python's premier templating engine. Expressions inside {{ ... }} execute with standard Python language semantics. String multiplication replicates strings ('7'*7 produces '7777777'). RCE is achieved by traversing Python's object hierarchy to access os or subprocess modules.
{{cycler|attr('__init__')|attr('__globals__')|attr('__getitem__')('os')|attr('popen')('id')|attr('read')()}}::3.3 Mapping Character Filtering & Blacklist Rules
When testing classic Python SSTI discovery payloads such as {{ config }} or {{ ''.__class__ }}, the application responded with generic validation rejections. We systematically probe character acceptance to establish the boundary rules:
The filter enforcement drops any request containing the following characters:
| Banned Character | Reason for Exclusion | Impact on Standard Payloads |
|---|---|---|
' (Single quote) | String literal limiter | Blocks single-quoted string keys |
_ (Underscore) | Dunder protection | Breaks __class__, __globals__, __mro__, __subclasses__ |
; (Semicolon) | Command chaining | Prevents multi-statement shell injection |
& (Ampersand) | Backgrounding / chaining | Prevents bash backgrounding and && chaining |
# (Hash) | Jinja comment syntax | Prevents comment-based template truncation |
The most critical barrier is the exclusion of the underscore (_), which prevents direct reference to Python's internal reflection attributes.
::3.3 Crafting the Filter Bypass: Hex Escapes & Attribute Introspection
To overcome these constraints without single quotes or literal underscores, we combine two features of the Jinja2 engine and Python string syntax:
- ▹
Hexadecimal String Escaping: Python evaluates hex escapes inside quoted strings. In double quotes,
\x5fevaluates to ASCII95(_). Therefore:~ / jinja"\x5f\x5fclass\x5f\x5f" --> "__class__"The application's character filter inspects the raw payload before Jinja parses the string literal, so
\x5fpasses cleanly through the perimeter filter. - ▹
Jinja's Built-in
|attr()Filter: Rather than using dot notation (obj.__class__), Jinja provides theattrfilter:~ / jinjaobj|attr("\x5f\x5fclass\x5f\x5f")This performs attribute lookup dynamically using the decoded string without requiring a literal dot or underscore.
- ▹
Double Quotes are Allowed: While single quotes (
') are banned, double quotes (") are permitted.
::3.4 Python RCE Gadget: Walking the cycler Namespace
Standard CTF writeups often brute-force the index of subprocess.Popen or os._wrap_close across hundreds of subclasses via:
This is fragile across Python minor versions and environment dependencies.
Instead, we leverage Jinja's built-in helper functions. Jinja exposes global utilities into the template namespace, including cycler, joiner, and namespace. The cycler class is defined in jinja2.utils, which explicitly imports the os module at the top of the file!
By inspecting cycler.__init__.__globals__, we can access os directly:
Translating this gadget into our filtered syntax using |attr() and \x5f:
Why cycler is Superior: You never need to hunt for subclass array indices or worry about library offsets. As long as Jinja2 is running in Python, cycler's global module namespace reliably provides a direct pointer to the os module.
::3.5 Automated SSTI Weaponization Script
We construct an automated Python exploit script to register a unique payload user, authenticate, retrieve the reflected command output from /account, and sanitize all command arguments:
::3.6 Initial Foothold & User Flag
We test our exploit script against the target:
We have achieved remote code execution as bill.
We enumerate /home/bill and retrieve the user flag:
4. Privilege Escalation — Logstash Pipeline Abuse
::4.1 Internal Enumeration & Process Auditing
With a command execution bridge established as bill, we survey running background services:
Logstash is running under process ID 1437 as root.
Logstash is an open-source server-side data processing pipeline that ingests data from multiple sources simultaneously, transforms it, and then sends it to a "stash" like Elasticsearch.
::4.2 Inspecting Logstash Configuration & Pipeline Permissions
We check the permissions and configuration settings under /etc/logstash:
The master pipeline file loads every configuration matching /etc/logstash/conf.d/*.conf. Next, we inspect /etc/logstash/logstash.yml for dynamic reload triggers:
config.reload.automatic: true with an interval of 3 seconds indicates that Logstash actively polls /etc/logstash/conf.d/ and dynamically hot-reloads pipelines on change!
Now we check filesystem permissions on the configuration directory:
The Privilege Escalation Primitive: Look closely at the file mode for logstash-sample.conf:
-r--r--rw-
The file is world-writable (rw for "others"). Any local user on the system can modify this file. Because Logstash runs as root and continuously re-reads *.conf every 3 seconds, writing an execution directive into this file triggers immediate code execution in the security context of root.
::4.3 Weaponizing the Logstash Exec Plugin
Logstash configurations consist of three blocks: input, filter, and output. The exec input plugin runs a command at a scheduled interval and records the output as an event.
We craft a malicious pipeline configuration:
::4.4 Overcoming Filter Constraints via Base64 Smuggling
Writing this configuration file directly through our SSTI command execution wrapper would trigger the character filter because bash reverse shells require single quotes (') and ampersands (&).
To bypass this restriction, we base64-encode the entire payload on our local machine and decode it on the target directly into the destination file:
::4.5 Dropping the Pipeline & Catching the Root Shell
We stage our Netcat listener on port 4444:
Then, using our SSTI script, we decode the base64 payload into /etc/logstash/conf.d/logstash-sample.conf:
We verify the configuration was successfully written:
Within 3 seconds, Logstash detects the timestamp update, parses the pipeline, and fires the exec command as root:
::4.6 Root Flag Capture
With an interactive root shell established, we navigate to /root to harvest the final flag and inspect root-owned artifacts:
Reading flag2.txt:
Checking /root/script.sh confirms how Logstash was initiated:
5. Automation (/autosolve)
To demonstrate how the entire attack chain can be automated end-to-end—from directory enumeration and SSTI weaponization to Logstash pipeline injection and root privilege escalation—we present the interactive terminal solver replay:
6. References
::Technical Specifications & Documentation
- ▹PortSwigger Web Security — Server-Side Template Injection (SSTI)
- ▹PayloadsAllTheThings — Jinja2 Template Injection Methodology
- ▹Elasticsearch — Logstash Exec Input Plugin Reference
- ▹Jinja2 Official Documentation — Built-in Filters & Utilities
::

Red Team Consultant · Penetration Tester · Bug Bounty Hunter
Offensive security professional with 250+ vulnerabilities reported across 50+ organizations including Atlassian, Vimeo, and AT&T. Sharing research, tools, and field notes.