CVE-2020-15257 Docker (容器逃逸)分析
2020-12-05 01:00:00 Author: bestwing.me(查看原文) 阅读量:368 收藏

CVE-2020-15257(Docker 容器逃逸)

前言

2020/11/30,公开了 CVE-2020-15257 的细节。该漏洞影响 containerd 1.3.x, 1.2.x, 1.4.x 版本

由于在 host 模式下,容器与 host 共享一套 Network namespaces ,此时 containerd-shim API 暴露给了用户,而且访问控制仅仅验证了连接进程的有效UID为0,但没有限制对抽象Unix域套接字的访问。所以当一个容器为 root 权限,且容器的网络模式为 --net=host 的时候,通过 ontainerd-shim API 可以达成容器逃逸的目的

ontainerd-shim

在进一步了解漏洞原理之前, 我们需要了解一下啊 ontainerd-shim 是什么?

在 1.11 版本中,Docker 进行了重大的重构,由单一的 Docker Daemon,拆分成了 4 个独立的模块:Docker Daemon、containerd、containerd-shim、runC

其中,containerd 是由 Docker Daemon 中的容器运行时及其管理功能剥离了出来。docker 对容器的管理和操作基本都是通过 containerd 完成的。

它向上为 Docker Daemon 提供了 gRPC 接口,向下通过 containerd-shim 结合 runC,实现对容器的管理控制。containerd 还提供了可用于与其交互的 API 和客户端应用程序 ctr。所以实际上,即使不运行 Docker Daemon,也能够直接通过 containerd 来运行、管理容器。

image-20201206002348825

而中间的 containerd-shim 夹杂在 containerd 和 runc 之间,每次启动一个容器,都会创建一个新的 containerd-shim 进程,它通过指定的三个参数:容器 id、bundle 目录、运行时二进制文件路径,来调用运行时的 API 创建、运行容器,持续存在到容器实例进程退出为止,将容器的退出状态反馈给 containerd

关于 containerd-shim 的作用细节可以参考作者的 slide

最终 ** containerd-shim ** 创建的容器的操作其实还是落实到了 runc 上, 而众所周知runC 是一个根据 OCI (Open Container Initiative)标准创建并运行容器的 CLI tool。

漏洞原因

漏洞原因在前言部分已经写得很清楚了,说白了就说 暴露了不该有的 API 接口,而 containerd-shim 的 API 接口由 Unix 域套接字 实现。代码实现位于

https://github.com/containerd/containerd/blob/b321d358e6eef9c82fa3f3bb8826dca3724c58c6/runtime/v1/linux/bundle.go#L136

实际上在, docker 容器中(以 –net=host 运行), containerd-shim API 大概长这样

image-20201206003829351

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/{sha256}.sock 这一类的抽象 Unix 域套接字不一样,它没有依靠 mnt 命名空间做隔离,而是依靠网络命名空间做隔离。

containerd 传递 Unix 域套接字文件描述符给 containerd-shimcontainerd-shim 在正式启动之后,会基于父进程(也就是 containerd)传递的 Unix 域套接字文件描述符,建立 gRPC 服务,对外暴露一些 API 用于 container、task 的控制:

通过查阅代码,我们大概知道我们如果能正常访问 containerd-shim 接口,我们大概能有这些操作

https://github.com/containerd/containerd/blob/v1.4.2/runtime/v1/shim/v1/shim.proto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
service Shim {

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);


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

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

rpc Wait(WaitRequest) returns (WaitResponse);
}

这些接口,从名字基本可以猜测与容器管理说有关系的, 比如 CreateStartDelete

通过查看代码

https://github.com/containerd/containerd/blob/v1.4.2/vendor/github.com/containerd/ttrpc/unixcreds_linux.go#L80

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29





func UnixSocketRequireSameUser() UnixCredentialsFunc {
euid, egid := os.Geteuid(), os.Getegid()
return UnixSocketRequireUidGid(euid, egid)
}

func requireRoot(ucred *unix.Ucred) error {
return requireUidGid(ucred, 0, 0)
}

func requireUidGid(ucred *unix.Ucred, uid, gid int) error {
if (uid != -1 && uint32(uid) != ucred.Uid) || (gid != -1 && uint32(gid) != ucred.Gid) {
return errors.Wrap(syscall.EPERM, "ttrpc: invalid credentials")
}
return nil
}

