Azure容器实例中首次跨租户容器接管漏洞

1. 上篇

1.1. "每租户一个节点"模式

基于 Kubernetes 集群构建多租户环境中,隔离是通过节点虚拟机来实现的,每个客户的容器都运行在一个专用的、单租户的节点上,每个节点对应一个 Kubernetes pod。这种每个租户分配一个节点的Kubernetes 多租户模式通常被称为"每租户一个节点"模式。
Pasted image 20250416180123.png 本文就介绍一种跨租户攻击方法,恶意用户可以从自己的容器中逃逸出来,获取一个具有高权限的Kubernetes 服务账户令牌,并进而控制 Kubernetes api-server,就能完全控制多租户集群以及集群内运行的所有客户容器。

1.2. 当前容器逃逸到 Node 宿主机

1.2.1. 获取容器的运行时信息

使用 WhoC ,(一个特殊的容器镜像,它能够读取执行它的容器运行环境。它基于是 Linux 容器中设计缺陷,允许容器读取底层宿主机的容器运行时)。

部署 WhoC 后,成功获取了平台所用容器的运行时信息,运行时是行业标准的容器运行时 runC。比较出乎意料的是它的版本,

./aci_container_runtime -v

Pasted image 20250416222527.png

1.2.2. 使用旧版本 runC 漏洞逃逸

CVE-2019-5736,在容器当前K8S Node节点宿主机获取 root Shell 权限。
Pasted image 20250416222604.png
虽然逃离了容器,但仍然处于租户边界之内 — — 即节点 VM。
Pasted image 20250416180206.png

1.3. 节点信息收集

在进行节点信息收集时,Kubelet 的凭证是十分重要的,使用该凭证,列出了集群中的 pod 和节点,确认了容器是该节点上唯一的客户容器。这个集群托管了约 100 个客户 pod,并拥有大约 120 个节点。每个客户被分配到一个专属的 Kubernetes 命名空间中运行他们的 pod,我们的命名空间名为 caas-d98056cf86924d0fad1159XXXXXXXXXX

#查看集群版本信息
kubectl version

Pasted image 20250416222736.png

1.4. 尝试利用 K8S CVE-2018-1002102

利用原理
  • api-server 会不定期与 Kubelets 进行通信。例如,当执行 kubectl exec <pod> <cmd> 命令时,api-server 会将这个请求转发到相应的 Kubelet 的 /exec 接口,
  • CVE-2018-1002102 揭示了 api-server 与Kubelets 通信时存在任意URL重定向漏洞。通过重定向 api-server 的请求到另一个节点的 Kubelet,一个恶意的 Kubelet 可以在集群中利用此漏洞,

Pasted image 20250416180242.png
利用此漏洞需要满足以下前提条件:

  • 存在漏洞的 api-server 版本
  • 被入侵的节点
  • 使 api-server 访问被入侵节点

显然,目前满足了这三个条件,ACI 支持通过 az container exec 命令在容器中执行命令,这与 kubectl exec 的功能类似,

az container exec --name  --exec-command

接着创建了一个利用 CVE-2018-1002102 的自定义 Kubelet 镜像,将接收到的 exec 请求重定向到其他节点上的 pod。
为了提高危害,Unit 42 将其配置为目标指向 api-server pod,然后运行 az container exec my-ctr --exec-command /bin/bash ,期望在 api-server 容器上建立一个 shell,但利用失败了。

实际利用 Unit 42 发现重定向操作仅在目标容器和当前容器位于同一节点上时才有效,无法影响其他节点。

再次确定发送到节点的 exec 请求,我们原本预期这些请求会来自 api-server 的 IP,但请求 IP 实际上来自一个在默认命名空间运行的名为 k8s-agentpool1(下文用 'bridge' 代称) 的 pod,
Pasted image 20250416223247.png
ACI 将 exec 请求处理从 api-server 转移到了一个自定义服务,应该是通过将 az container exec 命令的路由指向 bridge pod,而不是直接发往 api-server 来实现的,

Pasted image 20250416180341.png

为什么CVE2018利用失败

