目录
摘要:本文复盘一场围绕云安全的技术授课,按原始讲解顺序整理 Docker 的隔离机制、运行时架构与四类容器逃逸案例,并继续分析 Kubernetes 的控制平面、etcd、ServiceAccount、Token 和 RBAC。文章保留了实验中的配置错误、网络失败、版本不匹配和未完成复现,重点呈现从现象到判断、从操作到结果的推理过程,以及这些风险在授权靶场和面试准备中的边界。
一、复盘范围与问题主线
本次课程从早间渗透案例中出现的未授权访问切入,主题逐步收敛到云环境中“容器边界是否真实可靠”这一问题。讲解没有把容器当成天然安全边界,而是先解释 Docker 为什么能够隔离,再逐层追踪命令从客户端到守护进程、containerd 和 runC 的传递路径。只有先确认这些组件各自承担的职责,后续看到 2375、docker.sock、特权参数或旧版 runC 时,才能判断它们究竟破坏了哪一层边界。
原稿反复强调,实验必须停留在已授权的本地环境、靶场或教学服务器中。下面的命令、路径和截图只作为原稿实验过程的技术记录,不能被理解为面向未知目标的操作建议。文章保留了课堂中“先观察结果,再推断原因”的节奏,但将其改写为第三人称技术说明。
复盘的核心判断是:容器逃逸并不是一个单一漏洞名称,而是隔离机制、管理接口、运行参数、组件版本和权限配置中的任一边界失效后,形成的后续控制链。
二、Docker 的隔离模型:先确认边界,再讨论逃逸
课程先回到 Docker 的基础设计。容器通常运行在 Linux 环境中,底层共享宿主机的 CPU、内存和磁盘,因此必须通过隔离与资源控制让多个容器能够并行存在。讲解将这一设计拆成 namespace、network namespace 和 cgroup 三个观察点:前者限制进程、用户和挂载点可见范围;网络命名空间提供独立的网卡、端口和地址空间;cgroup 则限制容器能够消耗的 CPU、内存等共享资源。
这三个对象解决的问题不同。容器内部执行 ps 时只能看到自己的进程,不代表宿主机真的只有这些进程;容器看到的网卡和地址也不等于宿主机的网络;资源限制则是为了避免一个容器耗尽共享机器的内存和 CPU。由此可见,容器“像一台独立服务器”主要是观察视角造成的,底层仍然依赖宿主机的内核和设备。
图 1:Docker 架构、隔离机制与运行时组件
网络部分继续以 Docker 的默认 bridge 模式为例。宿主机上会出现类似 docker0 的网桥,容器通过自己的网络接口与该网桥通信,常见地址落在 172.17 一类的网段。若启动容器时使用 --network host,容器与宿主机共享网络命名空间,前面的网络隔离就不再成立;none 模式则表示容器不配置常规网络。课程把这几种模式并列展示,目的不是记忆参数,而是让排查者知道“当前容器究竟隔离了什么”。
图 2:Docker 架构、隔离机制与运行时组件
图 3:Docker 架构、隔离机制与运行时组件
图 4:Docker 架构、隔离机制与运行时组件
图 5:Docker 架构、隔离机制与运行时组件
图 6:Docker 架构、隔离机制与运行时组件
图 7:Docker 架构、隔离机制与运行时组件
官方架构图随后成为理解后续实验的参照。Docker 客户端负责接收命令,命令被发送到 Docker daemon;daemon 所在的 Docker host 管理镜像与容器。原稿同时指出,官方示意图为了入门阅读进行了简化,真实链路还包括 containerd 与 runC。这也是课程强调阅读官方文档的原因:官方资料通常比二手解释更完整、具体且准确,但篇幅较长,需要带着问题阅读。
图 8:Docker 架构、隔离机制与运行时组件
图 9:Docker 架构、隔离机制与运行时组件
图 10:Docker 架构、隔离机制与运行时组件
在更完整的运行时链路中,客户端输入的 docker run 先到达 daemon,daemon 再通过 containerd 管理容器生命周期。containerd 会为具体容器创建相应的任务进程,并调用 runC 的运行时接口。真正负责创建、启动和销毁容器的是最底层的 runC,上层组件更像命令分发者和状态管理者。容器退出后,状态沿相反方向返回,最终才会在 docker ps 中显示为 Exited。
这个链路解释了为什么 docker ps、docker rm 等命令看似由客户端完成,实际却要经过多个进程。课程用 Nginx 的 master/worker 结构作类比:主进程接收和分派工作,多个 worker 负责并行处理请求。Docker 采用类似的分层思路,是为了同时处理多个容器和多个管理请求,而不是把所有任务塞进单一进程。
三、容器识别:用可观察证据确认当前位置
第一个实验问题是:拿到 WebShell 后,如何判断当前位置是在 Docker 容器还是宿主机。原稿没有把单一特征当作绝对结论,而是组合了文件、进程和命令可用性三个证据。首先观察根目录附近是否出现 .dockerenv。该文件在课程演示中出现在容器环境,而普通宿主机目录中没有,因此可以作为直观线索,但不能替代其他检查。
图 11:容器与宿主机的识别、进程数量和网络配置验证
图 12:容器与宿主机的识别、进程数量和网络配置验证
图 13:容器与宿主机的识别、进程数量和网络配置验证
图 14:容器与宿主机的识别、进程数量和网络配置验证
图 15:容器与宿主机的识别、进程数量和网络配置验证
图 16:容器与宿主机的识别、进程数量和网络配置验证
图 17:容器与宿主机的识别、进程数量和网络配置验证
第二个证据来自进程列表。容器中的 ps 通常只能看到数量很少的进程,往往只有主进程及其子进程;刚安装好的普通服务器即使没有部署太多服务,进程数量也明显更多。原稿先在 Nginx 容器里查看进程,再回到宿主机对照,发现两边的可见范围差异。判断依据不是“进程少就一定是容器”,而是“进程少、命令精简、并且出现容器标志文件”这些现象同时成立。
图 18:容器与宿主机的识别、进程数量和网络配置验证
图 19:容器与宿主机的识别、进程数量和网络配置验证
图 20:通过文件、进程和挂载信息判断容器边界
图 21:通过文件、进程和挂载信息判断容器边界
图 22:通过文件、进程和挂载信息判断容器边界
第三个证据是命令集合。为了减小镜像体积,容器镜像经常只保留运行服务所需的最小工具,许多常见命令并不存在。原稿在执行查看磁盘、网络和进程的操作时多次遇到命令缺失,这一失败反过来帮助确认了当前环境的精简属性。排查时应记录“命令不存在”本身,而不是把它误判成权限不足或服务异常。
图 23:通过文件、进程和挂载信息判断容器边界
图 24:通过文件、进程和挂载信息判断容器边界
图 25:通过文件、进程和挂载信息判断容器边界
图 26:通过文件、进程和挂载信息判断容器边界
图 27:通过文件、进程和挂载信息判断容器边界
图 28:通过文件、进程和挂载信息判断容器边界
图 29:通过文件、进程和挂载信息判断容器边界
图 30:通过文件、进程和挂载信息判断容器边界
图 31:通过文件、进程和挂载信息判断容器边界
随后课程把视角从“识别容器”转向“理解逃逸条件”。如果容器真的只能看到自己的进程、网络和挂载点,那么它与宿主机之间存在边界;一旦管理接口、危险挂载、特权能力或运行时组件破坏了这些边界,就可能从容器视角触达宿主机。后面的四个案例分别对应接口暴露、套接字挂载、启动权限过高和运行时版本缺陷。