HTB: MonitorsThree Writeup

MonitorsThree - HackTheBox Writeup

Machine Information

AttributeDetails
NameMonitorsThree
OSLinux
DifficultyMedium
PointsN/A
Release DateN/A
IP Address10.10.11.X
Authord3vn0mi

Machine Rating

⭐⭐⭐☆☆ (3/5)

Difficulty Assessment:

  • Enumeration: ⭐⭐⭐☆☆
  • Real-world: ⭐⭐⭐⭐☆
  • CVE: ⭐⭐⭐⭐☆
  • CTF-like: ⭐⭐⭐☆☆

Summary

MonitorsThree presents a company site for a networking-solutions vendor sitting behind an nginx front end at monitorsthree.htb. The forgotten-password flow leaks a blind SQL injection in the username parameter, which was enough to dump the application’s user table and crack the admin hash. That password didn’t work on the main login — it belonged to a Cacti monitoring instance hiding on a separate vhost, cacti.monitorsthree.htb. Cacti 1.2.26 is vulnerable to CVE-2024-25641, an authenticated arbitrary file write in the Package Import feature, which was used to drop a PHP payload and pop a www-data shell. From there, database credentials in Cacti’s own config led to a second set of crackable credentials for the local user marcus, whose SSH private key completed the foothold. Root came from a Duplicati backup service bound to localhost, exploited via a documented authentication-bypass technique against its SQLite-stored server passphrase, followed by abusing its run-script-before advanced option to execute a script as root against the host filesystem mounted into the container’s backup source.

TL;DR: Blind SQLi in forgot_password.php → cracked admin hash → Cacti 1.2.26 admin login on a discovered subdomain → CVE-2024-25641 Package Import arbitrary file write → www-data shell → DB creds in global.php → cracked marcus bcrypt hash → SSH as marcus (user flag) → localhost-only Duplicati 2.0.8.1 → SQLite passphrase leak → auth bypass → run-script-before RCE as root (root flag).


Reconnaissance

Port Scanning

Terminal window
nmap -sC -sV -T4 -p- 10.10.11.X

Results:

  • 22/tcp — SSH
  • 80/tcp — nginx, redirects to the monitorsthree.htb virtual host

The redirect meant the host header mattered from the very first request, so monitorsthree.htb went into /etc/hosts before any further enumeration:

Terminal window
echo "10.10.11.X monitorsthree.htb" | sudo tee -a /etc/hosts

Service Enumeration

The site at monitorsthree.htb is a marketing page for a networking-services company, with a login page and a “Forgot Password” flow. Neither the login form nor the obvious injection points on it reacted to payloads, but the forgot-password page’s username field behaved differently under injection — the response changed based on truthy/falsy conditions even though no SQL error text was reflected back.

Virtual-host discovery against the same IP turned up a second subdomain, cacti.monitorsthree.htb, which was also added to /etc/hosts. This hosted a separate Cacti network-monitoring login page.

Vulnerability Assessment

  • Blind SQL injection in forgot_password.php (username POST parameter) on monitorsthree.htb.
  • Cacti 1.2.26 on cacti.monitorsthree.htb, vulnerable to CVE-2024-25641.

Initial Foothold

SQL Injection → Admin Credential Dump

On this live instance the injection was blind-only: MySQL error output was never reflected in the immediate response, and the actual true/false oracle only showed up on the post-redirect page, not the injection response itself. That ruled out error-based extraction and pushed this toward a time-based blind approach.

Terminal window
# Captured the forgot_password.php POST request with Burp, marked the
# username parameter as the injection point, and handed it to sqlmap
sqlmap -r forgot_password_req.txt -p username --risk 3 --level 5 --dbs

With the database confirmed (monitorsthree_db), the users table was pulled via sqlmap’s time-based technique (forced, since boolean/error responses weren’t usable directly against the redirect target):

Terminal window
sqlmap -r forgot_password_req.txt -p username \
-D monitorsthree_db -T users --dump --technique=T

This produced the admin row with an MD5 hash, 31a181c8…, which cracked to:

greencacti2001

Why this works: the forgot-password lookup concatenates the submitted username directly into a SELECT used to decide whether to show a “reset sent” vs. “user not found” state, without parameterization. Even with error display suppressed, that conditional branch is a perfectly usable boolean/time oracle for sqlmap.

Cacti 1.2.26 — CVE-2024-25641 (Package Import Arbitrary File Write)

greencacti2001 didn’t work against the main site login — it worked as admin on the Cacti instance found at cacti.monitorsthree.htb, which displayed version 1.2.26 in the UI footer.

Cacti 1.2.26 is vulnerable to CVE-2024-25641, an arbitrary file write in import_package() (lib/import.php). The Package Import feature accepts a signed XML/gzip package and blindly trusts the filename and file contents embedded in it, writing them to disk without validating the path or verifying the signature against any Cacti-configured key — the “trusted” key is whatever public key is embedded in the package itself, self-signed on the fly. An admin account with Import Templates rights (default for admin) can therefore write an arbitrary PHP file into a web-reachable directory.

Two live-environment quirks mattered for actually getting this to fire on this box:

  • The “Trust signer” option had to be submitted as the literal string trust_signer=on (not 1) for the import to be accepted as trusted.
  • preview_only had to be left empty — leaving it non-empty caused Cacti to only preview the package contents instead of writing the file to disk.
Terminal window
# Generated a self-signed package containing a PHP payload under resource/,
# per CVE-2024-25641, then gzip'd it for import:
# 1. openssl_pkey_new() → generate an RSA keypair
# 2. embed PHP payload + its signature + the public key in the XML
# 3. sign the whole XML blob, wrap in <signature>, gzip -9
php build_package.php # -> package.xml.gz

The generated .xml.gz was uploaded through Cacti’s Import Packages page as admin. Cacti’s cron job wipes the resource/ directory roughly every minute, so a webshell dropped there had a very short usable window — instead of a stealthy webshell, the payload written to resource/ was a one-shot reverse shell, triggered immediately after import by requesting it:

Terminal window
nc -lnvp 4444 &
curl http://cacti.monitorsthree.htb/cacti/resource/<payload>.php
$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

Stable www-data shell obtained before cron could clean up the dropped file.


Privilege Escalation

www-data → marcus

Cacti’s own configuration file leaked its database credentials:

Terminal window
www-data@monitorsthree:~/html/cacti$ cat include/global.php | grep database_
$database_type = 'mysql';
$database_default = 'cacti';
$database_hostname = 'localhost';
$database_username = 'cactiuser';
$database_password = 'cactiuser';

Logging into the local cacti MySQL database with cactiuser:cactiuser exposed the user_auth table, which stores bcrypt hashes for Cacti’s own web users — including marcus, matching the machine’s local Linux account of the same name.

-- cacti.user_auth
mysql> SELECT username, password FROM user_auth WHERE username='marcus';

The bcrypt hash for marcus cracked to:

12345678910

That password worked directly against the local account:

Terminal window
www-data@monitorsthree:~$ su marcus
Password: 12345678910
$ id
uid=1000(marcus) gid=1000(marcus) groups=1000(marcus)

As marcus, an SSH private key was sitting in ~/.ssh/id_rsa, providing a clean, stable SSH foothold rather than relying on the reverse shell:

Terminal window
ssh -i marcus_id_rsa marcus@10.10.11.X

User flag captured from /home/marcus/user.txt.

marcus → root: Duplicati Auth Bypass + run-script-before RCE

Local port enumeration as marcus turned up a service bound only to loopback on port 8200. SSH local port-forwarding exposed it:

Terminal window
ssh -i marcus_id_rsa -L 8200:127.0.0.1:8200 marcus@10.10.11.X

Browsing to the forwarded port revealed Duplicati 2.0.8.1, a backup client running as root, with the host filesystem mounted into its backup-source view at /source — i.e., /source/home/marcus/... on the Duplicati side maps back to the real /home/marcus/... on the host.

Duplicati stores its web-UI authentication state, including the server passphrase, in a local SQLite database:

Terminal window
marcus@monitorsthree:~$ find / -name "Duplicati-server.sqlite" 2>/dev/null
/opt/duplicati/config/Duplicati-server.sqlite

The file was world-readable, and the Option table inside it held the server passphrase used to derive login tokens. Duplicati’s web login challenge/response is not a plain password check — it’s a nonce-salted SHA-256 scheme:

noncedpwd = base64( SHA256( nonce_bytes || base64decode(server_passphrase) ) )

Reconstructing that client-side (matching the server’s own nonce for the current login attempt) produced a valid login token without ever needing the plaintext passphrase — a documented authentication bypass against Duplicati’s nonce-based login when the server passphrase is recoverable from disk.

With an authenticated session, Duplicati’s Advanced options expose run-script-before, a hook that executes an arbitrary script as the Duplicati process user — root, in this case — immediately before a backup job runs. Since the backup source path (/source) is the real host root mounted read/write into the service, a script could be staged on the host (under marcus’s home) and then referenced from the Duplicati side of that same mount:

/source/home
# Backup job configuration (via the Duplicati web UI):
# Advanced option: run-script-before = /source/home/marcus/pwn.sh

Running the backup job executed pwn.sh as root before the backup itself started, since run-script-before fires under the Duplicati service’s own privileges rather than the invoking user’s.

Terminal window
$ id
uid=0(root) gid=0(root) groups=0(root)

Root shell confirmed with euid=0. Root flag captured.


Attack Chain Summary

Blind SQLi (forgot_password.php) → dump monitorsthree_db.users → crack admin MD5 (greencacti2001)
→ discover cacti.monitorsthree.htb → Cacti 1.2.26 admin login
→ CVE-2024-25641 Package Import arbitrary file write → reverse shell (www-data)
→ include/global.php DB creds (cactiuser:cactiuser) → cacti.user_auth bcrypt for marcus → crack (12345678910)
→ su marcus → SSH key (~/.ssh/id_rsa) → SSH as marcus (USER FLAG)
→ port-forward Duplicati (127.0.0.1:8200) → leak server passphrase (Duplicati-server.sqlite)
→ nonce-based auth bypass → run-script-before via /source mount → root shell (ROOT FLAG)

Tools Used

ToolPurpose
nmapPort/service discovery
Burp SuiteIntercepting and shaping the injection request for sqlmap
sqlmapTime-based blind SQL injection, database/table/credential dump
Custom PHP (openssl_pkey_new/openssl_sign)Building the signed Cacti package for CVE-2024-25641
nc / curlTriggering and catching the reverse shell from the written payload
mysql clientReading Cacti’s user_auth table for the marcus hash
Password cracker (offline, bcrypt/MD5)Cracking the admin MD5 and marcus bcrypt hashes
ssh / scpStable foothold as marcus; port-forwarding Duplicati; retrieving the SQLite DB
Browser dev console (SHA-256/Base64)Reconstructing the Duplicati nonce-salted login token
Duplicati Web UIConfiguring the malicious backup job (run-script-before)

Key Learnings

Techniques Practiced

  • Identifying a blind SQLi oracle even when error output isn’t reflected, by watching for redirect/response-state differences
  • Virtual-host discovery to uncover a second application (Cacti) not linked from the main site
  • Exploiting CVE-2024-25641 against Cacti’s Package Import signature trust model
  • Racing a cron-based cleanup window with a one-shot reverse shell instead of a persistent webshell
  • Credential reuse across an application’s own database layer to escalate from a web user to a system user
  • Exploiting a local-only service exposed via SSH port-forwarding
  • Reconstructing a nonce-salted authentication scheme client-side from a leaked server secret
  • Abusing a backup tool’s pre-job script hook combined with a host-filesystem bind mount to get code execution as the service’s privileged user

Lessons Learned

  1. A password reused between a leaked application DB and a separate monitoring tool’s admin account is often the actual intended pivot — always retry cracked credentials against every discovered login surface, not just the one it came from.
  2. Signed-package import features are only as safe as the signature verification path — if the “trusted” key is whatever key ships inside the package, the signature buys nothing.
  3. Services bound to 127.0.0.1 are not out of scope once you have any shell — they’re frequently where the privileged process (backup agents, monitoring daemons, local admin panels) actually lives.
  4. “World-readable config/database on disk” is functionally equivalent to “credentials are public” the moment that file contains anything used to derive an auth token, even without a plaintext password stored directly.
  5. Any backup/automation tool that lets you point a pre/post-job hook at an attacker-writable path, while also mounting privileged filesystem locations into its own working view, is a direct root primitive — treat run-script-before/run-script-after-style options as code execution, not configuration.

Proof of Ownership

User Flag: <redacted>
Root Flag: <redacted>

References

  • MonitorsThree — Official HackTheBox Writeup by k1ph4ru (Document No D25.100.318, Machine Authors: ruycr4ft & kavigihan) — used to confirm the CVE-2024-25641 Package Import mechanism and the general shape of the Duplicati authentication-bypass technique referenced above.