bridge 镜像的标签是 master_20201125.1,表明它在 CVE-2018-1002102 发布后更新。从它的最新构建时间和拒绝重定向 exec 请求来看,看起来 CVE-2018-1002102 的补丁被移植到了 bridge pod。看来微软意识到这个漏洞会影响他们的自定义bridge pod 并给它打上了补丁。

1.5. 逃逸至 Cluster Admin 权限

由于 CVE-2018-1002102 利用不了,但在调试过程中,Unit 42注意到一个不寻常的情况:即从 brige pod 请求 至 K8S Node节点的 exec HTTP请求中包含了一个 Authorization 头部,携带着一个Kubernetes 服务账户令牌
Pasted image 20250416223631.png
抓取 brige pod 请求 node kubelet 的包

apt update > /dev/null 2>&1 && apt install -y mitmproxy > /dev/null 2>&1

# -d -d 输出详细
mitmproxy -d -d --reverse https://localhost:10025 -p 1234 >> /root/kubelet_comms &

# 把外部请求的10250端口流量转发到 1234端口
iptables -A INPUT -i eth0 -p tcp --dport 10250 -j ACCEPT
iptables -A INPUT -i eth0 -p tcp --dport 1234 -j ACCEPT
iptables -A PREROUTING -t nat -i eth0 -p tcp --dport 10250 -j REDIRECT --to-port 1234

Kubernetes 服务账户令牌是未加密的 JSON Web 令牌(JWTs),因此它们可以被解码。从下面的内容可以看出,收到的令牌属于 'bridge' 服务账户。鉴于请求是从 bridge pod 发起的,看着好像没啥问题,
Pasted image 20250416223642.png
一般拿到 token 都会通过 kubectl auth can-i 命令 查看 token 的权限,以下是 bridge 的 token 在默认命名空间的权限:
Pasted image 20250416223650.png
在查看其他命名空间发现权限是一样的,也就是说 token 是集群权限(而非仅限于特定命名空间的)
下面是该令牌在 kube-system 命名空间的权限。尝试找出一个可以让我们在多租户集群中都可用的权限:Pasted image 20250416223710.png
pods/exec 权限,这意味着该令牌可以用于在集群中的任何 pod 上执行命令 —— 包括 api-server pod
Pasted image 20250416223726.png
自此,多租户 k8s 集群就拿下了,能够管理多租户集群中的所有租户的容器。

1.6. 总结

  1. 利用 CVE-2019-5736 的漏洞部署一个恶意镜像到 ACI。该镜像实现了在 k8s Node节点 RCE,当通过 exec 等方式在改镜像执行命令后,即可触发在容器宿主机执行恶意命令,此时逃逸至 Node 节点宿主机。
  2. 在 K8S Node 节点上监听 Kubelet 端口流量(10250端口),等待包含 JWT token 的请求。
  3. 在受控容器上运行命令执行 az container exec 命令,bridge pod 随后会向受控节点的 Kubelet 发送HTTP 请求。
  4. 从HTTP请求头中获取 bridge token(cluster role token),并进入 apiserver pod 中获取集群昀高权限。

1.7. 修复:

在执行 exec 时,去除 bridgePod 请求节点时携带的 JWT token。应该属于历史遗留功能,忘记删除了。

1.8. 参考文章

2. 下篇

2.1. 前言

紧接上篇逃逸到 Node 宿主机后,另一条拿下K8S集群路径 —— Bridge SSRF。

2.2. 发现 SSRF 漏洞

当 bridge pod 处理一个 az container exec <ctr> <cmd> 命令时,它会向相应的 Kubelet 的 /exec 接口发送请求,

https://<nodeIP>:10250/exec/<customer-namespace>/<customer-pod>/<customer-ctr>?command=<url-encoded-cmd>&error=1&input=1&output=1&tty=1

bridge 需要以某种方式填充尖括号 < > 中缺失的参数,其中 <nodeIP> 的值是从客户 pod 的status.hostIP 字段中获取的。当节点被授权更新其 pod 的状态(例如,更新其 pod 的status.state 字段为 Running 、 Terminated 等)时就比较有用了。

2.3. 篡改 status.hostIP

尝试使用受控节点的 token 更改我们 pod 的 status.hostIP 字段,hostIP 字段更新了,但在一两秒后,api-server 将 hostIP 字段更正为原始值。虽然更改没有持久化,但并没有什么能阻止我们反复更新这个字段,

