I manage around 3,000 WordPress sites. Paid plugins often get stuck on old versions. Bad license keys, expired subscriptions, broken updaters. I wanted a way to see which plugins were drifting across the fleet and fix them in bulk.

So I built captaincore drift. Run it against a plugin name, and it shows how versions are distributed across all managed sites. Which version is the latest. How many sites are on it. How many have drifted.

Claude Code Session
Austin
While managing thousands of sites, paid WordPress themes and plugins get stuck on older versions all the time. Bad license, bad code, expired subscription. Here is my dream CLI command: captaincore drift --plugin gravity-forms or drift --core or drift --plugin gravity-forms --target 2.9.29
Claude
Great feature idea. A single new command that queries the local SQLite database, already synced with site and environment data including plugin/theme JSON. Three modes: drift --plugin for version distribution, drift --core for WordPress core versions, and a default overview mode showing the top 20 worst-drifting plugins across the fleet.
CaptainCore drift command showing WP All Import Pro version distribution across 83 sites

I added a --steer flag to auto-upgrade drifted sites to the latest version. The tool was working. Then I had an idea that changed what the tool was for.

I challenged Claude to find something suspicious.

The idea was to use the drift command to scan for suspicious plugins across the fleet. I added a --hashes flag that computes file hashes for each plugin installation. If two sites both run Gravity Forms 2.9.30 but have different file hashes, something was changed. That’s when a version tracking tool became a security scanner.

Claude Code Session
Austin
I have a challenge for you. Can you use captaincore drift to locate suspicious looking plugins? You can use the --hashes argument to find sites running divergent versions of a theme or plugin as compared to the rest of the fleet.
Claude
Running hash checks across commonly targeted plugins. The wpmudev-updates plugin stands out: 8 different hashes across 87 sites with 7 unique singleton hashes. Also found contact-form-7, the-events-calendar, and events-calendar-pro pinned to version 9999. That’s a nulled plugin signature. Version set artificially high to block updates.

Version 9999. Someone had manually edited the version numbers to prevent WordPress from ever updating them. One site even had a note in the plugin description: “DO NOT UPDATE!! Breaks the Get a Quote form.” Not malware, but a permanent security hole. These sites would never receive patches.

One outlier against a thousand is not a config difference.

The early hash runs produced a lot of noise. Themes with config differences. Plugins that write to their own directories. Premium themes like Betheme and Avada showed every copy with a different hash, but the differences turned out to be configuration, not tampering.

I needed a way to filter down to the things that matter. The breakthrough was simple: look for a single outlier hash against a large majority.

Claude Code Session
Austin
How about a new strategy. Look for divergents with only a single hash where there is a rather large group of others. That could indicate that the plugin has been tampered with.
Claude
Now we’re talking. The highest-signal finds 1 singleton against 1,000+ identical copies: gravityforms 2.9.30 (1 outlier vs 1,071 matching), elementor-pro 3.35.1 (1 outlier vs 882 matching), cleantalk-spam-protect 6.74 (1 outlier vs 1,265 matching).

One outlier out of a thousand. That’s not a config file difference. Something was deliberately modified.

The first line told me everything.

I SSH’d into the outlier site and diffed Gravity Forms against a known-good copy from our fleet. Only one file was different: gravityforms.php. And the change was small. About 55 lines were injected at the top of the file.

The first line told me everything:

define( 'GRAVITY_MANAGER_URL', 'https://gf.gpltimes.com' );

GPLTimes. A piracy platform that distributes nulled WordPress plugins. The site wasn’t just running a cracked plugin. It was phoning home to a piracy server on every page load.

I dug deeper into the site. It wasn’t just Gravity Forms. The customer had five nulled components installed: the GPLTimes plugin itself, Gravity Forms, the Avada theme, Yoast SEO Premium, and MonsterInsights Pro. All sourced from the same piracy pipeline. All modified with the same pattern.

A note to Anchor Hosting customers.
Do not install nulled plugins. If we find them on your site, we will either remove them or replace them with licensed versions. The rest of this post explains why. It is simply not worth the risk.

Every nulled plugin follows the same three steps.

No original code is modified. The injections are 40 to 55 lines added to the top of a single file per plugin.

Step 1: Fake license injection. Write hardcoded license data to wp_options on every page load using update_option(). This makes the plugin show “Licensed” in the admin UI.

Step 2: API interception. Hook into pre_http_request to catch outbound license validation calls and return spoofed responses. When Gravity Forms tries to verify its key with gravityapi.com, the filter intercepts the request and returns a fake Elite License valid until 2035.

Step 3: Server redirect. Rewrite URL constants and filters to redirect updates, addons, and downloads to *.gpltimes.com servers.

Step 3 is what makes this dangerous.
Steps 1 and 2 are cosmetic. They trick the plugin into showing Licensed in the UI. Step 3 gives a third party the ability to push arbitrary PHP code to the site through WordPress’s own update mechanism. The backdoor isn’t in the code on disk. It’s in who controls the update server.

The code is clean. That’s the point.

I scanned all 1,664 PHP files across the five components. No eval() chains. No obfuscated payloads. No hidden admin users. No web shells. The code is clean, readable PHP throughout.

That’s by design. The modifications are surgically minimal. Clean code makes the nulled versions hard to detect through automated scanning. The rest of the codebase is identical to the legitimate distribution.

But clean code doesn’t mean safe. Four of the five components route update traffic through GPLTimes-controlled infrastructure. The piracy operator can push a malicious update at any time, and WordPress will install it automatically.

