The malware scanner flagged gambling spam — 62 casino doorway pages stuffed with Italian and Indonesian SEO content. Standard cleanup, or so it seemed. But buried inside a plugin directory named after a legitimate form builder addon was something far more dangerous: a fully architected data exfiltration suite that had been silently uploading the site’s passwords, database credentials, and environment variables to a public JSON storage API every hour.
The spam was loud. The exfiltration was silent. And it had been running for weeks.
The Disguise
The plugin was named forminator-donations-buttons — designed to look like a companion addon for Forminator, a popular WordPress form plugin that was legitimately installed on the site. The directory structure was professional:
forminator-donations-buttons/
my-monitoring-plugin.php # Main plugin file (entry point)
adminer.php # Full Adminer database tool
includes/
data-collector.php # Harvests site data
sender.php # Uploads to C2 endpoint
encryption.php # AES-256-CBC encryption layer
data-compressor.php # gzip compression before upload
health-checker.php # Monitors its own uptime
This isn’t a quick-and-dirty webshell. It’s a modular exfiltration framework with separation of concerns: one module collects, one compresses, one encrypts, one sends, and one monitors the pipeline’s health. Each file had Russian-language comments throughout the source code.
The Collection Engine
The data-collector.php module gathered data in categories, each controlled by a flag stored in wp_options:
// Configuration stored in wp_options (14 entries)
my_monitoring_enabled = 1
my_monitoring_send_interval = hourly
my_monitoring_collect_stats = 1 // WordPress version, PHP version, server info
my_monitoring_collect_plugins = 1 // Full plugin list with versions
my_monitoring_collect_config = 1 // wp-config.php contents (DB creds, salts)
my_monitoring_collect_health = 1 // Server resources, disk space
my_monitoring_collect_passwords = 1 // <-- This is the dangerous one
my_monitoring_collect_env = 1 // Environment variables (API keys, tokens)
my_monitoring_max_retries = 3
my_monitoring_retry_delay = 60
Every option name starts with my_monitoring_ — innocuous enough that a database audit scanning for eval or base64 patterns would skip right over them. No obfuscation, no encoding. Just boring configuration rows that look like they belong to a monitoring plugin.
The collect_passwords flag is the critical one. When enabled, the collector enumerates WordPress user records and extracts password hashes, application passwords, session tokens, and authentication cookies. Combined with collect_config (which dumps wp-config.php including database credentials and authentication salts), the attacker gets everything needed to crack password hashes offline or forge authenticated sessions.
The Dead Drop: JSONBin.io as C2
Traditional malware phones home to attacker-controlled servers. Those domains end up on blocklists, their IPs get flagged, and firewall rules catch the traffic. This plugin took a different approach.
The C2 endpoint was JSONBin.io — a legitimate, free JSON storage API used by developers for prototyping and data sharing. The exfiltration used a single API endpoint:
// C2 configuration in wp_options
my_monitoring_api_url = https://api.jsonbin.io/v3/b/{bin-id}
my_monitoring_api_type = jsonbin
// The API key, stored encrypted
_my_monitoring_api_key_encrypted = qPFW0PRQ8Sb6K0GDFQhz...
The sender.php module uploaded collected data via standard HTTPS PUT requests to JSONBin’s REST API. From a network perspective, the traffic looks identical to a legitimate WordPress plugin calling a third-party API — because that’s exactly what it is. The request goes to api.jsonbin.io over HTTPS port 443. No custom ports, no unusual protocols, no DNS tunneling.
This is the “dead drop” technique: the attacker doesn’t need to run their own C2 infrastructure. They create a free JSONBin account, generate an API key, and use the legitimate service as a mailbox. The exfiltrated data sits in a JSON bin that only the attacker’s API key can read. The compromised site pushes data; the attacker pulls it at their convenience.
Why This Evades Detection
- Domain reputation —
api.jsonbin.iois a legitimate service with a clean reputation. No blocklist includes it. No WAF rule flags it. - TLS encryption — the payload is encrypted in transit. Even if you capture the traffic, you see a standard HTTPS request to a known API.
- Application-layer encryption — the plugin encrypts data with AES-256-CBC before sending. Even if JSONBin cooperated with an investigation, the stored data is encrypted with a key only the attacker has.
- Low volume — one request per hour. No burst traffic, no bandwidth anomalies. A single 50–200 KB PUT request blends into normal plugin API traffic.
- No outbound signatures — the HTTP headers are standard JSONBin API headers (
X-Master-Key,Content-Type: application/json). Nothing to trigger IDS/IPS rules.
The Encryption Layer
The encryption.php module implemented AES-256-CBC with a twist: the encryption key wasn’t hardcoded in the file. It was derived from the API key stored (itself encrypted) in wp_options:
// Simplified encryption flow
function encrypt_payload($data, $api_key) {
$key = hash('sha256', $api_key, true);
$iv = openssl_random_pseudo_bytes(16);
$encrypted = openssl_encrypt(
gzencode($data), // Compress first, then encrypt
'aes-256-cbc',
$key,
OPENSSL_RAW_DATA,
$iv
);
return base64_encode($iv . $encrypted);
}
The _my_monitoring_api_key_encrypted option in the database stored the JSONBin API key in an encrypted form. To decrypt it, you needed a key derived from the WordPress installation’s AUTH_SALT — a value unique to each site. This means the encrypted API key in the database is useless without access to wp-config.php, and the encrypted payloads on JSONBin are useless without the API key. A three-layer key chain where each link depends on the previous one.
The Health Monitor
The health-checker.php module added self-monitoring that most malware lacks. It tracked:
- Whether the last upload succeeded or failed
- How many consecutive failures had occurred
- Whether the JSONBin API was reachable
- Disk space on the server (to avoid filling logs that might trigger alerts)
Failed uploads were logged to wp-content/my-monitoring-logs/ — another directory that looks like it belongs to a legitimate monitoring plugin. The health checker also implemented exponential backoff: if uploads failed three times, it would wait retry_delay seconds before trying again, increasing the delay with each failure to avoid hammering a temporarily unavailable endpoint.
This level of engineering — retry logic, health monitoring, configurable intervals, compressed and encrypted payloads — indicates a professional toolkit, not a script kiddie’s one-off.
The Wider Campaign
The exfiltration plugin was just one component of a multi-vector compromise. The same site had:
- 62 gambling SEO spam posts — Indonesian and Italian casino doorway content, published by a compromised editor account
- Eight Akismet-clone fake plugins — directories with names like
analytics_1782935632,security_1782935313,widget_1782935298, each containing Akismet’s source with modified headers - Eight Hello Elementor-clone fake themes — same pattern, cloned from the default Elementor starter theme
- Two self-hiding plugins — one that deregistered itself from WordPress’s plugin list so it wouldn’t appear in the admin panel, and a dropper that could deploy additional payloads
- Nine fake Google Search Console verification files — allowing the attacker to verify the domain in GSC and manipulate its search appearance
- An exposed Adminer instance — bundled inside the same fake plugin directory, giving the attacker a full web-based database management interface
The fake plugins and themes all used Unix timestamps in their directory names (e.g., 1782935632), likely generated at deployment time. The Akismet clones were particularly clever: because they contained mostly legitimate Akismet code, file-level malware scanners that check against known-bad signatures would pass them as clean. Only a checksum comparison against the official Akismet release would flag the modifications.
Two Rogue Administrators
The database contained two attacker-created administrator accounts, both with auto-generated usernames following distinct patterns:
+----+-----------------+--------------------------------------+---------------------+
| ID | user_login | user_email | user_registered |
+----+-----------------+--------------------------------------+---------------------+
| 4 | sys_10eec2d1 | noreply@[redacted] | 2026-06-29 xx:xx:xx |
| 5 | adm_4091f7 | adm_4091f7@[redacted] | 2026-07-05 xx:xx:xx |
+----+-----------------+--------------------------------------+---------------------+
The sys_ and adm_ prefixes followed by hex fragments are a fingerprint of automated account creation. Two accounts rather than one suggests either redundancy (a backup if one gets deleted) or two separate phases of the attack, six days apart. The email addresses used the site’s own domain, making them harder to spot in a quick user audit.
The Reinfection Pattern
Investigation of the zero-byte stubs told a story of ongoing conflict between the site’s security software and the attacker. Over 20 files across wp-includes/ and wp-content/ had been cleaned to zero bytes by the scanner — but the attacker kept deploying new ones:
wp-includes/cookie.php 0 bytes (cleaned)
wp-includes/core.php 0 bytes (cleaned)
wp-includes/wp-cache.php 0 bytes (cleaned)
wp-includes/check_db.php 0 bytes (modified TODAY)
wp-includes/check_db2.php 0 bytes (cleaned)
wp-includes/hash_files.php 0 bytes (cleaned)
wp-includes/hash_files2.php 0 bytes (cleaned)
The check_db.php file had been modified on the day of investigation — proof that the attacker’s automation was still actively attempting reinfection even as the scanner was cleaning files. This is the cleanup arms race: the scanner removes payloads, the attacker redeploys through one of their persistence mechanisms (the rogue admin accounts, the self-hiding plugins, or the exfiltrated credentials themselves).
Detection and Hunting
If you suspect a similar exfiltration plugin on your WordPress site:
Check wp_options for monitoring-pattern entries
SELECT option_name, LEFT(option_value, 80) as preview
FROM wp_options
WHERE option_name LIKE '%monitoring%'
OR option_name LIKE '%_api_key%'
OR option_name LIKE '%_api_url%'
OR option_name LIKE '%_send_interval%'
ORDER BY option_name;
Search for outbound API calls in plugin code
grep -rl "wp_remote_post\|wp_remote_put\|curl_exec\|file_get_contents.*http" \
wp-content/plugins/ --include="*.php" | \
xargs grep -l "jsonbin\|pastebin\|requestbin\|webhook\.site\|pipedream" 2>/dev/null
Look for encryption modules alongside collection logic
# Legitimate plugins rarely bundle their own encryption
grep -rl "openssl_encrypt\|aes-256\|mcrypt" \
wp-content/plugins/ --include="*.php" | \
xargs grep -l "collect\|harvest\|gather\|exfil" 2>/dev/null
Check for plugins that don’t match wordpress.org slugs
# List all plugin directories and check which ones exist in the official repo
for dir in wp-content/plugins/*/; do
slug=$(basename "$dir")
status=$(curl -s -o /dev/null -w "%{http_code}" \
"https://api.wordpress.org/plugins/info/1.0/$slug.json")
if [ "$status" = "404" ]; then
echo "NOT IN REPO: $slug"
fi
done
The Bigger Picture
Using legitimate third-party services as C2 infrastructure is not new in the broader malware landscape — APT groups have used Dropbox, Google Drive, Telegram bots, and GitHub repositories as dead drops for years. But in the WordPress ecosystem, where most malware still relies on hardcoded C2 domains or reverse shells, this represents an escalation in sophistication.
The implications are uncomfortable. JSONBin.io is just one of dozens of free JSON/data storage APIs. Alternatives like npoint.io, mockapi.io, or any serverless function endpoint could serve the same purpose. You cannot blocklist them all — legitimate plugins use them. You cannot inspect the traffic — it’s encrypted end-to-end. The only reliable detection is at the code level: finding the collection and upload logic before it runs.
Traditional WordPress malware — webshells, backdoors, SEO spam injections — is noisy. It modifies visible output, creates detectable files, generates suspicious traffic patterns. Data exfiltration is none of those things. The site looks and functions perfectly normally while its credentials, configurations, and user data are silently siphoned to a service designed to store JSON objects.
The gambling spam was the distraction. The real payload was the data. And without a code-level investigation, no scanner would have found it.