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은 파드 로그 디렉터리를 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)은 Unix에서만 perm을 사용합니다. Windows에서는 모드가 무시되고 디렉터리는 상속된 ACL을 받습니다.

근본 원인 분석 (Root Cause)

pkg/kubelet/kubelet.go의 setupDataDirs()는 파드 로그 루트(C:\var\log\pods)를 os.MkdirAll(dir, 0750)으로 만들었습니다. Go는 Windows에서 모드를 무시하므로 이 디렉터리와 그 안에 쓰이는 로그 파일은 부모의 ACL을 상속했고, BUILTIN\Users는 읽기, NT AUTHORITY\Authenticated Users는 수정 권한을 가졌습니다. PR #126104는 Unix 모드를 명시적 DACL로 바꿔 PROTECTED_DACL_SECURITY_INFORMATION으로 적용하는 MkdirAll()과 Chmod()를 pkg/util/filesystem에 추가하고, 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)
}

엔지니어링 및 시스템 보안 강화 체크리스트

출처