wp-cron et malware : des tâches qui se réenregistrent
Supprimer un événement wp-cron ne sert à rien si une option, un mu-plugin ou un thème le réenregistre au prochain chargement. Voici où la tâche vit vraiment, comment la figer, et ce qu'il ne faut pas vider à l'aveugle.
Une option en base ré-ajoute la tâche après suppression. Vider le cron transitoire sans retirer l'option ne suffit pas. Regardez wp_options et les mu-plugins.
Ce que wp-cron est, et ce qu'il n'est pas
wp-cron n'est pas le cron Unix. C'est une file d'événements stockée en base (`cron` dans `wp_options`) que WordPress parcourt quand quelqu'un charge une page — ou quand un vrai cron HTTP appelle `wp-cron.php`. Un malware s'y enregistre comme n'importe quel plugin de sauvegarde : un hook, un intervalle, une fonction. La différence, c'est que la fonction envoie du spam, réécrit un fichier, ou rappelle un serveur.
Désactiver wp-cron dans `wp-config` (`DISABLE_WP_CRON`) arrête le déclenchement par visiteur. Ça n'efface pas la file, et ça n'empêche pas un cron système ou un voisin d'appeler encore `wp-cron.php`. Beaucoup de gens « coupent le cron » et croient avoir gagné ; la tâche est toujours là, prête au prochain hit.
L'inventaire se fait à froid : `wp cron event list` (WP-CLI), ou lecture de l'option `cron`. Notez hook, recurrence, next run, et le plugin qui a enregistré l'action si vous le voyez. Copiez avant d'effacer. Une file légitime (WooCommerce, Updraft, newsletter) mélangée au malware se reconstruit mal à la main.
La tâche qui revient après suppression
Vous videz l'événement dans un plugin « cron manager », vous rechargez, il est revenu. Trois mécanismes classiques. Un `add_action('init')` ou `admin_init` qui rappelle `wp_schedule_event` si l'événement n'existe pas — code vivant dans le thème enfant, un plugin abandonné, ou un mu-plugin. Une option maison (`wp_options`) lue à chaque boot qui réarme. Un drop-in (`object-cache.php`, `advanced-cache.php`) qui s'exécute trop tôt pour que votre manager le voie.
Traiter le symptôme (la ligne dans la file) sans l'enregistreur, c'est recommencer toutes les cinq minutes. Cherchez le callback : `wp cron event list` puis grep du nom de hook dans `wp-content`. Si le hook n'apparaît dans aucun fichier du thème ou des plugins, il est en base (option sérialisée qui `eval`, ou objet stocké) ou dans un fichier hors `wp-content`.
C'est le même schéma qu'une injection dans wp_options : le fichier PHP « officiel » est propre, la persistance est ailleurs.
L'option en base qui ré-ajoute l'événement
Cherchez dans `wp_options` les autoload `yes` récentes, les options dont le nom ne veut rien dire (`wp_cache_key`, `cdn_setting`, un hash), et celles dont la valeur contient `http`, `eval`, `base64`, un domaine inconnu, ou un `wp_schedule`. Ne les videz pas en masse : `theme_mods`, `active_plugins`, `rewrite_rules` cassent le site.
Une option de cache ou de « licence » est un camouflage fréquent. Grep SQL sur `cron`, `eval`, `<script`, `fromCharCode`. Exportez la ligne avant UPDATE. Restaurer « la base d'il y a un mois » sans lire ramène souvent le même réarmeur — voir base WordPress injectée.
Les transitoires (`_transient_`, `_site_transient_`) peuvent porter une copie de la file ou un payload. Les vider (`wp transient delete --all`) calme parfois l'affichage, jamais un `add_action` dans un mu-plugin. Faites les deux contrôles, dans cet ordre : code qui s'enregistre, puis options, puis transitoires.
- Export de l'option `cron` avant toute suppression.
- Grep des hooks suspects dans wp-content et la base.
- Autoload récents : lire, ne pas TRUNCATE.
Mu-plugins, must-use et drop-ins
Les must-use (`wp-content/mu-plugins`) s'exécutent sans être dans la liste des plugins. C'est l'endroit préféré d'une persistance « le site a l'air propre ». Un fichier d'une vingtaine de lignes qui schedule un HTTP sortant ou réécrit `index.php` suffit. L'admin WordPress ne les désactive pas d'un clic.
Les drop-ins (`object-cache.php`, `advanced-cache.php`, `maintenance.php`, `db.php`) sont encore plus tôt. Un cache Redis légitime et un drop-in pirate peuvent cohabiter sous le même nom après un « nettoyage » trop pressé. Comparez à l'original du plugin de cache, octet par octet.
Si vous n'avez pas FTP, le gestionnaire de fichiers de l'hébergeur ou une archive du compte montre ces dossiers. Ne réinstallez pas WordPress par-dessus : les mu-plugins survivent. C'est le motif n°1 des « on a tout réinstallé, le cron est revenu ».
Cron système à côté de wp-cron
Sur un VPS ou un mutualisé un peu ouvert, crontab de l'utilisateur, cron cPanel, ou une tâche planifiée Plesk appelle un PHP à la racine toutes les cinq minutes. Ça n'apparaît pas dans `wp cron event list`. Demandez à l'hébergeur la liste des tâches du compte, ou lisez crontab si vous avez SSH.
Un `wget https://votre-site/wp-cron.php` légitime (recommandé quand `DISABLE_WP_CRON` est à true) n'est pas le problème. Un `php /home/xxx/public_html/radio.php` l'est. Notez la commande, ne la « mettez pas à jour ».
Après un incident mail, le cron système est souvent le canal qui relance `mail()` ou un SMTP volé. Croisez avec le journal d'envoi : heure du pic, heure de la tâche.
Vider le transitoire sans se mentir
Beaucoup de tutos disent « videz le cron transitoire ». Il y a un transient lié à l'exécution, et l'option `cron` elle-même. Vider le premier sans retirer l'enregistreur donne une accalmie d'une page. Vider `cron` entier casse WooCommerce (actions planifiées de boutique), les publications programmées, les licences. Exportez, retirez les hooks que vous avez identifiés, pas la file entière.
Object cache (Redis, Memcached) peut resservir une vieille file. Videz-le après le correctif, comme après tout nettoyage. Sinon vous déboguez un fantôme. Voir aussi cache LiteSpeed qui ressert le malware.
Lire la file sans l'exécuter
Évitez de recharger massivement le site « pour voir si le cron part » : vous exécutez le malware. Travaillez en WP-CLI, ou sur une copie locale isolée (sans les URLs de callback). Si vous devez tester en production, coupez les sorties : firewall sortant, `mail()` désactivé le temps du dossier, SMTP révoqué.
Un hook qui fait un `wp_remote_get` vers un domaine inconnu est une exfiltration ou un C2. Notez l'URL, elle date et elle sert au constat. Ne la « pingez » pas depuis votre bureau pour voir : vous signalez que vous êtes dessus, et vous mélangez vos logs.
Refermer pour que rien ne se réarme
Entrée fermée (plugin vulnérable, admin fantôme, voisin), puis enregistreur retiré, puis file nettoyée, puis mots de passe panel / FTP / CMS, puis surveillance. Inverser remet une tâche neuve dans une file que vous venez de laver.
Régénérez les clés AUTH dans `wp-config` pour tuer les sessions. Vérifiez qu'aucun second site du compte n'écrit dans votre `wp-content`. Le cron d'un voisin est un classique sur mutualisé — WordPress propre, voisin sale.
- Plus de hook inconnu dans `wp cron event list`.
- mu-plugins et drop-ins alignés sur l'officiel.
- crontab du compte lue et nommée.
- Object cache et cache page vidés.
Surveillance les sept jours suivants
Une persistance mal retirée se montre en 24 à 72 h. Relistez les événements chaque matin, pas « quand ça ira mieux ». Un hook qui réapparaît avec le même nom et une `next run` trop tôt est le même enregistreur, pas un nouveau pirate.
Si vous déléguez, un prestataire sérieux fournit la liste des hooks retirés et le fichier qui les posait. « Cron nettoyé » sans ces deux lignes n'est pas un constat. Vous pouvez ouvrir un espace sans livrer d'accès tout de suite.