WP-Cron Malware: Scheduled Tasks That Rebuild the Hack
By Glenn Lyvers · Updated · 5 min read
You deleted the infected files. You checked them again an hour later and they were back — same filenames, same content, no new logins in the access log. That pattern points at persistence, and one of the most reliable places attackers hide it is WordPress’s own task scheduler.
What WP-Cron is, and why attackers like it
WP-Cron is WordPress’s internal scheduler. It runs your publish-a-scheduled-post, check-for-updates, and clean-up-transients jobs, and any plugin that needs recurring work registers there too. Unlike a real server cron job, it isn’t a file on disk — it’s a single serialized array stored in one row of your wp_options table, under the option name cron, and it fires opportunistically when someone visits your site.
That combination is why it makes such good cover. It’s inside the database, so a file scanner walking your directories never sees it. It’s a legitimate core feature, so its presence raises no alarms. It fires on ordinary page views, so there’s no suspicious process to notice. And a single scheduled event can call a function that rewrites files, recreates an admin user, or re-downloads a payload — meaning your cleanup gets undone by traffic to your own homepage.
What a malicious event looks like
Legitimate cron hooks have recognizable names: wp_version_check, wp_scheduled_delete, woocommerce_cleanup_sessions, and similar — a plugin name or a core prefix followed by a plain-English verb. Malicious ones usually fall into one of a few shapes: a random alphanumeric hook name with no matching code anywhere in your plugins or theme; a name that mimics core closely enough to skim past you (wp_update_check instead of wp_version_check); or a real-looking hook whose registered callback lives in a file you don’t recognize.
The tell that matters most is the orphan test: search your entire codebase for the hook name. Every legitimate scheduled event has a matching add_action somewhere in core, a plugin, or your theme. If the hook exists in the schedule but nothing in your files registers it, either you removed a plugin without cleaning up after it, or something else put it there. Check the frequency too — anything scheduled every minute or every five minutes deserves a hard look, since almost nothing legitimate needs to run that often on a normal site.
How to list what’s actually scheduled
The fastest route, if you have shell access, is WP-CLI: wp cron event list gives you every hook, its schedule, and its next run time in one table. It’s read-only, it’s fast, and it’s the same view I start with on nearly every reinfection job.
Without shell access, a scheduled-task viewer plugin such as WP Crontrol shows the same information in the dashboard, including which callback each hook is bound to — that callback column is the useful part, because it points straight at the file doing the work. Failing that, you can read the raw cron option directly in phpMyAdmin, though the serialized array is unpleasant to read by eye and easy to corrupt if you edit it there.
Whichever route you take, look at the whole list rather than searching for something you already suspect. The entries that matter are the ones you’d never have thought to look for.
Cleaning it without breaking real jobs
Unschedule the specific malicious event rather than wiping the entire cron array. wp cron event delete <hook> removes one hook cleanly; deleting the whole cron option nukes legitimate scheduled work along with it, and while WordPress core rebuilds its own entries, plugin-registered jobs frequently don’t come back until each plugin is reactivated. That turns a five-minute fix into a day of chasing broken backups and unsent emails.
Then, and this is the part people skip: delete the code the event was calling. Removing the schedule entry without removing its callback just means the next page load re-registers it, because the malicious file is still loaded on every request and its first act is to reschedule itself. That loop is the single most common reason a “cleaned” cron infection comes back within the hour.
The cron entry is a symptom, not the cause
A malicious scheduled task is persistence, not entry. Something got in first — a vulnerable plugin, a stolen password, a backdoor from an earlier compromise nobody finished cleaning — and the cron entry is just the insurance policy it left behind. Expect company: cron persistence is very often paired with an injected functions.php block, a hidden admin account, or malicious rows elsewhere in the database. My guides on hacked functions.php files, hidden admin users, and database malware cover the usual companions, and the reinfection guide covers how to prove you actually got all of it.
After it’s clean
Once the site is genuinely clean, there’s a small hardening step worth taking: disable WP-Cron’s page-load trigger with DISABLE_WP_CRON in wp-config.php and run wp-cron.php from a real server cron job on a fixed schedule. That won’t stop an attacker who has code execution from scheduling something — but it makes the scheduler predictable, keeps it out of your visitors’ page loads, and means an unexpected execution shows up in your server’s cron log rather than blending into normal traffic.
If you’ve been round this loop twice already and the site keeps rebuilding itself, that’s the point to stop guessing. Finding every piece of a persistence chain is exactly what my malware removal service does — files, database, scheduled tasks, and the way in, in one pass.
Common questions
Where are WordPress cron jobs actually stored?
In a single row of the wp_options table, under the option name ‘cron’, as one serialized PHP array. There is no file on disk for them, which is exactly why file-based malware scanners walk right past malicious entries and why a cron infection can survive a full reinstall of core, plugins, and theme.
Can I just delete every scheduled event to be safe?
I don’t recommend it. WordPress core rebuilds its own events, but plugin-registered jobs often don’t return until each plugin is reactivated — so you can silently lose backups, order processing, or scheduled emails. Delete the specific malicious hooks by name instead, and leave the rest alone.
Does setting DISABLE_WP_CRON stop the malware?
No. It changes when the scheduler runs, not what’s scheduled — an attacker with code execution can still register events, and a real server cron will happily run a malicious one. It’s a useful hardening and visibility step after the site is clean, not a cleanup step.
Why did the malware come back after I deleted the cron entry?
Almost always because the file the event was calling is still on disk and still loaded on every page request, and its first action is to reschedule itself. Deleting the schedule without deleting the callback just resets a timer. Remove both, then reload the site and re-list the events to confirm nothing reappeared.
Will Wordfence or Sucuri catch a malicious cron event?
Not reliably. Both are strong at file-level scanning and weaker on database contents, and a cron entry is a serialized value in a single options row — not a file with a signature. That gap is a large part of why sites come back ‘clean’ from a scan and keep reinfecting anyway.