跳转至

CVE-2025-1974 — ingress-nginx 准入 Webhook 未授权远程代码执行

发布于 2026-06-06

严重级别 — 未授权远程代码执行

拥有集群 Pod 网络访问权限的攻击者,无需任何身份认证或集群凭据,即可在 ingress-nginx 控制器进程内执行任意代码。

字段
Project ingress-nginx
受影响组件 准入 Webhook(CheckIngress 处理器)、NGINX 配置模板
Severity CRITICAL
CVSS 9.8 — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H (v3.1)
CWE CWE-653
受影响版本 < 1.11.5>= 1.12.0-beta.0, < 1.12.1
修复版本 1.11.5(2025-03-25),1.12.1(2025-03-25)
安全公告 GHSA-mgvx-rpfc-9mpv

1. 漏洞概述

ingress-nginx 是使用最广泛的 Kubernetes Ingress 控制器。1.11.5 和 1.12.1 之前的版本暴露了一个准入 Webhook(ValidatingWebhookConfiguration),Pod 网络中的任意对等方无需凭据即可访问。该 Webhook 属于"IngressNightmare"组的五个相关 CVE 之一。CVE-2025-1974 是其中最严重的:未经身份认证的攻击者可在控制器进程内执行任意代码。默认集群配置下,ingress-nginx 控制器可读取集群范围内的所有 Kubernetes Secret,因此该漏洞可导致完整的集群凭据泄露。

根本原因

该攻击将三个独立缺陷串联,最终使网络位置的攻击者得以向控制器进程加载任意共享库。

缺陷一 — 准入处理器中未经沙箱隔离的 nginx -t 调用internal/ingress/controller/controller.goCheckIngress)。

提交 CREATEUPDATE Ingress 资源时,准入处理器依据 Ingress 的注解生成 NGINX 配置,然后在任何身份认证检查之前调用 nginx -t 对其进行验证。因此,精心构造的 AdmissionReview 载荷(无需凭据)即可令控制器对攻击者控制的配置执行 NGINX 配置测试。修复方案是完全禁用该调用:

// Before (vulnerable):
err = n.testTemplate(content)
if err != nil {
    n.metricCollector.IncCheckErrorCount(ing.ObjectMeta.Namespace, ing.Name)
    return err
}

// After (mitigated):
/* Deactivated to mitigate CVE-2025-1974
// TODO: Implement sandboxing so this test can be done safely
err = n.testTemplate(content)
if err != nil {
    n.metricCollector.IncCheckErrorCount(ing.ObjectMeta.Namespace, ing.Name)
    return err
}
*/

缺陷二 — NGINX 配置模板中注解值未加引号rootfs/etc/nginx/template/nginx.tmpl)。

多个 Ingress 注解值在插入 NGINX 配置模板时未加引号。注解值中嵌入的换行符因此可以突破当前配置上下文,在主上下文级别注入任意 NGINX 指令。本次漏洞利用的注入字段为 nginx.ingress.kubernetes.io/auth-tls-match-cn。修复方案是对插入值应用 quote 过滤器:

