Skip to content

Instantly share code, notes, and snippets.

@Braunson
Created April 17, 2026 18:48
Show Gist options
  • Select an option

  • Save Braunson/b0effd822cabe3ab7e5ccf85345f949e to your computer and use it in GitHub Desktop.

Select an option

Save Braunson/b0effd822cabe3ab7e5ccf85345f949e to your computer and use it in GitHub Desktop.
IOC scanner and remediation for the April 2026 Essential Plugin WordPress supply-chain attack

IOC scanner for the April 2026 Essential Plugin WordPress supply-chain attack

Scanner Only Script

Read-only bash scanner that checks a multi-site WordPress server for indicators of compromise from the Essential Plugin / wpos-analytics backdoor disclosed April 2026 (31 plugins, 8-month dormant backdoor, wp-config.php injection serving SEO spam to Googlebot via an Ethereum-resolved C2).

Scans each site for: affected plugin slugs, presence of the wpos-analytics/ backdoor module, backdoor code traces (wpos_analytics_anl, fetch_ver_info, etc.), wp-config.php IOC strings, and the rogue wp-comments-posts.php dropper.

Reports per-plugin status (backdoor present / patched / module removed) and a summary count. Assumes layout /var/www/html/<site>/docs/wp-content/plugins — edit the glob for other layouts.

Based on Austin Ginder's writeup: https://anchor.host/someone-bought-30-wordpress-plugins-and-planted-a-backdoor-in-all-of-them/

Scan + Remediation Script

Companion remediation script to scan-essentialplugin-compromise.sh. Installs CaptainCore's pre-built patched plugin ZIPs (11 available) via wp-cli across a multi-site WordPress server, and removes the rogue wp-comments-posts.php dropper.

Defaults to dry-run - pass --apply to execute. Skips plugins already on a -patched build, flags affected plugins that have no patched build available, and never auto-edits wp-config.php (injections are reported for manual cleanup since a bad sed could take down every site). Assumes layout /var/www/html/<site>/docs/wp-content/plugins and requires wp-cli.

Recommended workflow: run the scanner → review output → dry-run this → eyeball → --apply → re-scan to confirm clean.

Patched builds and attack writeup by Austin Ginder: https://anchor.host/someone-bought-30-wordpress-plugins-and-planted-a-backdoor-in-all-of-them/


Usage