func requireUnixSocket(conn net.Conn) (*net.UnixConn, error) {
uc, ok := conn.(*net.UnixConn)
if !ok {
return nil, errors.New("a unix socket connection is required")
}

return uc, nil
}

UnixSocketRequireSameUser 仅仅检查了访问进程的 euid 和 egid ,而在默认情况下容器内部的进程都是以 root 用户启动,所以这个限制可以忽略不计。

漏洞利用

漏洞利用需要构建 gRPC ,我们可以通过查阅代码, 查看 ontainerd 项目呢关于 shim-client 是如何编写的

1
2
3
4
5
6
7
8
9
10
11

func WithConnect(address string, onClose func()) Opt {
return func(ctx context.Context, config shim.Config) (shimapi.ShimService, io.Closer, error) {
conn, err := connect(address, anonDialer)
if err != nil {
return nil, nil, err
}
client := ttrpc.NewClient(conn, ttrpc.WithOnClose(onClose))
return shimapi.NewShimClient(client), conn, nil
}
}

通过 ttrpc 构建 client,此时 conn 为 unix 套字节

然后返回 client

1
2
3
4
5
6
... ...
c, clo, err := WithConnect(address, func() {})(ctx, config)
if err != nil {
return nil, nil, errors.Wrap(err, "failed to connect")
}
return c, clo, nil
1
2
3
4
5
6
7
8
// ShimRemote is a ShimOpt for connecting and starting a remote shim
func ShimRemote(c *Config, daemonAddress, cgroup string, exitHandler func()) ShimOpt {
return func(b *bundle, ns string, ropts *runctypes.RuncOptions) (shim.Config, client.Opt) {
config := b.shimConfig(ns, c, ropts)
return config,
client.WithStart(c.Shim, b.shimAddress(ns, daemonAddress), daemonAddress, cgroup, c.ShimDebug, exitHandler)
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
func (r *Runtime) Create(ctx context.Context, id string, opts runtime.CreateOpts) (_ runtime.Task, err error) {
namespace, err := namespaces.NamespaceRequired(ctx)
if err != nil {
return nil, err
}

if err := identifiers.Validate(id); err != nil {
return nil, errors.Wrapf(err, "invalid task id")
}

ropts, err := r.getRuncOptions(ctx, id)
if err != nil {
return nil, err
}

bundle, err := newBundle(id,
filepath.Join(r.state, namespace),
filepath.Join(r.root, namespace),
opts.Spec.Value)
if err != nil {
return nil, err
}
defer func() {
if err != nil {
bundle.Delete()
}
}()

shimopt := ShimLocal(r.config, r.events)
if !r.config.NoShim {
var cgroup string
if opts.TaskOptions != nil {
v, err := typeurl.UnmarshalAny(opts.TaskOptions)
if err != nil {
return nil, err
}
cgroup = v.(*runctypes.CreateOptions).ShimCgroup
}
exitHandler := func() {
log.G(ctx).WithField("id", id).Info("shim reaped")

if _, err := r.tasks.Get(ctx, id); err != nil {

return
}

if err = r.cleanupAfterDeadShim(context.Background(), bundle, namespace, id); err != nil {
log.G(ctx).WithError(err).WithFields(logrus.Fields{
"id": id,
"namespace": namespace,
}).Warn("failed to clean up after killed shim")
}
}
shimopt = ShimRemote(r.config, r.address, cgroup, exitHandler)
}

例如这样的操作

更多的交互操作可以参考 张一白的 PoC

至于具体的利用,在这里就不进行细节探讨了,可以由读者自行完成。最后放一个我的利用视频

另外欢迎大家关注我的推特: https://twitter.com/bestswngs/status/1334867563914915840

安全建议

  1. 升级 containerd 至最新版本。

  2. 通过添加如 deny unix addr=@**的AppArmor策略禁止访问抽象套接字。

参考链接

https://www.chainnews.com/articles/937146786717.htm
https://github.com/containerd/containerd/security/advisories/GHSA-36xw-fx78-c5r4


文章来源: https://bestwing.me/CVE-2020-15257-anaylysis.html
如有侵权请联系:admin#unsafe.sh