flawopen.com/Teardowns/cve-2024-5321-kubernetes-windows-container-log-permissions

● CVE-2024-5321 · CVSS 6.1 · 中
セキュリティ研究 · FlawOpen

CVE-2024-5321:Kubernetes Windows ノードのコンテナログ権限の不備

CVE-2024-5321(CVSS 6.1、中):Windows ノードの kubelet は Pod のログディレクトリを os.MkdirAll で作成していましたが、この関数は Windows では権限モードを無視するため、ログは BUILTIN\Users に読み取りを、Authenticated Users に変更を許す ACL を継承していました。

💡 わかりやすい解説 (ELI5)

病院では患者ごとのカルテを書類キャビネットに保管しています。新しいキャビネットを注文するとき、職員は注文書に「医師のみ」と書きますが、この建物では鍵の欄が黙って無視され、キャビネットは部屋のデフォルトルールをそのまま引き継ぎます。つまり職員なら誰でも読め、入館証があれば誰でも書き込めます。修正策は、新しいキャビネットが届くたびに専用の鍵を取り付け、そのキャビネットについては部屋のルールを外す係を置くことです。

主要な概念と専門用語

DACL / ACE
Windows の任意アクセス制御リスト。どのアカウントがファイルやディレクトリを読み書き・実行できるかを定めるアクセス制御エントリの一覧です。
ACL の継承
既定では、Windows で新しく作ったファイルやディレクトリは親ディレクトリの DACL のうち継承可能なエントリをコピーします。保護された DACL を設定すると継承されません。
BUILTIN\Users / Authenticated Users
範囲の広い Windows グループ。すべてのローカルアカウント、および一度でもログオンしたすべてのアカウントです。Kubernetes は ContainerUser で動くコンテナがホストのファイルに触れるとき、BUILTIN\Users として扱います。
Windows 上の os.MkdirAll
Go の os.MkdirAll(path, perm) が perm を使うのは Unix だけです。Windows ではモードが無視され、ディレクトリは継承された ACL を受け取ります。

根本原因の分析 (Root Cause)

pkg/kubelet/kubelet.go の setupDataDirs() は、Pod ログのルート(C:\var\log\pods)を os.MkdirAll(dir, 0750) で作成していました。Go は Windows でモードを無視するため、このディレクトリと中に書かれるログファイルは親の ACL を継承し、BUILTIN\Users に読み取り、NT AUTHORITY\Authenticated Users に変更の権限が与えられていました。PR #126104 は pkg/util/filesystem に、Unix のモードを明示的な DACL に変換して PROTECTED_DACL_SECURITY_INFORMATION 付きで設定する MkdirAll() と Chmod() を追加し、kubelet のログ・プラグイン・チェックポイント用ディレクトリをそれらに切り替えました。

ステップ・バイ・ステップの攻撃フロー

ステップ 1

kubelet がログのルートを作成

Windows ノードで setupDataDirs() が os.MkdirAll(kl.getPodLogsDir(), 0750) を呼びます。0750 は無視され、ディレクトリは親の ACL を継承します。

ステップ 2

広いグループが権限を継承

継承されたエントリにより、BUILTIN\Users は Pod のログディレクトリとランタイムがそこに書くファイルを読み取れ、NT AUTHORITY\Authenticated Users は変更できます。

ステップ 3

他のワークロードのログを読む

ノード上の低権限ローカルアカウントや、hostPath でログのパスをマウントしたワークロードが、他の Pod の stdout と stderr を読めます。そこにはトークン、接続文字列、顧客データが含まれがちです。

ステップ 4

証拠の改ざん

同じアカウントは、ログ転送エージェントが収集する前にログファイルを編集・切り詰め、集中ログから活動を隠せます。

ソースコード比較:脆弱 vs 堅牢化

脆弱な実装
// pkg/kubelet/kubelet.go (kubelet v1.30.2)
func (kl *Kubelet) setupDataDirs() error {
	if err := os.MkdirAll(kl.getRootDir(), 0750); err != nil {
		return fmt.Errorf("error creating root directory: %v", err)
	}
	// BUG on Windows: os.MkdirAll ignores the 0750 mode. The directory inherits
	// its parent's ACL, which lets BUILTIN\Users read and
	// NT AUTHORITY\Authenticated Users modify every pod's log files.
	if err := os.MkdirAll(kl.getPodLogsDir(), 0750); err != nil {
		return fmt.Errorf("error creating pod logs root directory %q: %w", kl.getPodLogsDir(), err)
	}
	if err := os.MkdirAll(kl.getPodsDir(), 0750); err != nil {
		return fmt.Errorf("error creating pods directory: %v", err)
	}
	return nil
}
堅牢化されたセキュアパッチ
// pkg/kubelet/kubelet.go (fixed in v1.30.3, PR #126104)
func (kl *Kubelet) setupDataDirs() error {
	if err := os.MkdirAll(kl.getRootDir(), 0750); err != nil {
		return fmt.Errorf("error creating root directory: %v", err)
	}
	// FIX: utilfs.MkdirAll is plain os.MkdirAll on Linux. On Windows it also
	// calls utilfs.Chmod, which replaces the inherited ACL with an explicit one.
	if err := utilfs.MkdirAll(kl.getPodLogsDir(), 0750); err != nil {
		return fmt.Errorf("error creating pod logs root directory %q: %w", kl.getPodLogsDir(), err)
	}
	if err := os.MkdirAll(kl.getPodsDir(), 0750); err != nil {
		return fmt.Errorf("error creating pods directory: %v", err)
	}
	return nil
}

// pkg/util/filesystem/util_windows.go
func MkdirAll(path string, perm os.FileMode) error {
	if err := os.MkdirAll(path, perm); err != nil {
		return fmt.Errorf("Error creating directory %s: %v", path, err)
	}
	return Chmod(path, perm)
}

func Chmod(path string, filemode os.FileMode) error {
	// Maps owner, group and other mode bits to ACEs for the file's owner SID,
	// its group SID and BUILTIN\Users (BU). For 0750 the result is
	// "D:(A;OICI;FA;;;<owner>)(A;OICI;FRFX;;;<group>)(A;OICI;;;;BU)".
	// (Abridged: the real function builds this string inline.)
	dacl, err := daclForMode(path, filemode)
	if err != nil {
		return err
	}
	newSD, err := windows.SecurityDescriptorFromString(dacl)
	if err != nil {
		return fmt.Errorf("Error creating new security descriptor from DACL string: %v", err)
	}
	newDACL, _, err := newSD.DACL()
	if err != nil {
		return fmt.Errorf("Error getting DACL from new security descriptor: %v", err)
	}
	// PROTECTED_DACL_SECURITY_INFORMATION blocks inheritance from the parent,
	// so the BUILTIN\Users and Authenticated Users entries no longer apply.
	return windows.SetNamedSecurityInfo(path, windows.SE_FILE_OBJECT,
		windows.DACL_SECURITY_INFORMATION|windows.PROTECTED_DACL_SECURITY_INFORMATION,
		nil, nil, newDACL, nil)
}

エンジニアリング&システム堅牢化チェックリスト

参考資料