flawopen.com/Teardowns/cve-2024-5321-kubernetes-windows-container-log-permissions
CVE-2024-5321:Kubernetes Windows 节点容器日志权限问题
CVE-2024-5321(CVSS 6.1,中危):在 Windows 节点上,kubelet 用 os.MkdirAll 创建 Pod 日志目录,而该函数在 Windows 上会忽略权限模式,于是日志继承了一个允许 BUILTIN\Users 读取、Authenticated Users 修改的 ACL。
医院把每位病人的病历放在档案柜里。员工订购新柜子时会在订单上写“仅限医生”,但这栋楼的系统会悄悄忽略“锁”这一栏,新柜子直接沿用房间的默认规则:任何员工都能看,任何有工牌的人都能往里写。修复办法是安排一名文员:每来一个新柜子,就给它装上专用的锁,并对这个柜子取消房间的默认规则。
核心概念与专有名词
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() 用 os.MkdirAll(dir, 0750) 创建 Pod 日志根目录(C:\var\log\pods)。Go 在 Windows 上会忽略模式,因此该目录以及写入其中的日志文件继承了父目录的 ACL,BUILTIN\Users 可读,NT AUTHORITY\Authenticated Users 可改。PR #126104 在 pkg/util/filesystem 中新增 MkdirAll() 和 Chmod(),把 Unix 模式转换成显式 DACL 并以 PROTECTED_DACL_SECURITY_INFORMATION 写入,同时让 kubelet 的日志、插件和检查点目录改用它们。
攻击执行流程分解
kubelet 创建日志根目录
在 Windows 节点上,setupDataDirs() 调用 os.MkdirAll(。kl.getPodLogsDir(), 0750)0750 被忽略,目录继承父目录的 ACL。
大范围的组继承了权限
继承来的条目让 BUILTIN\Users 可以读取、NT AUTHORITY\Authenticated Users 可以修改 Pod 日志目录以及运行时写入其中的文件。
读取其他工作负载的日志
节点上任何低权限的本地账户,或通过 hostPath 挂载了日志路径的工作负载,都能读取其他 Pod 的 stdout 和 stderr,其中常含令牌、连接字符串或客户数据。
篡改证据
同样的账户可以在日志采集器收走日志之前编辑或截断日志文件,从而在集中式日志中隐藏活动。
源代码对比:漏洞与安全实现
// 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)
}
工程与系统安全加固清单
- ✓将所有 Windows 节点上的 kubelet 升级到 1.30.3、1.29.7、1.28.12、1.27.16 或更高版本。
- ✓用
kubectl get nodes -l kubernetes.io/列出 Windows 节点,并在每个节点上运行os=windowsicacls C:\var\log\pods:不应有任何 BUILTIN\Users 或 Authenticated Users 条目授予读取或修改权限。 - ✓在运行于 Windows 的 Go 代码中,不要依赖
os.Mkdir、os.MkdirAll、os.WriteFile或os.Chmod的perm参数;用windows设置受保护的 DACL,并在 Windows CI 任务中断言。.SetNamedSecurityInfo() - ✓除非是可信的日志代理,否则不要通过 hostPath 把
C:\var\log或其他节点日志路径挂载进工作负载。 - ✓不要把机密写入容器的 stdout 和 stderr,并在应用日志中脱敏令牌,因为节点日志目录的可读范围比 Pod 本身更大。