One of the most common WordPress malware patterns I’ve cleaned in the last two years isn’t a webshell or a credential stealer — it’s a “fake plugin” or “fake theme.” The attacker creates a directory in wp-content/plugins/ or wp-content/themes/ with a random-looking name like upndgxlgql, cxdymjztcu, or natewyrywax, drops a few small PHP files in it (typically index.php, js.php, style.php, plus a main file matching the directory name), and that’s their persistence. WordPress treats it like any other plugin; the contents are what matters.
The pattern is consistent enough across families that you can detect them in bulk with a single shell command. Here’s what to look for, why it works, and how to clean a multi-site infection without misclassifying any legitimate plugin.
The shape of a fake plugin
Real WordPress plugins follow conventions: descriptive slugs (woocommerce, jetpack, contact-form-7), readable plugin metadata, predictable file structures, and an author and version that show up in the WP plugin admin UI.
Fake plugins, by contrast, are remarkably consistent in their own way:
- Directory name: 8–12 lowercase characters, no dashes or underscores, no English-meaning.
bpvpfwudbm,cxdymjztcu,uturasagah,ujosakij,upndgxlgql. - File set: a tiny number of files — typically
index.php(6 bytes — just<?php+ space),js.php(50–80 bytes — a singleprint()call with random keyword),style.css(35 bytes — a fake “Theme Name: Twenty Fifteen” header),style.php(~3 KB — the actual obfuscated payload), and one matching the directory name (e.g.cxdymjztcu/cxdymjztcu.php) with a fake plugin header. - Plugin header: looks legitimate at first glance —
Plugin Name: cxdymjztcu,Author: Carlos SebastianorRoss Larry,Version: 2.12.1,License: GPL2. Different families pick different fake author names. - Total directory size: rarely above 5 KB. A real plugin is usually 50 KB+ minimum.
The “fake theme” variant has the same structure but lives in wp-content/themes/ and includes a style.css with a fake Theme Name: header — usually copying a popular default like Twenty Fifteen.
A one-shot detection command
# Across every WP install on the box, find plugin/theme directories whose
# name matches the random-string pattern AND aren't WP defaults / known plugins.
find /var/www -maxdepth 6 -type d \
\( -path "*/wp-content/plugins/*" -o -path "*/wp-content/themes/*" \) \
-regextype posix-extended \
-regex ".*/(wp-content/(plugins|themes))/[a-z]{8,12}$" \
-printf "%TY-%Tm-%Td %s %p\n" 2>/dev/null | sortThe regex catches lowercase-only 8–12-char directory names. The downside is it will also match a few WordPress defaults and legitimate plugins (twentytwenty, elementor, jetpack, etc.). Those need a whitelist filter — I keep a Python helper for this on shared-hosting servers:
WP_DEFAULTS = {'twentyten','twentyeleven','twentytwelve','twentythirteen',
'twentyfourteen','twentyfifteen','twentysixteen','twentyseventeen',
'twentyeighteen','twentynineteen','twentytwenty','twentytwentyone',
'twentytwentytwo','twentytwentythree','twentytwentyfour','twentytwentyfive'}
LEGIT_PLUGINS = {'woocommerce','elementor','jetpack','akismet','wordfence','mailpoet',
'flamingo','updraftplus','litespeed-cache','contact-form-7'}
# everything else with that name pattern is suspectConfirming the malware vs a false positive
Once you’ve got candidates, the confirmation is fast — three signals, any two of which together make it a hit:
- The “Carlos Sebastian” / “Ross Larry” / similar fake-author header. A real plugin author has a website (
Author URI: https://...) that resolves to something legitimate. Fake plugins use generic names with a domain that’s either a fresh registration or a typosquat. - An
index.phpthat’s only<?phpfollowed by 2 blank lines. Real plugins use the canonical// Silence is golden.stub, which is 18 bytes. The 6-byte version is a malware fingerprint. - A
style.phpwith the obfuscation patternfunction XX6() { echo 'XX7'; }— these random function names ending in 6 / outputting strings ending in 7 are a consistent signature of the SEO-spam family of WordPress malware. Easy to grep:grep -rlE "function [a-z]{2}6\\s*\\(\\s*\\)" path/to/plugin
If a directory matches all three, it’s not a false positive — it’s the same family of WordPress malware that’s been active since at least 2023.
Cleaning
Don’t just delete. Quarantine — preserve the files so you can see what changed if the same campaign comes back. The cleanup is two layers:
# 1. Quarantine the directories
QDIR=/root/wp-malware-quarantine-$(date +%F)
mkdir -p "$QDIR"
mv /var/www/site/wp-content/plugins/cxdymjztcu "$QDIR/"
mv /var/www/site/wp-content/themes/bfrbbnrcmk "$QDIR/"
# 2. Remove from active_plugins option (so WP doesn't try to load them)
mysql -uroot db -e "
UPDATE wp_options
SET option_value = REPLACE(option_value, '\"cxdymjztcu/cxdymjztcu.php\";', '')
WHERE option_name = 'active_plugins';"
# WARNING: that REPLACE() can leave the serialized array malformed.
# Better is to unserialize / remove element / re-serialize via a small PHP script.
# I learned that one the hard way.The note in the code comment is genuine — using SQL REPLACE() on the serialized active_plugins string corrupts the array length header in the serialized form, and PHP’s unserialize() then returns false, which means get_option('active_plugins') returns nothing, which means no plugins load at all. The site keeps returning HTTP 200 (themes work without plugins) so you don’t notice immediately, but features like LiteSpeed Cache, WooCommerce, and Mailpoet silently stop running. Use a proper unserialize / modify / serialize round-trip instead.
After cleanup: how it got there
Fake plugins / themes are persistence, not entry. The attacker got admin access first — usually via outdated WP core, a vulnerable plugin, or a brute-forced wp-login.php — then dropped these directories so they can come back later even if you change passwords. Cleaning the fake plugin without finding the entry vector means you’ll be cleaning the same fake plugin again in three weeks. Wordfence’s wfissues table is usually the fastest way to find the entry vector.
Once you’ve got the pattern, finding these is a single find + grep on a server. The hard part isn’t detecting them — it’s not deleting a legitimate plugin while you’re at it.
