
Kit de detección y explotación para CVE-2026-39987, un RCE sin autenticación previa en notebooks Marimo. Incluye un escáner en Python y un script NSE de Nmap para identificar instancias vulnerables mediante comprobaciones del endpoint WebSocket.
Una vulnerabilidad de ejecución remota de código pre-autenticada en Marimo, un notebook Python de código abierto para ciencia de datos e IA/ML. El endpoint WebSocket de terminal (/terminal/ws) omite por completo la validación de autenticación, mientras que el endpoint vecino de notebook (/ws) la aplica correctamente. Un atacante no autenticado puede conectarse a /terminal/ws y obtener un shell PTY interactivo completo en el sistema host sin credenciales.
Explotada en la naturaleza en menos de 10 horas desde su divulgación. Los atacantes robaron credenciales de AWS en menos de 3 minutos.
Afecta a Marimo <= 0.20.4. Corregido en Marimo 0.23.0.
| Campo | Detalle |
|---|---|
| ID CVE | CVE-2026-39987 |
| Proveedor | Proyecto Marimo |
| Producto | Marimo (Notebook Python) |
| Versiones Afectadas | <= 0.20.4 |
| CVSS v3.1 | 9.3 (Crítico) |
| CWE | CWE-306 — Autenticación Ausente para Función Crítica |
| Vector de Ataque | Red |
| Autenticación | No se requiere |
| Interacción del Usuario | Ninguna |
| Madurez del Exploit | Explotado activamente en la naturaleza |
| Tiempo hasta la Explotación | ~10 horas después de la divulgación |
| Corregido En | Marimo 0.23.0 |
Marimo es un notebook Python reactivo de código abierto diseñado como una alternativa moderna a Jupyter. Está construido para ciencia de datos, experimentación con IA/ML y análisis de datos interactivo. Sus características clave incluyen seguimiento automático de dependencias, ejecución reproducible y una experiencia de desarrollo más limpia en comparación con los notebooks tradicionales.
Marimo está ganando rápidamente tracción en la comunidad de Python e IA/ML, especialmente entre los profesionales que desean flujos de trabajo de notebook más estructurados de los que ofrece Jupyter.
Como todos los entornos de notebook, las instancias de Marimo suelen tener acceso a recursos sensibles: credenciales en la nube (AWS, GCP, Azure), cadenas de conexión a bases de datos, claves API para servicios de IA (OpenAI, Anthropic, etc.) y acceso a la red interna. A diferencia de las aplicaciones web tradicionales, los notebooks están diseñados para ejecutar código arbitrario. Ese es su propósito principal.
Esta combinación hace que cualquier omisión de autenticación en un entorno de notebook sea particularmente devastadora.``` Typical Marimo Deployment:
┌──────────────┐ ┌────────────────────────────────┐ │ │ HTTP │ Marimo Server │ │ Browser │────────>│ │ │ (User) │ │ ┌──────────────────────────┐ │ │ │<────────│ │ /ws (Notebook) │ │ └──────────────┘ WS │ │ ✅ validate_auth() │ │ │ └──────────────────────────┘ │ │ │ │ ┌──────────────────────────┐ │ │ │ /terminal/ws │ │ │ │ ❌ NO AUTH CHECK │ │ │ └──────────────────────────┘ │ │ │ │ ┌──────────────────────────┐ │ │ │ Python Environment │ │ │ │ .env files │ │ │ │ AWS credentials │ │ │ │ API keys │ │ │ └──────────────────────────┘ │ └────────────────────────────────┘
---
## Análisis Profundo de la Vulnerabilidad
### Los Dos Endpoints WebSocket
El servidor de Marimo implementa múltiples endpoints WebSocket para diferentes funciones. La diferencia crítica entre los dos endpoints principales es la presencia (o ausencia) de una verificación de autenticación:```
Authentication Flow Comparison:
/ws (Notebook WebSocket):
┌─────────┐ ┌───────────────┐ ┌──────────┐ ┌───────────┐
│ Connect │───>│ validate_auth │───>│ Accept │───>│ Notebook │
└─────────┘ └───────┬───────┘ └──────────┘ └───────────┘
│
❌ Reject if
not authenticated
/terminal/ws (Terminal WebSocket):
┌─────────┐ ┌───────────────┐ ┌──────────┐ ┌───────────┐
│ Connect │───>│ Check mode & │───>│ Accept │───>│ PTY Shell │
└─────────┘ │ platform only │ └──────────┘ └───────────┘
└───────────────┘
⚠️ No auth check!
Anyone gets a shell!
El endpoint del notebook (/ws) llama correctamente a validate_auth() para verificar la identidad del usuario antes de aceptar conexiones WebSocket. Este es el comportamiento de seguridad esperado.
El endpoint del terminal (/terminal/ws) solo verifica si el servidor está en modo de ejecución y si la plataforma soporta la funcionalidad del terminal. Nunca llama a validate_auth(). Después de pasar estas verificaciones básicas, acepta la conexión y crea una sesión PTY (pseudo-terminal) completa.```python
async def websocket_connect(self, message): await self.validate_auth() # ✅ Checks authentication await self.accept() # ... notebook communication
async def websocket_connect(self, message): if not self.is_running_mode(): # Only checks mode await self.close() return if not self.is_platform_supported(): # Only checks platform await self.close() return await self.accept() # ❌ No auth! Anyone gets a shell # ... PTY shell creation
Esto es **CWE-306: Falta de Autenticación para Funciones Críticas**. El endpoint más peligroso del servidor (el que proporciona un shell interactivo) no tiene autenticación alguna.
### El Ataque: De Advisory a Claves de AWS en 3 Minutos