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 example http://TARGET_IP/
  • Enumeration (-e): what to discover - users u, plugins p/ap/vp, themes t/at/vt, config backups cb, db exports dbe
  • 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 hit wp-login.php and xmlrpc.php
  • Password attack (-P with -U): tries logins via wp-login or xmlrpc; loud and lockout-prone
  • User IDs: WordPress exposes authors by numeric id, so u1-5 walks /?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.

FlagDoesFlagDoes
--urlTarget site root--api-tokenVuln database token (free)
-eEnumerate (u,ap,vp,at,vt,cb,dbe)--formatOutput format (json, cli)
--detection-modepassive / mixed / aggressive-oWrite results to a file
--plugins-detectionHow plugins are found-tMax threads (default 5)
-UUsername(s) to try--throttleMilliseconds between requests
-PPassword list for a login attack--random-user-agentVary the User-Agent
--password-attackwp-login / xmlrpc--stealthyPassive + random-UA preset
--forceSkip the “is WordPress” check--disable-tls-checksIgnore 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 seeMeaningDo next
WordPress version X.Y (confidence)Detected core releaseConfirm it; hidden or spoofed versions are common
Plugin/theme + version + confidenceInstalled asset foundLow confidence means verify by hand
[!] title with referencesKnown vuln matched (needs token)Read the reference; check the version really matches
User(s) IdentifiedEnumerated loginsContext for an authorized login test, not weak-password proof
Interesting Finding(s)xmlrpc.php, readme, dir listing, backupsUseful leads, not conclusions
Thin or empty outputNot WordPress, a WAF, or too-passive modeTry --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#

ProblemCauseFix
command not foundNot installed / stale PATHReinstall; open a fresh shell
“does not seem to be running WordPress”Not WordPress, or a WAF hiding tellsConfirm with whatweb; add --force if certain
No vulnerability referencesNo API tokenAdd --api-token TOKEN, then wpscan --update
Scan stalls or times outSlow host or rate limitingLower -t, add --throttle, narrow -e
Connection refused / unreachableWrong URL or host downRe-check the URL and that the host is up
SSL/TLS errorSelf-signed or bad cert--disable-tls-checks on lab targets
Many 403s mid-scanWAF blocking probesLower detection mode, add --random-user-agent
Login attack finds nothing fastXML-RPC disabled or wrong userTry --password-attack wp-login; confirm the username
Stale or wrong vuln dataOld local databaseRun 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-token means 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 u or -e vp) to cut request volume and noise.
  • --password-attack xmlrpc-multicall batches many passwords per request (tune with --multicall-max-passwords), so it is fast but hammers the box and is trivially logged; plain wp-login is slower but works when XML-RPC is off.
  • --stealthy is not silent. It only sets passive detection and a random UA; a determined defender still sees the pattern. Pair it with --throttle and 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.

Find us elsewhere

Merch, stickers, and moreSupport the work at the Solvere Labs shop