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

● CVE-2024-5321 · CVSS 6.1 · Média
Pesquisa · FlawOpen

CVE-2024-5321: permissões dos logs de contêineres em nós Windows do Kubernetes

CVE-2024-5321 (CVSS 6.1, média): em nós Windows, o kubelet criava os diretórios de log dos pods com os.MkdirAll, que ignora o modo de permissão no Windows; os logs herdavam uma ACL que permitia a BUILTIN\Users lê-los e a Authenticated Users modificá-los.

💡 Explicação em Linguagem Simples (ELI5)

Um hospital guarda o prontuário de cada paciente em um arquivo de aço. Ao pedir um arquivo novo, a equipe escreve "somente médicos" no formulário, mas neste prédio o campo da fechadura é ignorado sem aviso, e o arquivo fica com a regra padrão da sala: qualquer funcionário pode ler e qualquer pessoa com crachá pode escrever. A correção é um funcionário que, a cada arquivo novo, instala uma fechadura própria e descarta a regra padrão da sala para aquele móvel.

Conceitos Centrais e Termos

DACL / ACE
Lista de controle de acesso discricionário do Windows: as entradas que dizem quais contas podem ler, gravar ou executar um arquivo ou diretório.
Herança de ACL
Por padrão, um arquivo ou diretório novo no Windows copia as entradas herdáveis da DACL do diretório pai. Uma DACL protegida impede isso.
BUILTIN\Users / Authenticated Users
Grupos amplos do Windows: todas as contas locais e todas as contas que já fizeram logon. O Kubernetes mapeia contêineres que rodam como ContainerUser para BUILTIN\Users quando acessam arquivos do host.
os.MkdirAll no Windows
O os.MkdirAll(path, perm) do Go só usa perm no Unix. No Windows o modo é ignorado e o diretório recebe a ACL herdada.

Análise de Causa Raiz

setupDataDirs() em pkg/kubelet/kubelet.go criava a raiz de logs dos pods (C:\var\log\pods) com os.MkdirAll(dir, 0750). O Go ignora o modo no Windows, então o diretório e os arquivos de log gravados nele herdavam a ACL do pai, que dava leitura a BUILTIN\Users e modificação a NT AUTHORITY\Authenticated Users. O PR #126104 adicionou MkdirAll() e Chmod() em pkg/util/filesystem, que convertem o modo Unix em uma DACL explícita aplicada com PROTECTED_DACL_SECURITY_INFORMATION, e passou os diretórios de log, plugins e checkpoints do kubelet a usá-las.

Fluxo de Ataque Passo a Paso

Passo 1

O kubelet cria a raiz de logs

Em um nó Windows, setupDataDirs() chama os.MkdirAll(kl.getPodLogsDir(), 0750). O 0750 é ignorado e o diretório herda a ACL do pai.

Passo 2

Grupos amplos herdam acesso

As entradas herdadas dão leitura a BUILTIN\Users e modificação a NT AUTHORITY\Authenticated Users nos diretórios de log dos pods e nos arquivos que o runtime grava neles.

Passo 3

Leitura de logs de outras cargas

Qualquer conta local de baixo privilégio no nó, ou uma carga com o caminho de logs montado via hostPath, lê o stdout e o stderr de outros pods, que muitas vezes trazem tokens, strings de conexão ou dados de clientes.

Passo 4

Adulteração das evidências

As mesmas contas podem editar ou truncar arquivos de log antes que o coletor os envie, escondendo atividade do logging centralizado.

Código-Fonte: Vulnerável vs. Seguro

IMPLEMENTAÇÃO VULNERÁVEL
// 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
}
PATCH SEGURO E ROBUSTO
// 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)
}

Lista de Verificação de Segurança para Engenharia

Fontes