Unit 42 编写了一个小脚本,不断更新 pod 的状态,并将 status.hostIP 字段设置为 1.1.1.1 ,然后执行了一个 az container exec 命令。虽然失败了,但证实了 bridge 将 exec 请求发送到了1.1.1.1 而不是真实的节点 IP。接下来就是考虑如何利用特殊构造的 hostIP 欺骗 bridge 执行对其他pod 的命令,

仅将 pod 的 status.hostIP 设置为另一个节点的 IP 不起作用,因为 Kubelets 只接受指向它们托管的容器的请求。即便 bridge 将 exec 请求发送到另一个 Kubelet 的 IP,URL 仍然会指向我们的命名空间、pod 名称和容器名称,因为我们没办法修改这个请求的 <customer-namespace> <customer-pod><customer-ctr> 参数

https://<nodeIP>:10250/exec/<customer-namespace>/<customer-pod>/<customer-ctr>?command=<url-encoded-cmd>&error=1&input=1&output=1&tty=1

Pasted image 20250416180633.png

2.4. 构造异常的 hostIP 值

Unit 42 随后发现到 api-server 实际上并不验证 status.hostIP 的值是否为有效的 IP,它会接受任何字符串 —— 包括 URL 部分。经过几次尝试,Unit 42 找到了一个 hostIP 值,可以让 bridge 执行对api-server 容器的命令,而非我们自己的容器:

hostIP 值: <apiserver-nodeIP>:10250/exec/kube-system/apiserver-pod/apiserver-container?command=whoami&error=1&input=1&output=1&tty=1# 会让 bridge 将 exec 请求发送到以下 URL:

https://**<apiserver-nodeIP>:10250/exec/kube-system/<apiserver-pod-name>/<apiserver-ctr>?command=<url-encoded-command>&error=1&input=1&output=1&tty=1**<u>#</u>:10250/exec/<customer-namespace>/<customer-pod-name>/<customer-ctr-name>?command=<command>&error=1&input=1&output=1&tty=1

bridge 初始的命令模板:

https://<nodeIP>:10250/exec/<customer-namespace>/<customer-pod>/<customer-ctr>?command=<url-encoded-cmd>&error=1&input=1&output=1&tty=1

使用 # 后缀确保 URL 的其余部分被视为 URI 锚点,从而忽略后面的异常部分。
例如: https://www.baidu.com#:10250/exec///?command=&error=1&input=1&output=1&tty=1
URL 中的 # 符号后的部分被称为锚点,属于前端的功能,与 HTTP 请求无关。

2.5. 利用 SSRF 成功 RCE master 节点

我们将我们的 pod 的 status.hostIP 设置为这个值,并通过 az container exec 执行命令,利用成功并获得 api-server 容器的 shell。

2.6. 总结

  1. bridgePod 的 SSRF 漏洞:当 Bridge Pod 执行命令 az container exec <ctr> <cmd> 时,会向相应 Kubelet 的 /exec 端点发起请求。这个请求的 URL 是由多个参数拼接而成的,其中<nodeIP> 的值来自客户 Pod 的 status.hostIP 字段。
  2. 篡改 Pod 的 status.hostIP 字段:尝试利用受控Node节点的凭证来修改其 Pod 的status.hostIP 字段。尽管 apiserver 服务器会迅速纠正该字段的值,但Unit 42发现可以不断地对其进行更新。
  3. 构造异常的 hostIP 值:尽管节点会对 status.hostIP 进行有效性校验,但 apiserver 并不会验证该字段是否为有效的 IP 地址,而是接受任何字符串,包括 URL 组件。因此,我们可以构造了一个特殊的 hostIP 值,使得 bridge Pod 的执行请求被发送到 apiserver 容器,而非自己的容器。
  4. 成功实施攻击:将我们的 Pod 的 status.hostIP 设置为构造好的值,并通过 az container exec 发起命令。利用成功后即获得了对 apiserver 容器的 shell 访问权限。

2.7. 修复:

bridge 在发送 exec 请求之前会验证 pod 的 status.hostIP 字段是否为有效的 IP 地址。