|
#!/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." |