WPScan
WPScan turns a confirmed WordPress site into a clear inventory: the core version, the installed plugins and themes with their versions, the user accounts, and which of those map to known vulnerability references. Reach for it the moment basic fingerprinting says “this is WordPress, dig deeper,” whether you are mapping a CTF box, auditing your own homelab, or working the web phase of an authorized assessment.
Mental model: wpscan --url URL [--enumerate ...] [options]. Everything else is one flag away.
New to WordPress? Core concepts
- Target URL (
--url): the site root everything keys off, for examplehttp://TARGET_IP/ - Enumeration (
-e): what to discover - usersu, pluginsp/ap/vp, themest/at/vt, config backupscb, db exportsdbe - Detection mode (
--detection-mode):passive(quiet),mixed(default),aggressive(requests known paths directly) - Vulnerability database (
--api-token): turns detected versions into known-issue references; a free token is required - WordPress layout: plugins live under
/wp-content/plugins/, themes under/wp-content/themes/; logins hitwp-login.phpandxmlrpc.php - Password attack (
-Pwith-U): tries logins viawp-loginorxmlrpc; loud and lockout-prone - User IDs: WordPress exposes authors by numeric id, so
u1-5walks/?author=N - Fingerprints are clues, not proof: renamed assets, hidden versions, and WAFs all skew results
When to reach for WPScan (and when not)
Reach for it to inventory a confirmed WordPress target: core version, plugin and theme versions matched to known issues, exposed config backups and database exports, and valid usernames for an authorized login test.
Reach for something else when the site is not WordPress (fingerprint with whatweb, nikto, or curl first), when the CMS is unknown (CMSeeK, droopescan), when you want breadth across every stack (nuclei), or when you just need to brute directories and files (gobuster, ffuf).
Install, update, verify#
sudo apt install -y wpscan # Debian / Ubuntu / Kali
sudo gem install wpscan # RubyGems, any distro with Ruby
docker pull wpscanteam/wpscan # containerized, no local Ruby
sudo apt install --only-upgrade -y wpscan # update the package
wpscan --update # refresh the vuln + fingerprint database
wpscan --version # version and last database update
command -v wpscan # confirm it is on PATH
WPScan is a Ruby tool. Keep the vulnerability database current with wpscan --update, since a stale copy produces stale findings. With Docker the invocation is docker run -it --rm wpscanteam/wpscan --url URL instead of wpscan.
Flags you’ll actually use#
Skim these first; the cheat sheet below is these flags combined.
| Flag | Does | Flag | Does |
|---|---|---|---|
--url | Target site root | --api-token | Vuln database token (free) |
-e | Enumerate (u,ap,vp,at,vt,cb,dbe) | --format | Output format (json, cli) |
--detection-mode | passive / mixed / aggressive | -o | Write results to a file |
--plugins-detection | How plugins are found | -t | Max threads (default 5) |
-U | Username(s) to try | --throttle | Milliseconds between requests |
-P | Password list for a login attack | --random-user-agent | Vary the User-Agent |
--password-attack | wp-login / xmlrpc | --stealthy | Passive + random-UA preset |
--force | Skip the “is WordPress” check | --disable-tls-checks | Ignore a bad cert (labs) |
Cheat sheet#
Start with a bare scan and add one thing at a time, so you can stop at the line that does the job.
# Confirm target first (WPScan is WordPress-only)
whatweb URL # is this actually WordPress?
# Build up an enumeration scan: stop at the line you need
wpscan --url URL # 1. core version, theme, obvious findings
wpscan --url URL --detection-mode passive # 2. + quietest pass, reads the page only
wpscan --url URL -e u # 3. + enumerate users
wpscan --url URL -e vp,vt,u --api-token TOKEN # 4. + vulnerable plugins/themes with refs
# Enumerate (pick what you need; comma-joined)
wpscan --url URL -e u # users (author IDs, default u1-10)
wpscan --url URL -e u1-20 # widen the user ID range
wpscan --url URL -e p # popular plugins only
wpscan --url URL -e ap # all known plugins (slower, thorough)
wpscan --url URL -e vp --api-token TOKEN # only plugins with known vulns
wpscan --url URL -e vt --api-token TOKEN # only themes with known vulns
wpscan --url URL -e cb,dbe # config backups + db exports
wpscan --url URL -e u,ap,at,cb,dbe --api-token TOKEN # broad inventory in one run
# Find what passive detection missed
wpscan --url URL -e ap --plugins-detection aggressive # request known plugin paths directly
# Output and notes
wpscan --url URL -e vp,u --api-token TOKEN -o scans/enum.txt # save human-readable
wpscan --url URL -e vp,u --api-token TOKEN --format json -o run.json # machine-readable
jq '.users | keys' run.json # pull usernames out of saved JSON
# Authorized login attack (loud, scope-gated)
wpscan --url URL -U USERNAME -P WORDLIST --password-attack wp-login # via login form
wpscan --url URL -U admin -P WORDLIST --password-attack xmlrpc # via XML-RPC (faster)
# Lower noise / evade
wpscan --url URL -e vp --stealthy --api-token TOKEN # passive + random UA preset
wpscan --url URL -e vp -t 5 --throttle 500 --random-user-agent # slow, gentle, blended
wpscan --url URL --force # scan past a WAF "not WordPress" block
wpscan --url URL --disable-tls-checks # ignore a self-signed cert (labs)
# Keep current
wpscan --update # refresh vuln + fingerprint database
Command breakdowns#
Each block below explains one situation, the exact command, and what to expect back.
Run a first pass on a confirmed WordPress site#
wpscan --url http://TARGET_IP/ -o scans/basic.txt
Prints a banner, an “Interesting Finding(s)” block, and a WordPress version line if detection succeeds. Low effort and low noise; the baseline before you enumerate anything.
Keep the quietest possible footprint#
wpscan --url URL --detection-mode passive -o scans/passive.txt
Reads only what the page already exposes instead of probing known paths. Fewer findings, far fewer requests, and much less obvious in access logs.
Enumerate usernames#
wpscan --url URL -e u -o scans/users.txt
Walks author IDs (/?author=1, 2, …) and returns the logins it uncovers. This is context for a later authorized login test, not proof of weak passwords. Use -e u1-20 to widen the default u1-10 range.
Find vulnerable plugins and themes#
wpscan --url URL -e vp,vt --api-token TOKEN -o scans/vuln.txt
Detects installed plugins and themes, then matches each version against the vulnerability database. Without --api-token you still get the versions, but no known-issue references print.
Discover plugins passive detection missed#
wpscan --url URL -e ap --plugins-detection aggressive -o scans/plugins.txt
ap checks all known plugins and aggressive requests their paths directly, catching plugins that leave no trace on the page. Louder and slower, but far more thorough.
Build a broad inventory in one run#
wpscan --url URL -e u,ap,at,cb,dbe --api-token TOKEN -o scans/full.txt
Users, all plugins, all themes, config backups, and database exports in a single pass. Re-verify anything version-specific by hand afterward.
Pull exposed config backups and DB exports#
wpscan --url URL -e cb,dbe -o scans/leaks.txt
cb hunts wp-config.php backups (.bak, .old, ~) and dbe looks for exported database dumps. A hit here often hands you credentials directly; fetch the file with curl.
Save machine-readable output#
wpscan --url URL -e vp,u --api-token TOKEN --format json -o scans/run.json
JSON you can parse later without re-scanning. Pair it with jq to extract exactly the fields you want.
Extract usernames from a saved scan#
jq '.users | keys' scans/run.json
Turns the JSON user block into a clean list for your notes or a login test. No new requests hit the target.
Run an authorized password attack via the login form#
wpscan --url URL -U USERNAME -P WORDLIST --password-attack wp-login
Tries each password against wp-login.php. Loud and lockout-prone: run it only when brute force is explicitly in scope. Any valid credential is printed at the end of the run.
Speed up a login attack through XML-RPC#
wpscan --url URL -U admin -P WORDLIST --password-attack xmlrpc
xmlrpc.php accepts many attempts per request, so it is faster than the login form when the endpoint is open. If XML-RPC is disabled, fall back to --password-attack wp-login.
Scan quietly with the stealthy preset#
wpscan --url URL -e vp --stealthy --api-token TOKEN
--stealthy bundles passive detection with a random User-Agent, so you skip the obvious default scanner signature. Add -t 5 --throttle 500 on fragile or production-adjacent gear.
Force a scan past a WAF block#
wpscan --url URL --force -o scans/forced.txt
Skips the “is this WordPress” check and scans anyway. Use it only when you already know it is WordPress and a WAF or hardening is hiding the usual tells.
Ignore a self-signed certificate#
wpscan --url https://TARGET_IP/ --disable-tls-checks
Skips TLS verification so a lab certificate does not abort the scan. Labs only; a bad cert on a real target is itself a finding to note.
Reading the output#
| You see | Meaning | Do next |
|---|---|---|
WordPress version X.Y (confidence) | Detected core release | Confirm it; hidden or spoofed versions are common |
| Plugin/theme + version + confidence | Installed asset found | Low confidence means verify by hand |
[!] title with references | Known vuln matched (needs token) | Read the reference; check the version really matches |
User(s) Identified | Enumerated logins | Context for an authorized login test, not weak-password proof |
Interesting Finding(s) | xmlrpc.php, readme, dir listing, backups | Useful leads, not conclusions |
| Thin or empty output | Not WordPress, a WAF, or too-passive mode | Try --force or a higher --detection-mode |
Confidence is a percentage on every fingerprint; treat anything low as a lead to confirm, not a fact. The “Interesting Finding(s)” block is where exposed endpoints (xmlrpc.php, readme.html, directory listings) surface first.
Troubleshooting#
| Problem | Cause | Fix |
|---|---|---|
command not found | Not installed / stale PATH | Reinstall; open a fresh shell |
| “does not seem to be running WordPress” | Not WordPress, or a WAF hiding tells | Confirm with whatweb; add --force if certain |
| No vulnerability references | No API token | Add --api-token TOKEN, then wpscan --update |
| Scan stalls or times out | Slow host or rate limiting | Lower -t, add --throttle, narrow -e |
Connection refused / unreachable | Wrong URL or host down | Re-check the URL and that the host is up |
SSL/TLS error | Self-signed or bad cert | --disable-tls-checks on lab targets |
Many 403s mid-scan | WAF blocking probes | Lower detection mode, add --random-user-agent |
| Login attack finds nothing fast | XML-RPC disabled or wrong user | Try --password-attack wp-login; confirm the username |
| Stale or wrong vuln data | Old local database | Run wpscan --update |
Gotchas#
- A detected plugin version is a fingerprint, not an exploit. Renamed asset directories, spoofed readme files, and cached pages all lie; confirm the version by hand before you trust a vulnerability reference.
- No
--api-tokenmeans no vulnerability references, ever. You still see plugin and theme versions, but the known-issue lines simply never print, which reads like a clean site when it is not. - The default
-e(no value) enumerates a lot:vp,vt,tt,cb,dbe,u,m. Name exactly what you want (-e uor-e vp) to cut request volume and noise. --password-attack xmlrpc-multicallbatches many passwords per request (tune with--multicall-max-passwords), so it is fast but hammers the box and is trivially logged; plainwp-loginis slower but works when XML-RPC is off.--stealthyis not silent. It only sets passive detection and a random UA; a determined defender still sees the pattern. Pair it with--throttleand a low-t, and never assume you are invisible.- Never paste your API token or any discovered credentials into shared notes, JSON, or screenshots. The token is tied to your account quota and the credentials are live.
Pairs with#
whatweb and wappalyzer confirm the target is WordPress before WPScan runs, so you do not waste a scan on the wrong CMS. nmap finds the web port that led you here in the first place. gobuster and ffuf brute the paths, backups, and hidden directories WPScan does not cover, while curl cleanly pulls any readme, config backup, or database export it flags. jq slices the JSON output into notes. When the CMS is unknown reach for CMSeeK or droopescan; for breadth across every stack nuclei goes wide where WPScan goes deep.