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 来运行、管理容器。
而中间的 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 大概长这样
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-shim。containerd-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); }
|
这些接口,从名字基本可以猜测与容器管理说有关系的, 比如 Create
、Start
、Delete
通过查看代码
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
安全建议
升级 containerd 至最新版本。
通过添加如 deny unix addr=@**的AppArmor策略禁止访问抽象套接字。
参考链接
https://www.chainnews.com/articles/937146786717.htm
https://github.com/containerd/containerd/security/advisories/GHSA-36xw-fx78-c5r4