chmod +x ~/remediate-essentialplugin-compromise.sh
sudo ~/remediate-essentialplugin-compromise.sh /var/www/html           # dry run
sudo ~/remediate-essentialplugin-compromise.sh /var/www/html --apply   # execute
#!/usr/bin/env bash
# scan-essentialplugin-compromise.sh
# IOC scanner for the April 2026 Essential Plugin supply-chain attack
# Ref: https://anchor.host/someone-bought-30-wordpress-plugins-and-planted-a-backdoor-in-all-of-them/
# Usage: sudo ./scan-essentialplugin-compromise.sh [/var/www/html]
set -uo pipefail
BASE="${1:-/var/www/html}"
PLUGINS=(
accordion-and-accordion-slider album-and-image-gallery-plus-lightbox
audio-player-with-playlist-ultimate blog-designer-for-post-and-widget
countdown-timer-ultimate featured-post-creative footer-mega-grid-columns
hero-banner-ultimate html5-videogallery-plus-player
meta-slider-and-carousel-with-lightbox popup-anything-on-click
portfolio-and-projects post-category-image-with-grid-and-slider
post-grid-and-filter-ultimate preloader-for-website
product-categories-designs-for-woocommerce sp-faq
sliderspack-all-in-one-image-sliders sp-news-and-widget
styles-for-wp-pagenavi-addon ticker-ultimate timeline-and-history-slider
woo-product-slider-and-carousel-with-category wp-blog-and-widgets
wp-featured-content-and-slider wp-logo-showcase-responsive-slider
wp-logo-showcase-responsive-slider-slider wp-responsive-recent-post-slider
wp-slick-slider-and-image-carousel wp-team-showcase-and-slider
wp-testimonial-with-widget wp-trending-post-slider-and-widget
)
echo "================================================================================"
echo " Essential Plugin Supply-Chain Attack Scanner"
echo " Base: $BASE/*/docs/wp-content/plugins"
echo "================================================================================"
site_hits=0; config_hits=0; dropper_hits=0; backdoor_hits=0
for plugins_dir in "$BASE"/*/docs/wp-content/plugins; do
[ -d "$plugins_dir" ] || continue
wp_root="$(dirname "$(dirname "$plugins_dir")")"
site="$(basename "$(dirname "$wp_root")")"
wp_config="$wp_root/wp-config.php"
hit=0
# 1. Plugin presence + backdoor module check
for slug in "${PLUGINS[@]}"; do
plugin_path="$plugins_dir/$slug"
[ -d "$plugin_path" ] || continue
hit=1
main_file=$(grep -rlE '^\s*\*?\s*Plugin Name:' "$plugin_path" 2>/dev/null | head -n1)
version="?"
[ -n "$main_file" ] && version=$(grep -iE '^\s*\*?\s*Version:' "$main_file" | head -n1 | sed -E 's/.*Version:\s*//I' | tr -d '\r')
if [ -d "$plugin_path/wpos-analytics" ]; then
status="!! BACKDOOR MODULE PRESENT"
backdoor_hits=$((backdoor_hits+1))
elif grep -rqE 'wpos_analytics_anl|fetch_ver_info|Plugin Wpos Analytics Data Starts|analytics\.essentialplugin\.com' "$plugin_path" 2>/dev/null; then
status="!! BACKDOOR CODE TRACES"
backdoor_hits=$((backdoor_hits+1))
elif [[ "$version" == *-patched ]]; then
status="patched"
else
status="module removed (verify)"
fi
printf " [%s] %-46s v%-14s %s\n" "$site" "$slug" "$version" "$status"
done
# 2. wp-config.php injection
if [ -f "$wp_config" ]; then
size=$(stat -c%s "$wp_config" 2>/dev/null || stat -f%z "$wp_config" 2>/dev/null || echo 0)
if grep -qE 'essentialplugin|wp-comments-posts\.php|eth_call|0x[a-fA-F0-9]{40}' "$wp_config" 2>/dev/null; then
echo " [$site] !! wp-config.php CONTAINS IOC STRINGS (${size} bytes)"
config_hits=$((config_hits+1)); hit=1
elif [ "$size" -gt 8000 ]; then
echo " [$site] ?? wp-config.php is ${size} bytes — review manually"
fi
fi
# 3. Rogue dropper (note the extra 's')
if [ -f "$wp_root/wp-comments-posts.php" ]; then
echo " [$site] !! ROGUE FILE: wp-comments-posts.php (backdoor dropper)"
dropper_hits=$((dropper_hits+1)); hit=1
fi
[ $hit -eq 1 ] && site_hits=$((site_hits+1))
done
echo "--------------------------------------------------------------------------------"
echo " Sites with affected plugins or IOCs : $site_hits"
echo " Plugins with backdoor still present : $backdoor_hits"
echo " wp-config.php injections detected : $config_hits"
echo " Rogue wp-comments-posts.php files : $dropper_hits"
echo "================================================================================"
#!/usr/bin/env bash
# remediate-essentialplugin-compromise.sh
# Companion to scan-essentialplugin-compromise.sh
# Installs CaptainCore patched builds for affected plugins + removes rogue dropper.
# Based on Austin Ginder's writeup:
# https://anchor.host/someone-bought-30-wordpress-plugins-and-planted-a-backdoor-in-all-of-them/
#
# DEFAULTS TO DRY RUN. Pass --apply to execute changes.
# Usage: sudo ./remediate-essentialplugin-compromise.sh [/var/www/html] [--apply]
set -uo pipefail
BASE="/var/www/html"
APPLY=0
for arg in "$@"; do
case "$arg" in
--apply) APPLY=1 ;;
/*) BASE="$arg" ;;
-h|--help)
echo "Usage: $0 [base_path] [--apply]"
echo " base_path Root directory containing sites (default: /var/www/html)"
echo " --apply Execute changes (default is dry-run)"
exit 0
;;
*) echo "Unknown arg: $arg"; exit 1 ;;
esac
done
# slug -> patched ZIP URL (only the 11 CaptainCore has published patches for)
declare -A PATCHED=(
[countdown-timer-ultimate]="https://plugins.captaincore.io/countdown-timer-ultimate-2.6.9.1-patched.zip"
[popup-anything-on-click]="https://plugins.captaincore.io/popup-anything-on-click-2.9.1.1-patched.zip"
[wp-testimonial-with-widget]="https://plugins.captaincore.io/wp-testimonial-with-widget-3.5.1-patched.zip"
[wp-team-showcase-and-slider]="https://plugins.captaincore.io/wp-team-showcase-and-slider-2.8.6.1-patched.zip"
[sp-faq]="https://plugins.captaincore.io/sp-faq-3.9.5.1-patched.zip"
[timeline-and-history-slider]="https://plugins.captaincore.io/timeline-and-history-slider-2.4.5.1-patched.zip"
[album-and-image-gallery-plus-lightbox]="https://plugins.captaincore.io/album-and-image-gallery-plus-lightbox-2.1.8.1-patched.zip"
[sp-news-and-widget]="https://plugins.captaincore.io/sp-news-and-widget-5.0.6-patched.zip"
[wp-blog-and-widgets]="https://plugins.captaincore.io/wp-blog-and-widgets-2.6.6.1-patched.zip"
[featured-post-creative]="https://plugins.captaincore.io/featured-post-creative-1.5.7-patched.zip"
[post-grid-and-filter-ultimate]="https://plugins.captaincore.io/post-grid-and-filter-ultimate-1.7.4-patched.zip"
)
# Full list of affected plugins (patched + unpatched)
PLUGINS=(
accordion-and-accordion-slider
album-and-image-gallery-plus-lightbox
audio-player-with-playlist-ultimate
blog-designer-for-post-and-widget
countdown-timer-ultimate
featured-post-creative
footer-mega-grid-columns
hero-banner-ultimate
html5-videogallery-plus-player
meta-slider-and-carousel-with-lightbox
popup-anything-on-click
portfolio-and-projects
post-category-image-with-grid-and-slider
post-grid-and-filter-ultimate
preloader-for-website
product-categories-designs-for-woocommerce
sp-faq
sliderspack-all-in-one-image-sliders
sp-news-and-widget
styles-for-wp-pagenavi-addon
ticker-ultimate
timeline-and-history-slider
woo-product-slider-and-carousel-with-category
wp-blog-and-widgets
wp-featured-content-and-slider
wp-logo-showcase-responsive-slider
wp-logo-showcase-responsive-slider-slider
wp-responsive-recent-post-slider
wp-slick-slider-and-image-carousel
wp-team-showcase-and-slider
wp-testimonial-with-widget
wp-trending-post-slider-and-widget
)
command -v wp >/dev/null || { echo "ERROR: wp-cli not found. Install it first: https://wp-cli.org/"; exit 1; }
MODE="[DRY RUN]"
[ $APPLY -eq 1 ] && MODE="[APPLY]"
echo "================================================================================"
echo " Essential Plugin Remediation $MODE"
echo " Base: $BASE"
[ $APPLY -eq 0 ] && echo " (Re-run with --apply to execute changes)"
echo "================================================================================"
run() {
echo " > $*"
[ $APPLY -eq 1 ] && "$@"
}
manual=()
sites_scanned=0
patches_applied=0
droppers_removed=0
for plugins_dir in "$BASE"/*/docs/wp-content/plugins; do
[ -d "$plugins_dir" ] || continue
wp_root="$(dirname "$(dirname "$plugins_dir")")"
site="$(basename "$(dirname "$wp_root")")"
sites_scanned=$((sites_scanned+1))
site_has_action=0
# Collect per-site actions first so we only print the site header if needed
site_output=""
# 1. Rogue dropper file (note the plural 's' — core is singular)
rogue="$wp_root/wp-comments-posts.php"
if [ -f "$rogue" ]; then
site_output+=" Rogue dropper found: $rogue\n"
if [ $APPLY -eq 1 ]; then
rm -f "$rogue" && site_output+=" > rm -f $rogue [done]\n"
else
site_output+=" > rm -f $rogue\n"
fi
droppers_removed=$((droppers_removed+1))
site_has_action=1
fi
# 2. Patch or flag each affected plugin
for slug in "${PLUGINS[@]}"; do
plugin_path="$plugins_dir/$slug"
[ -d "$plugin_path" ] || continue
main_file=$(grep -rlE '^\s*\*?\s*Plugin Name:' "$plugin_path" 2>/dev/null | head -n1)
version=""
[ -n "$main_file" ] && version=$(grep -iE '^\s*\*?\s*Version:' "$main_file" | head -n1 | sed -E 's/.*Version:\s*//I' | tr -d '\r')
if [[ "$version" == *-patched ]]; then
site_output+=" $slug already patched (v$version) — skip\n"
site_has_action=1
continue
fi
if [ -n "${PATCHED[$slug]:-}" ]; then
site_output+=" Patching $slug (current v$version)\n"
cmd="wp --path=$wp_root --allow-root plugin install ${PATCHED[$slug]} --force"
if [ $APPLY -eq 1 ]; then
if wp --path="$wp_root" --allow-root plugin install "${PATCHED[$slug]}" --force >/dev/null 2>&1; then
site_output+=" > $cmd [done]\n"
patches_applied=$((patches_applied+1))
else
site_output+=" > $cmd [FAILED — check manually]\n"
manual+=("$site: $slug (wp-cli install failed)")
fi
else
site_output+=" > $cmd\n"
fi
site_has_action=1
else
site_output+=" !! $slug (v$version) — no patched build available\n"
manual+=("$site: $slug (deactivate/remove, or strip wpos-analytics/ manually)")
site_has_action=1
fi
done
# 3. wp-config.php IOCs — flag only, never auto-edit
wp_config="$wp_root/wp-config.php"
if [ -f "$wp_config" ] && grep -qE 'essentialplugin|wp-comments-posts\.php|eth_call|0x[a-fA-F0-9]{40}' "$wp_config" 2>/dev/null; then
size=$(stat -c%s "$wp_config" 2>/dev/null || stat -f%z "$wp_config" 2>/dev/null || echo "?")
site_output+=" !! wp-config.php contains IOC strings (${size} bytes) — MANUAL cleanup required\n"
manual+=("$site: clean injection from $wp_config")
site_has_action=1
fi
if [ $site_has_action -eq 1 ]; then
echo "--- $site ---"
printf "%b" "$site_output"
fi
done
echo "================================================================================"
echo " Sites scanned : $sites_scanned"
echo " Patches $([ $APPLY -eq 1 ] && echo 'applied' || echo 'to apply') : $patches_applied"
echo " Droppers $([ $APPLY -eq 1 ] && echo 'removed' || echo 'to remove'): $droppers_removed"
echo "================================================================================"
if [ ${#manual[@]} -gt 0 ]; then
echo " Manual actions still required:"
printf ' - %s\n' "${manual[@]}"
echo "================================================================================"
fi
[ $APPLY -eq 0 ] && echo "Dry run complete. Re-run with --apply to execute."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment