CVE-2018-1002105是k8s的一个提权漏洞,提权中利用高权限websocket通道来窃取凭据文件、利用凭据文件接管apiserver的过程还是比较有趣,这里记录CVE-2018-1002105漏洞原理和利用。
漏洞基础知识
基于角色的访问控制(Role-Based Access Control)
基于角色的访问控制(Role-Based Access Control,即”RBAC”)使rbac.authorization.k8s.io API Group 实现授权决策,允许管理员通过 Kubernetes API 动态配置策略。
要启用 RBAC,请使用 --authorization-mode=RBAC 启动 API Server。
RBAC API 声明了四种 Kubernetes 对象:
Role #一系列权限的集合,通常是命名空间
ClusterRole #一系列权限的集合,通常是无命名空间
RoleBinding
ClusterRoleBinding
用户可以像使用其他 Kubernetes API 资源一样 (例如通过 kubectl、API 调用等)与这些资源进行交互。例如,命令 kubectl create -f (resource).yml
。
在 RBAC API 中,一个角色包含了一套表示一组权限的规则。 权限以纯粹的累加形式累积(没有” 否定” 的规则)。 角色可以由命名空间(namespace)
内的Role
对象定义,而整个 Kubernetes 集群范围内有效的角色则通过 ClusterRole
对象实现。
Role 对象
一个 Role 对象只能用于授予对某一单一命名空间中资源的访问权限。 以下示例描述了”default” 命名空间中的一个 Role 对象的定义,用于授予对pod的读访问权限:
kind: Role
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
namespace: default
name: pod-reader
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "watch", "list"]
ClusterRole 对象
ClusterRole 定义可用于授予用户对某一特定命名空间,或者所有命名空间中的secret的读访问权限:
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
# 鉴于 ClusterRole 是集群范围对象,所以这里不需要定义 "namespace" 字段
name: secret-reader
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "watch", "list"]
RoleBinding 与 ClusterRoleBinding对象
简单的来说就是把role和clusterrole定义的权限和我们的Role进行绑定的,二者的区别也是作用范围的区别:RoleBinding只会影响到当前namespace下面的资源操作权限,而ClusterRoleBinding会影响到所有的namespace。角色绑定包含了一组相关主体(即 subject, 包括用户 ——User、用户组 ——Group、或者服务账户 ——Service Account
)。
下面示例中定义的 RoleBinding 对象在”default” 命名空间中将”pod-reader” 角色授予用户”jane”。 这一授权将允许用户”jane” 从”default” 命名空间中读取 pod。
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: read-pods
namespace: default
subjects:
- kind: User
name: jane
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
websocket协议
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
例子:
1、客户端:申请协议升级
首先,客户端发起协议升级请求。可以看到,采用的是标准的HTTP报文格式,且只支持GET方法。
GET / HTTP/1.1
Host: localhost:8080
Origin: http://127.0.0.1:3000
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==
重点请求首部意义如下:
- Connection: Upgrade:表示要升级协议
- Upgrade: websocket:表示要升级到websocket协议。
- Sec-WebSocket-Version: 13:表示websocket的版本。如果服务端不支持该版本,需要返回一个Sec-WebSocket-Versionheader,里面包含服务端支持的版本号。
- Sec-WebSocket-Key:与后面服务端响应首部的Sec-WebSocket-Accept是配套的,提供基本的防护,比如恶意的连接,或者无意的连接。
2、服务端:响应协议升级
服务端返回内容如下,状态代码101表示协议切换。到此完成协议升级,后续的数据交互都按照新的协议来。
HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=
k8s apiserver代理转发功能
这个洞就是利用了api server具备的代理转发功能,比如kubectl的exec功能就是为进入目标pod的目标容器中执行命令(挂载标准输入和输出、标准错误的情景),kubectl exec
访问kube-apiserver
的connect接口,kube-apiserver把请求转发至对应节点的kubelet进程
漏洞原理:
CVE-2018-1002105漏洞大致原理是与k8s apiserver通信时切换websockect协议,处理出错时但apiserver却保留了这个通道,导致了打通了client到kubelet的通道。
漏洞处是在:staging/src/k8s.io/apimachinery/pkg/util/proxy/upgradeaware.go
发现有两处goroutine:
通过注释大概就能看出这是要建立一个proxy通道,而漏洞的点就是无论rawResponseCode会返回多少,都会成功走到这两个Goroutine中,建立起proxy通道。
这里就导致一个问题就是,如果一个正常请求在进行协议切换时,是会返回一个101的返回码,继而建立起一个websocket通道,该websocket通道是建立在原有tcp通道之上的,且在该TCP的生命周期内,其只能用于该websocket通道,而代码中没有对处理出错的异常进行捕获和判断, 无论是否有错都会保留这个TCP通道,造成TCP连接的复用,打通了client到kubelet的通道。
查看发送api 请求的代码在:
pkg/kubelet/server/server.go
,其中在InstallDebuggingHandlers
方法中注册了exec、attach、portForward
等接口:
而如果要构造失败的请求,pkg/kubelet/server/remotecommand/httpstream.go
,如果对exec接口的请求参数中不包含stdin、stdout、stderr三个,则可以构造一个错误:
回到staging/src/k8s.io/apimachinery/pkg/util/proxy/upgradeaware.go
中建立proxy
通道的上方, 发现属于tryUpgrade
函数:
这里tryUpgrade函数首先调用了IsUpgradeRequest方法进行请求的过滤,满足HTTP请求头中包含 Connection和Upgrade 要求的将返回True。
IsUpgradeRequest返回False的则直接退出tryUpdate函数,而返回True的则继续运行。
所以只需发送给API Server的攻击请求HTTP头中携带Connection/Upgrade Header即可运行到建立proxy的代码处。
环境搭建
而构造的请求需要是认证的用户和满足API server 往后端转发(通过HTTP头检测),且后端kubelet会返回失败,利用错误返回没有被处理导致连接可以继续保持的特性来复用通道打成后面的目的。
前面看了代码如果不包含stdin、stdout、stderr三个则可以构造一个错误。
构造一个命名空间test,和一个test命名空间的pod,原有权限是对test命名空间下的pod的exec权限,漏洞利用后将权限提升为了API Server权限,这里用metarget靶场起一个环境:
创建namespace:
apiVersion: v1
kind: Namespace
metadata:
name: test
创建role:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: test
namespace: test
rules:
- apiGroups:
- ""
resources:
- pods
verbs:
- get
- list
- delete
- watch
- apiGroups:
- ""
resources:
- pods/exec
verbs:
- create
- get
创建role_binding.yml:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: test
namespace: test
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: test
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: Group
name: test
创建pod:
apiVersion: v1
kind: Pod
metadata:
name: test
namespace: test
spec:
containers:
- name: ubuntu
image: ubuntu:latest
imagePullPolicy: IfNotPresent
# Just spin & wait forever
command: [ "/bin/bash", "-c", "--" ]
args: [ "while true; do sleep 30; done;" ]
serviceAccount: default
serviceAccountName: default
最后给用户配置一个静态的token文件来配置用户的认证:
当在命令行上指定 --token-auth-file=SOMEFILE 选项时,API server 从文件读取 bearer token。
token 文件是一个 csv 文件,每行至少包含三列:token、用户名、用户 uid:
token,user,uid,"group1,group2,group3"
这里使用到的配置token:
password,test,test,test
验证:
对指定test空间下的pod执行命令是可以的:
kubectl --token=password --server=https://192.168.1.22:6443 --insecure-skip-tls-verify exec -it test -n test /bin/hostname
对其他命名空间越权操作发现提示权限不足:
kubectl --token=password --server=https://192.168.1.22:6443 --insecure-skip-tls-verify get pods -n kube-system
漏洞复现:
exp:https://github.com/Metarget/cloud-native-security-book/blob/main/code/0403-CVE-2018-1002105/exploit.py
exp中也是会创建一个挂载宿主机根目录的pod,实现容器逃,而创建的基础是利用前面说的高权限websocket连接,利用这个连接向apiserver发送命令,窃取高凭据文件,再利用凭据文件创建pod,挂载宿主机根目录。
挂载了以后读取宿主机节点的/etc/kubernetes/pki目录下的大量敏感凭据:
exp中指定读取的证书文件:
利用:
这样就拿到了凭据,最后就是创建pod挂载宿主机根目录:
# attacker.yaml
apiVersion: v1
kind: Pod
metadata:
name: attacker
spec:
containers:
- name: ubuntu
image: ubuntu:latest
imagePullPolicy: IfNotPresent
# Just spin & wait forever
command: [ "/bin/bash", "-c", "--" ]
args: [ "while true; do sleep 30; done;" ]
volumeMounts:
- name: escape-host
mountPath: /host-escape-door
volumes:
- name: escape-host
hostPath:
path: /
host-escape-door 目录为pod挂载宿主机的目录,发现已经可以查看apiserver宿主机的目录: