WPProbe: A Pragmatic Approach to Detecting WordPress Plugins
Table of Contents
Introduction
WPProbe is a tool I built to solve a simple problem: detecting installed WordPress plugins in a reliable and quiet way. In this post, I explain how the tool came to life, its strengths, its limits, and why this REST-based idea is still not widely used, even though it works well.
Why WPProbe?
Honestly, I still don’t get why WordPress shows all REST endpoints by default when debug is on. Even if some bug bounty programs reward people for finding plugin vulnerabilities, there seems to be very little awareness of the attack surface exposed by this REST feature, which is on by default.
I’m not saying I’m a security expert, there are clearly people way more skilled than me, but I think this default setting is risky. Especially when you know that WordPress runs around 47% of all websites (based on recent numbers). That kind of popularity should come with stronger default protections.
And one quick reminder: if you get an idea, don’t throw it away too fast. WPProbe is proof of that. I had the idea almost one year before I built it, but I forgot about it. Then one day I remembered and just built it, fast and simple. Sometimes, old ideas are worth finishing.
Let’s be real: WPProbe isn’t magic. I was just bored, like often, and decided to work on something random. This time, it turned out to be useful. People liked it, maybe because it’s simple and solves a real problem.
I had no clue if it would even work. I try lots of things that don’t lead anywhere. But even failed projects teach me something. This time, it worked.
While working on WordPress modules for Metasploit, I saw something strange: a default WordPress install (like the official Docker image) shows all REST endpoints at ?rest_route=/. That’s on by default and lets anyone see every route from WordPress core and plugins.
This is great for passive fingerprinting, but very few tools use it. At the time, I wanted to help LeakIX scan WordPress sites without overloading servers. Tools like wpfinger work, but they’re too aggressive, sending too many requests. I wanted something calmer but still efficient.
Even WPScan, as far as I know, doesn’t use this method directly (I didn’t test their API). But it’s strange, because many recent critical CVEs can be used through REST endpoints.
Building the Database
V1: The Original Single-Container Approach (2025)
The first version was simple: one Docker container, processing plugins one by one.
flowchart LR
subgraph V1["๐ณ Single Container"]
WP[WordPress + MariaDB]
end
SVN[plugins.svn.wordpress.org] -->|100k plugins| WP
WP -->|sequential| JSONL[(routes.jsonl)]
The process:
- Fetch plugin list from WordPress SVN
- Install plugin via WP-CLI
- Run PHP script to extract REST routes
- Delete plugin, repeat
The PHP script (V1 - no baseline):
<?php
define('WP_USE_THEMES', false);
require_once 'wp-load.php';
global $wp_rest_server;
$wp_rest_server = new WP_REST_Server();
do_action('rest_api_init');
$blacklisted_routes = ["/", "/wp/v2", "/wp/v2/posts"];
$routes = $wp_rest_server->get_routes();
$filtered_routes = [];
foreach ($routes as $route => $details) {
if (!in_array($route, $blacklisted_routes)) {
$filtered_routes[] = addslashes($route);
}
}
echo json_encode(array_values($filtered_routes), JSON_PRETTY_PRINT);
?>
Problems with V1:
- โฑ๏ธ 2 weeks to scan everything
- ๐ฅ When the container crashed, we had to manually restart
- ๐ No way to detect corruption (bad plugin leaves traces โ pollutes next scans)
- ๐ Sequential = painfully slow
Technical Background
I’m not a Go developer. I have little experience with the language. So yes, the WPProbe code is probably not perfect, but it works pretty well. The main goal was to make something that just works.
At first, WPProbe was a quick Python PoC. But it was way too slow. I also wanted to learn Go and make something that could be used by LeakIX in production. I didn’t want something noisy like wpfinger. No judgment here, I actually admire the work LeakIX has done, but in this specific case, it felt too risky for wide use.
I’m not a sysadmin either. The first version used one single Docker instance for everything. The full scan took almost two weeks (because of bad plugins, container crashes, slow DB startup, etc.). This has since been replaced by a much faster V2 system (see below).
How WPProbe Works (and What It Can’t Do)
WPProbe has 3 steps:
- It fetches all REST endpoints via
?rest_route=/(orwp-jsonalternative) - It matches those to known plugin signatures from a local database
- If possible, it tries to read
readme.txt,README.txtto find the version, then checks for known CVEs via Wordfence
When I started, WPProbe could detect about 900 plugins. After the V1 lab, it supported ~3,000. With V2, it now has 3,400+ plugins (and growing). Note that WordPress.org keeps adding new plugins over time, so part of this increase comes from new plugins released between 2025 and 2026.
All the official plugins from WordPress.org are supported. But non-official ones are hard to handle automatically. If you know a trick to find and scan those, I’d love to hear it. Only ~5% of plugins expose REST routes, but that’s still a solid database for passive detection.
Plugins may have slightly different endpoints depending on the version. So WPProbe uses a confidence score, based on how many routes match, unless we get the version, then it’s accurate.
Some plugins use the same routes, which can give false positives. That’s normal. I didn’t want to remove all shared routes, since some are valid. So I left them as-is. That said, for bigger plugins like Jetpack or WooCommerce, I cleaned some of the noise using bash scripts.
Vulnerability Info (Wordfence Matching)
Thanks to the free Wordfence API, WPProbe can say if a plugin version has known vulnerabilities.
To get the version, WPProbe tries to fetch the readme file with different cases: readme.txt, Readme.txt, README.txt (up to 3 requests per plugin). It then parses the Stable tag: or Version: field.
Total requests in stealthy mode:
- 1 request: HTML page (discover plugins from
<script>and<link>tags) - 2 requests: REST API (
/?rest_route=/and/wp-jsonin parallel) - Up to 3 requests per detected plugin: readme files for version detection
So for a site with 5 detected plugins, that’s only ~18 requests max. Compare that to bruteforce scanners that send thousands.
References & Ideas
The idea came from building WordPress modules for Metasploit. It started while I was working on a module, actually several, where I used this handy technique to check which endpoints were available. It helped me reproduce PoCs locally before writing the module itself. But I also heard it mentioned in Critical Thinking Podcast - Episode 55 by a Wordpress Security Researcher. That pushed me to work on it.
I also took some ideas from wpfinger
, made by the LeakIX team. For the local Wordfence vulnerability database, I actually reused that idea, big shout-out to Gregory Boddin, the CTO of LeakIX, who developed it. It was a smart move that I adapted into WPProbe.
V2: Parallel Workers with Auto-Recovery (2026)
The V1 approach worked but was too slow and fragile. So I rebuilt everything from scratch: 50 parallel workers, automatic crash recovery, and a baseline/diff system to ensure clean data.
flowchart LR
subgraph V1["โ V1: Single Container"]
OLD[1 WordPress]
end
subgraph V2["โ
V2: Worker Pool"]
W1[WP 1]
W2[WP 2]
WN[WP N...]
end
V1 -->|"2 weeks ๐"| R1[3k plugins]
V2 -->|"2-3 days ๐"| R2[3.4k+ plugins]
Architecture
Instead of one container doing everything sequentially, I now run up to 50 parallel WordPress workers managed by a Python orchestrator. Each worker is an isolated WordPress instance that can scan plugins independently.
flowchart TB
subgraph Orchestrator["๐ฏ Python Orchestrator"]
M[main.py]
TP[ThreadPoolExecutor]
SC[PluginScanner]
end
subgraph Workers["๐ณ Docker Worker Pool"]
subgraph DB["๐๏ธ Shared MariaDB"]
MDB[(MariaDB)]
end
W1[WordPress:8001]
W2[WordPress:8002]
WN[WordPress:800N...]
end
subgraph Storage["๐พ Storage"]
JSONL[scanned_plugins.jsonl]
STATE[scan_state.json]
end
M --> TP
TP --> SC
SC -->|installs plugin| Workers
W1 & W2 & WN --> MDB
SC --> JSONL
SC --> STATE
Baseline/Diff Extraction
The old single-container method worked, but it had issues: we couldn’t detect when the environment was corrupted, and when it happened, we had to manually restart the only container we had. This was slow and painful.
The new system uses baseline/diff extraction. The idea is simple: how do we know which routes belong to a plugin? We capture all routes before installing it, then capture again after, and keep only the difference.
sequenceDiagram
participant S as ๐ Scanner
participant W as ๐ณ Worker
participant PHP as ๐ extract_routes.php
Note over S,PHP: 1๏ธโฃ BASELINE - Before plugin install
S->>W: php extract_routes.php
W->>PHP: Load WordPress core
PHP-->>S: ["wp/v2", "wp/v2/posts", ...] (42 routes)
Note right of S: Store as baseline
Note over S,PHP: 2๏ธโฃ INSTALL - Add the plugin
S->>W: cp plugin + wp plugin activate
W-->>S: Plugin active โ
Note over S,PHP: 3๏ธโฃ DIFF - Extract NEW routes only
S->>W: php extract_routes.php {baseline_b64}
W->>PHP: Load WP + plugin routes
PHP->>PHP: array_diff(current, baseline)
PHP-->>S: ["plugin/v1", "plugin/v1/endpoint"] (2 NEW routes)
Note over S,PHP: 4๏ธโฃ CLEANUP
S->>W: wp plugin uninstall --deactivate
This ensures we only capture routes actually added by the plugin, not WordPress core routes or leftovers from previous scans.
Worker Corruption Handling
PHP fatal errors from buggy plugins can corrupt a worker’s state. The new system detects this (exit code 255) and handles it gracefully:
flowchart TD
A[๐ Plugin Scan] --> B{Exit Code?}
B -->|0| C[โ
Success]
B -->|255| D{wp eval check}
D -->|OK| E[Plugin crashed
Worker OK]
D -->|FAIL| F[Worker corrupted]
F --> G[๐งน Cleanup]
G --> H{Fixed?}
H -->|No| I[โ Remove worker]
H -->|Yes| J[๐ Retry]
E --> J
I --> K{Workers < 5?}
K -->|Yes| L[๐ Reinit cluster]
K -->|No| J
Results: V1 vs V2
| Metric | V1 (2025) | V2 (2026) |
|---|---|---|
| Scan time | ~2 weeks | ~2-3 days |
| Workers | 1 | 50 (dynamic) |
| Plugins detected | ~3,000 | 3,400+ |
| Routes collected | ~25,000 | 30,000+ |
| Crash recovery | Manual restart | Automatic |
| Corruption detection | None | wp eval health check |
The scanner dynamically calculates workers based on available RAM and can reinitialize the entire cluster if too many workers die. Running stable with 49 workers on 32GB RAM.
Final Words
WPProbe is a small but useful tool for scanning WordPress in a quiet and smart way.
I plan to keep improving it:
- Update the plugin signature list when needed
I’m not sure if I’ll keep working on it completely on my own. Help is always welcome, whether it’s sharing ideas or contributing code. The project is under the MIT license, but please don’t abuse that. Even a small mention or credit means a lot. Between open source fans, it’s important to give credit where it’s due.
Source code: https://github.com/Chocapikk/WPProbe