Three components phone home on every page load.

The GPLTimes plugin collects every installed plugin and theme, packages it with the site URL and admin email, base64-encodes it, and POSTs it to gpltimes.com/version_check_post.php.

// Get authentication data
$token   = esc_attr(get_option('gplstatus'));
$user_id = esc_attr(get_option('gpltokenid'));
$domain  = get_site_url();
$email   = get_option('admin_email');

// Collect ALL installed plugins and themes
$slugarray = $this->collect_installed_items();

// Package into a single base64-encoded payload
$data_string = implode(',', $slugarray)
    . '@__@' . $token
    . '@__@1@__@' . $domain
    . '@__@' . $email
    . '@__@' . $user_id;

// Send to GPLTimes server
$response = wp_safe_remote_post(
    'https://www.gpltimes.com/version_check_post.php',
    ['body' => ['data' => base64_encode($data_string)]]
);

Gravity Forms sends even more. All plugins with versions, theme info, WordPress and PHP and MySQL versions, form counts, entry counts, locale, and multisite status. All going to gf.gpltimes.com.

Five components, one build pipeline.

Here’s how the five components compare:

GPLTimesGravity FormsAvadaYoast SEOMonsterInsights
ModificationEntire plugin (~30 files)~55 lines in 1 file~37 lines in 1 file~40 lines in 1 file~47 lines in 1 file
GPLTimes redirectgpltimes.comgf.gpltimes.comdl.gpltimes.comNonemonster.gpltimes.com
Data sentAll plugins, themes, email, URLAll plugins, PHP/WP/MySQL, formsNoneNoneSite URL, versions
Update channelHijacked (priority 99M)HijackedBlockedIntactBroken
BackdoorsNoneNoneNoneNoneNone

Yoast SEO Premium is the cleanest of the group. No piracy domain references. No data exfiltration. Updates still come from the legitimate channel. It only spoofs the license check. Still not safe to run, but notably different from the others.

They share a hardcoded license key which appears in both Gravity Forms and MonsterInsights, confirming they come from the same build pipeline.

Avada disables SSL and blocks its own bundled updates.

The Avada null had two things the others didn’t.

First, it rewrites all bundled plugin download URLs to GPLTimes’ CDN and blanks the version field. That means TGMPA’s version comparison never fires. Bundled plugins like Fusion Builder will never show as needing an update.

$tgmpa_instance->plugins[$slug]['source'] =
    "https://dl.gpltimes.com/file/gpltimes/avada/plugins/{$slug}.zip";
$tgmpa_instance->plugins[$slug]['version'] = '';  // blocks updates

Second, it disables SSL certificate verification when downloading demo imports from the piracy CDN. Any network attacker could inject malicious content into those ZIP files.

$response = wp_remote_get(
    "https://dl.gpltimes.com/file/gpltimes/avada/demos/{$demo}.zip",
    [ 'sslverify' => false, 'timeout' => 30 ]
);

GPLTimes overrides every update source at priority 99 million.

The GPLTimes plugin is not nulled. It’s the piracy platform’s own commercial product. The delivery mechanism for all the other nulled plugins.

It hooks into WordPress’s update system at priority 99,999,999 to override all legitimate update sources. Both plugins_api and plugins_api_result are intercepted, meaning it overrides not just WordPress’s default update source but also any custom updater a plugin has registered for itself.

add_filter('site_transient_update_plugins',     [$this, 'inject_update'], 99999999);
add_filter('pre_set_site_transient_update_plugins', [$this, 'inject_update'], 99999999);
add_filter('plugins_api',        [$this, 'provide_info'], 99999999, 3);
add_filter('plugins_api_result', [$this, 'provide_info'], 99999999, 3);

It also has a whitelabel mode that uses file_put_contents() to rewrite its own PHP file header. Resellers can disguise the plugin’s origin. And it can suppress all admin notices site-wide, including security warnings from other plugins.

I automated the patterns and scanned the entire fleet.

After the manual investigation, I built a reusable script called find-nulled-components and deployed it across all 2,900+ sites. The script checks for GPL club domain references across 30+ known piracy domains, known nulled management plugins, version 9999 plugins, pre_http_request interception of license APIs, and hardcoded fake license activation.

I tested it on the compromised site and 12 clean sites to verify no false positives. Then deployed fleet-wide with captaincore ssh @all.

Cleaning up one site took five steps.

Step 1
Remove all five nulled components. GPLTimes plugin, nulled Gravity Forms, Avada, Yoast SEO Premium, and MonsterInsights Pro.
Step 2
Replace with legitimate copies. Pulled clean versions from other fleet sites using WP-CLI --force install.
Step 3
Clean residual data. Removed piracy keys from wp_options: rg_gforms_key, fusion_registration_data, monsterinsights_license, gplstatus, gpltokenid, and the plaintext username and password keys.
Step 4
Update WordPress core. The site was on 6.7.5. Updated to 6.9.4.
Step 5
Change all admin passwords. GPLTimes had access to the admin email address. Reviewed user accounts for unauthorized additions.

One different hash out of a thousand is worth investigating every time.

A tool I built for tracking version sprawl turned into a security scanner. The --hashes flag on captaincore drift surfaced something that traditional malware scanners would miss. There were no backdoors to find. No obfuscated code. No suspicious file names. Just 40 lines of clean PHP that quietly handed control of the update pipeline to a piracy operator.

If you manage WordPress sites at scale, version tracking with file integrity is one of the most powerful signals you have.