容器渗透

容器渗透最直接的容器渗透入口,往往不是容器渗透内核漏洞,而是容器渗透配置疏漏。去年有个团队用 Kubernetes 跑了一套内部 CI/CD,容器渗透图省事把 Docker socket 挂进了构建容器。容器渗透攻击者只需要在流水线里提交一个带恶意脚本的容器渗透 PR,就能在宿主机上以 root 执行任意命令。容器渗透整个过程没有触发任何已知 CVE,容器渗透纯粹是容器渗透权限给多了。

这类问题在真实环境里比比皆是容器渗透。容器安全常被简化成“镜像扫描”四个字,容器渗透但镜像里没漏洞,容器渗透不代表跑起来就安全。容器渗透一个容器启动后,容器渗透它和宿主机之间只隔了 namespace 和 cgroup 两层抽象。容器渗透这两层一旦被绕过,或者被错误配置削弱,隔离就形同虚设。

从容器里看到宿主机

先看一个经典场景。容器默认以 root 运行,而容器内的 root 和宿主机的 root 共享同一个 UID 0。如果容器没有启用 user namespace 重映射,并且挂载了宿主机的 /proc 或 /sys,攻击者就能直接读写宿主机的内核参数。比如通过 /proc/sys/kernel/core_pattern 写入一个管道命令,再触发一个 core dump,命令就会在宿主机上执行。这不是内核漏洞,是 Linux 本身的机制被滥用了。

再比如 cgroup release_agent。在 cgroup v1 下,如果容器内可以挂载 cgroup 文件系统,攻击者就能设置 release_agent 指向一个脚本,然后触发 cgroup 释放,脚本以宿主机 root 身份运行。这个手法在 2019 年就被公开过,但直到今天,仍有不少容器默认挂载了 cgroup。

内核漏洞是另一条路

配置问题靠加固能堵住,内核提权漏洞则更难防。容器共享宿主机内核,内核里的任何提权漏洞,对容器来说都是直达宿主机的跳板。Dirty Pipe(CVE-2022-0847)就是个典型例子。它允许覆盖只读文件的内容,攻击者可以改写宿主机的 /etc/passwd,直接拿到 root。容器里利用这个漏洞,和直接在宿主机上利用没有本质区别。

这类漏洞的修复依赖内核升级,但生产环境里内核升级的节奏往往很慢。一个跑了三年没重启的节点,内核版本可能落后几十个安全补丁。容器编排系统再新,底层内核是旧的,渗透风险就一直在。

逃逸之后做什么

逃逸到宿主机只是第一步。攻击者通常会先看这个节点上跑了哪些 Pod,有没有挂载 Service Account Token,能不能访问 API Server。如果节点上的 kubelet 配置了 --anonymous-auth=true,那连 token 都不用,直接调 API 就能列出所有 Pod 和 Secret。很多集群的 kubelet 端口 10250 在内网是敞开的,这给了横向移动极大的便利。

我见过一个真实案例:攻击者从一个存在 SSRF 漏洞的 Web 应用打进容器,利用挂载的 docker.sock 逃逸到宿主机,再从宿主机上的 kubeconfig 拿到集群管理员权限。整个过程不到二十分钟。事后复盘,最致命的一步就是 docker.sock 挂载——它把容器和宿主机之间的墙直接拆了。

防守的着力点

容器渗透的防守不能只靠一个层面。镜像扫描要做,但别指望它挡住所有攻击。运行时行为监控更有价值,比如检测容器内突然出现的 mount 调用、对 /proc/sys 的写入、或者异常的子进程创建。这些信号比镜像里有没有 CVE 更直接。

权限控制是另一条底线。非 root 运行容器、禁用 privileged 模式、不挂载 docker.sock、启用 user namespace、限制 cgroup 挂载——这些措施单独看都不复杂,组合起来能挡掉大部分常见逃逸路径。难的是在开发效率和安全性之间找平衡,很多团队为了 CI/CD 方便,主动把这些限制去掉了。

内核层面,及时打补丁是硬道理。但现实是,很多集群的节点根本不允许随意重启。折中方案是用 gVisor 或 Kata Containers 这类沙箱运行时,把容器和宿主机内核隔开。代价是性能损耗和兼容性问题,不是所有场景都能接受。

容器渗透的本质是隔离边界的攻防。边界由内核、运行时、编排系统、配置共同定义,任何一处松动,整条防线就可能被撕开。没有银弹,只有一层层地堆防御,然后祈祷没有短板。