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

● CVE-2024-5321 · CVSS 6.1 · Средний
Исследования · FlawOpen

CVE-2024-5321: права на журналы контейнеров на Windows-узлах Kubernetes

CVE-2024-5321 (CVSS 6.1, средняя): на Windows-узлах kubelet создавал каталоги журналов подов через os.MkdirAll, который в Windows игнорирует режим доступа, поэтому журналы наследовали ACL, дающий BUILTIN\Users чтение, а Authenticated Users — изменение.

💡 Простыми словами (ELI5)

Больница хранит карту каждого пациента в шкафу для документов. Заказывая новый шкаф, сотрудники пишут в бланке «только для врачей», но в этом здании поле «замок» молча игнорируется, и шкаф получает правило комнаты по умолчанию: читать может любой сотрудник, писать — любой с пропуском. Исправление — служащий, который каждому новому шкафу ставит собственный замок и отменяет для него правило комнаты.

Ключевые понятия и термины

DACL / ACE
Дискреционный список управления доступом Windows: записи, которые определяют, какие учётные записи могут читать, изменять или выполнять файл или каталог.
Наследование ACL
По умолчанию новый файл или каталог в Windows копирует наследуемые записи DACL родительского каталога. Защищённый DACL это отключает.
BUILTIN\Users / Authenticated Users
Широкие группы Windows: все локальные учётные записи и все учётные записи, хоть раз вошедшие в систему. Kubernetes сопоставляет контейнеры, работающие как ContainerUser, с BUILTIN\Users при доступе к файлам хоста.
os.MkdirAll в Windows
В Go os.MkdirAll(path, perm) учитывает perm только в Unix. В Windows режим игнорируется, и каталог получает унаследованный ACL.

Анализ первопричины

setupDataDirs() в pkg/kubelet/kubelet.go создавала корень журналов подов (C:\var\log\pods) вызовом os.MkdirAll(dir, 0750). Go игнорирует режим в Windows, поэтому каталог и записанные в него журналы наследовали ACL родителя, дававший BUILTIN\Users чтение, а NT AUTHORITY\Authenticated Users — изменение. PR #126104 добавил в pkg/util/filesystem функции MkdirAll() и Chmod(), которые переводят Unix-режим в явный DACL и применяют его с PROTECTED_DACL_SECURITY_INFORMATION, и перевёл на них каталоги журналов, плагинов и контрольных точек kubelet.

Пошаговый сценарий атаки

Шаг 1

Kubelet создаёт корень журналов

На Windows-узле setupDataDirs() вызывает os.MkdirAll(kl.getPodLogsDir(), 0750). Значение 0750 игнорируется, и каталог наследует ACL родителя.

Шаг 2

Широкие группы получают доступ

Унаследованные записи дают BUILTIN\Users чтение, а NT AUTHORITY\Authenticated Users — изменение каталогов журналов подов и файлов, которые туда пишет среда выполнения.

Шаг 3

Чтение чужих журналов

Любая малопривилегированная локальная учётная запись на узле или нагрузка, смонтировавшая путь журналов через hostPath, читает 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)
}

Чек-лист по защите системы для инженеров

Источники