-        if ( $ssl_client_s_dn !~ {{ $server.CertificateAuth.MatchCN }} ) {
+        if ( $ssl_client_s_dn !~ {{ $server.CertificateAuth.MatchCN | quote }} ) {

-            set $target {{ $externalAuth.URL }};
+            set $target {{ $externalAuth.URL | quote }};

-            mirror {{ $location.Mirror.Source }};
+            mirror {{ $location.Mirror.Source | quote }};

缺陷三 — 通过错误 Content-Length 进行文件描述符暂存

NGINX 的 client_body_in_file_only on 设置将请求体缓冲到磁盘上的临时文件中。攻击者发送实际大小小于声明 Content-Length 的 HTTP 请求体,即可无限期保持 Socket(以及由此产生的缓冲文件)处于打开状态。该文件保留在控制器 Pod 内的可预测路径(/tmp/nginx/client-body/<N>),可通过路径穿越(../../../../tmp/nginx/client-body/<N>)从 NGINX 配置测试上下文访问。

三个缺陷组合起来:攻击者通过保持上传连接,将恶意共享库(.so)预暂存到控制器的临时存储;再发送一个 AdmissionReview,将 auth-tls-match-cn 注解设为注入 ssl_engine ../../../../tmp/nginx/client-body/<N>;,把该指令插入生成的 NGINX 配置主上下文。预身份认证阶段的 nginx -t 运行时,NGINX 对该路径调用 ENGINE_by_iddlopen 暂存的 .so 并触发其构造函数,在控制器进程内执行攻击者的代码。


2. 漏洞复现环境

本次复现使用 kind(Kubernetes-in-Docker)在本地创建 Kubernetes 集群。kind 在宿主机 Docker 上提供真实的多进程 Kubernetes 控制平面,包括文件描述符暂存上传连接与 AdmissionReview Webhook 调用之间真实的网络并发,这正是漏洞利用所依赖的时序条件。无需云资源。

环境组件:

组件 角色 版本 / 镜像
ingress-nginx 控制器 漏洞目标;同时提供 HTTP Ingress 监听器(用于文件描述符暂存)和准入 Webhook registry.k8s.io/ingress-nginx/controller:v1.11.4
stage-backend 良性上游(标准 nginx),使 stage.local Ingress 拥有实时 Endpoint,确保 NGINX 进入请求体读取阶段 nginx:1.27.3-bookworm
kind 控制平面 Kubernetes API 服务器及集群网络;支撑 Webhook kind 集群 cve-2025-1974

控制器在宿主机上通过 127.0.0.1:8080(HTTP Ingress 监听器)和 127.0.0.1:8443(准入 Webhook)对外暴露。漏洞利用客户端仅使用这两个端点,不持有任何 kubeconfig 或集群凭据。

预先创建的 Kubernetes Secret ingress-nginx/ingressnightmare-auth-ca(键名 ca.crt)用作渲染门控:ingress-nginx v1.11.4 的 authtls.Parse() 通过调用 getPemCertificate() 解析 auth-tls-secret 注解,该函数要求被引用的 Secret 包含 ca.crt 键。只有 Secret 解析成功,MatchCN 字段(承载注入的 ssl_engine 指令)才会被渲染进 NGINX 配置。攻击者仅需指定此 Secret 的名称,无法从中获取任何凭据。

环境文件可通过 env.zip 下载。单个文件:

启动环境:

bash env/up.sh

确认环境为漏洞版本:

运行冒烟测试:

bash env/smoke.sh

SMOKE OK 表示以下四个条件均满足:

  1. 控制器固定为 v1.11.4 版本且处于 Ready 状态。
  2. 127.0.0.1:8443 上的准入 Webhook 对空的未认证 POST 请求返回 HTTP 400(Webhook 正常运行)。
  3. stage.local Ingress 返回 HTTP 200 且后端 Endpoint 存活(HTTP 监听器处于请求体读取阶段,文件描述符暂存可将字节写入磁盘)。
  4. 包含键 ca.crt 的 Secret ingress-nginx/ingressnightmare-auth-ca 已存在(渲染门控已开启)。

如需验证完整链路充分性,运行:

bash env/positive-control.sh

该两阶段验证探针独立验证时序和渲染条件,不依赖漏洞利用程序:

  • 第一部分确认:保持的超声明 Content-Length 上传会将客户端请求体文件缓冲到控制器 Pod 内的 /tmp/nginx/client-body/<N>,该文件在并发 Webhook 请求期间保持驻留,并可通过 ../../tmp/nginx/client-body/<N> 穿越路径从 NGINX 配置测试上下文访问。
  • 第二部分通过同样的 auth-tls-match-cn 突破路径提交一个注入了无害哨兵指令(不加载攻击者代码)的 AdmissionReview,断言 Webhook 返回 allowed:false 且哨兵指令名称出现在 nginx -t 错误输出中,以此证明注入的文本确实到达了 NGINX 配置解析器。

运行漏洞利用程序前,两个部分均须通过。


3. 漏洞利用过程

漏洞利用文件可通过 exploit.zip 下载。单个文件:

步骤

第一步:确认环境就绪。

bash env/smoke.sh
bash env/positive-control.sh

两者均须通过后再继续。

第二步:选择新鲜的 nonce 和标记文件路径。

nonce 是验证方为本次尝试选择的字符串,在编译时嵌入 .so 构造函数。只有成功在控制器内 dlopen 该特定共享库的代码才能写入该 nonce。标记文件路径须是控制器进程(uid 101 / www-data)可写的位置。

# 示例(每次尝试使用新值):
NONCE="verify-1780300425-11267"
MARKER_PATH="/tmp/${NONCE}"

第三步:运行漏洞利用程序。

运行时须取消 KUBECONFIG 环境变量,以强制模拟无集群凭据的攻击者角色。

env -u KUBECONFIG bash exploit/run.sh verify-1780300425-11267 \
    /tmp/verify-1780300425-11267 ingress-nginx/ingressnightmare-auth-ca

三个参数分别为:

  1. <NONCE> — 写入标记文件的本次尝试字符串。
  2. <MARKER_PATH> — 在控制器 Pod 内写入 nonce 的路径。
  3. <SECRET_REF> — 用于开启渲染门控的可引用 auth-tls-secret。使用 env/up.sh 预先创建的 ingress-nginx/ingressnightmare-auth-ca,值须与之完全一致。

可选的第四和第五个参数(N_MIN N_MAX)控制客户端请求体计数器 N 的暴力扫描范围(默认 0 200)。

漏洞利用程序内部执行流程:

  1. build_so.shalpine:3.20 --platform linux/arm64 容器内编译 engine.c(控制器运行 musl/aarch64)。编译后的共享库的 __attribute__((constructor))dlopen 时将 <NONCE> 写入 <MARKER_PATH>
  2. run.pyhttp://127.0.0.1:8080Host: stage.local)发起 6 个并发 HTTP 上传,发送完整的 .so 内容但声明更大的 Content-Length,连接保持约 40 秒。NGINX 将请求体字节缓冲至控制器 Pod 内的 /tmp/nginx/client-body/<N> 文件。
  3. run.py 从 N=0 扫描至 N=200,为每个候选 N 提交一个 AdmissionReviewauth-tls-match-cn 注解值突破 NGINX 配置的 if 块,在主上下文级别注入 ssl_engine ../../../../tmp/nginx/client-body/<N>;。预身份认证阶段的 nginx -t 到达该指令时,调用 ENGINE_by_iddlopen 该路径对应的文件。当 N 与某个驻留暂存文件匹配,构造函数触发并写入标记文件。

漏洞利用程序不回读标记文件,该操作由验证方通过独立特权通道执行。程序在所有 201 个候选 N 值的 AdmissionReview 均处理完毕后以退出码 0 结束。

第四步:通过独立特权通道进行验证。

验证方通过 kubectl exec 进入控制器 Pod 读取标记文件,该通道是漏洞利用程序(无 kubeconfig)无法访问的。

KUBECONFIG=env/kubeconfig kubectl -n ingress-nginx exec \
  "$(KUBECONFIG=env/kubeconfig kubectl -n ingress-nginx get pod \
       -l app.kubernetes.io/component=controller \
       -o jsonpath='{.items[0].metadata.name}')" \
  -- cat /tmp/verify-1780300425-11267
KUBECONFIG=env/kubeconfig kubectl -n ingress-nginx exec \
  "$(KUBECONFIG=env/kubeconfig kubectl -n ingress-nginx get pod \
       -l app.kubernetes.io/component=controller \
       -o jsonpath='{.items[0].metadata.name}')" \
  -- stat -c 'uid=%u gid=%g owner=%U path=%n' /tmp/verify-1780300425-11267

证明漏洞触发的证据

在已验证的复现运行中,漏洞利用执行前标记文件不存在:

cat: can't open /tmp/verify-1780300425-11267: No such file or directory

漏洞利用程序运行后(网络受限,KUBECONFIG 已取消),验证方的 kubectl exec 读取到该文件,内容为:

verify-1780300425-11267

stat 输出:

uid=101 gid=82 owner=www-data path=/tmp/verify-1780300425-11267

控制器自身的 id 返回 uid=101(www-data),文件归控制器进程标识所有。标记文件的值与本次尝试选择的 nonce(verify-1780300425-11267)完全匹配,与漏洞利用源码中示例的 nonce(verify-1780300131-13064)不同,排除了硬编码或重放值的可能性。该文件仅由 .so 构造函数在 nginx -t 期间于控制器内部执行时写入,正是 1.11.5 和 1.12.1 补丁所移除的代码路径。

该观测结果与漏洞利用程序相互独立:漏洞利用程序没有 Pod 或节点访问权限,不运行任何 kubectl,也无法直接写入控制器的 /tmp 目录。nonce 到达标记文件的唯一路径是 nginx -tENGINE_by_iddlopen → 构造函数调用链。

环境销毁

bash env/down.sh

或等效命令:

env/bin/kind delete cluster --name cve-2025-1974

该命令删除 kind 集群 cve-2025-1974 并清理生成的 env/ca/ 材料。


4. 安全建议

修复措施

升级至 ingress-nginx 1.11.51.12.1,两者均于 2025-03-25 发布。补丁提交 e6716b13f237fb42a05117784fdee004e74fc801(kubernetes/ingress-nginx PR #13069 和 #13070)一次性修复了全部五个 IngressNightmare CVE。CVE-2025-1974 的关键修复点有两处:禁用准入处理器中预身份认证阶段的 nginx -t 调用,以及为所有插入 NGINX 配置模板的注解值添加引号。

若无法立即升级,可将准入 Webhook 服务(端口 8443)的网络访问限制为仅允许 Kubernetes API 服务器访问,以阻止任意 Pod 直接向控制器提交 AdmissionReview 请求。

缓解措施

  • 限制 Webhook 可达性。 Webhook(控制器服务的 8443 端口)应仅允许 API 服务器访问,不对任意 Pod 开放。通过 Kubernetes NetworkPolicy 限制控制器服务端口的入站流量,仅允许来自 API 服务器地址范围的流量。
  • 设置 --annotations-risk-level 较新版本的 ingress-nginx 支持 --annotations-risk-level 标志,将其设置为阻断未知或高风险注解可缩小注入攻击面,但该标志不能替代打补丁。
  • 最小化 Secret 访问权限。 默认安装下,ingress-nginx 控制器可读取集群范围内的所有 Secret。将其 RBAC 权限限定在所需命名空间内,可降低代码执行成功后的影响范围。

参考资料