Bezpłatne Dni Otwarte Sekurak Academy! Hackowanie na żywo, szkolenia, ebooki, …

#IngressNightmare – czyli jak przejąć klaster Kubernetes

04 kwietnia 2025, 16:24 | W biegu | 0 komentarzy

Podatności określane jako krytyczne mogą wzbudzać skrajne emocje. W sekuraku jesteśmy pewni, że nie wszyscy zgodzą się z punktacją CVSS 3.1 (9.8/10) przypisaną do serii podatności określonych jako IngressNightmare, które zostały opisane 24.03.2025 przez badaczy z wiz.io.

TLDR:

  • Badacze z Wiz.io opublikowali łańcuch eksploitów, który w pewnych warunkach pozwala wykonać kod, a nawet przejąć cały klaster. 
  • Atakujący nie zawsze musi być uwierzytelniony, jednak może potrzebować dodatkowej podatności jak np. SSRF (Server-Side Request Forgery) wewnątrz klastra
  • Podatność wynika ze sposobu działania (sprawdzania poprawności konfiguracji) w komponencie Ingress NGINX Controller, który jest bardzo popularnym modułem, ale nie jest wymagany czy używany domyślnie. 
  • Stosowne łatki zostały już wydane, administratorom zaleca się natychmiastową aktualizację lub wyłączenie tzw. admission webhooka

Problematycznym komponentem jest Ingress NGINX Controller, czyli ingress controller (kontroler ruchu wejściowego, spotkaliśmy również określenie “kontroler ingressu”) wykorzystujący znany serwer www i reverse proxy – nginx. O tym komponencie można myśleć jak o rozbudowanym routerze, który kieruje ruch przychodzący do odpowiednich usług wewnątrz klastra. Odbywa się to na podstawie informacji takich jak nazwa hosta czy URI. Dzięki temu żądania trafiają do odpowiednich podów (najmniejszej jednostki obliczeniowej składającej się z jednego lub więcej kontenerów), które są w stanie obsłużyć je zgodnie z wymaganiami. Według szacunków badaczy ponad 40% klastrów k8s korzysta z kontrolera wykorzystującego nginx (co nie znaczy, że wszystkie te klastry są podatne). Jest to też stosunkowo popularny projekt na GitHubie – Wiz podkreśla, że repozytorium z projektem ma ponad 18 000 gwiazdek, co świadczy o jego dużej popularności (było to jedno z kryterium wyboru celów do badań, co przyznali podczas wywiadu umieszczonego w serwisie YouTube). 

Sama podatność dotyczy funkcji walidacji (sprawdzenia poprawności) konfiguracji nginxa (administratorzy znają pewnie komendę nginx -t, która pozwala zweryfikować, czy plik jest poprawną konfiguracją serwera). 

Rysunek 1. Schemat ataku na klaster k8s, atakujący może uzyskać RCE, wysyłając odpowiednie żądanie do Ingress NGINX Controllera. W przypadku atakującego, który nie może wykonać zapytania z wnętrza klastra potrzebna jest dodatkowa podatność, np. SSRF z poda (źródło: wiz.io)

Weryfikacji konfiguracji dokonuje tzw. admission controller, którego zadaniem jest  sprawdzenie poprawności obiektu ingress. Ten kontroler jest dostępny z wnętrza klastra (a czasami z sieci zewnętrznej, jeśli np. został nieumiejętnie skonfigurowany) bez uwierzytelnienia – to samo w sobie powinno zapalać ostrzegawczą lampkę. A teraz najlepsze – otrzymując ingressowy obiekt, admission controller dokonuje jego przekształcenia w konfigurację nginx, a następnie uruchamia sprawdzenie jej poprawności. A to z kolei może skutkować otrzymaniem zdalnego wykonania kodu w podzie z kontrolerem. Dlaczego tak się dzieje?

W celu zapewnienia stabilnej pracy nginxa w środowisku kubernetesowym wprowadzono mechanizm walidacji konfiguracji przed jej uruchomieniem na klastrze. W tym celu wykorzystywany jest szablon konfiguracji, który pozwala przekształcić obiekty przesłane do kontrolera na ustawienia rozumiane przez serwer www/proxy. Przez brak uwierzytelniania i autoryzacji, każdy, kto ma dostęp do webhooka kontrolera, jest w stanie wstrzyknąć własną konfigurację, która zostanie przetestowana. 

// testTemplate checks if the NGINX configuration inside the byte array is valid
// running the command "nginx -t" using a temporal file.
func (n *NGINXController) testTemplate(cfg []byte) error {
...
    tmpfile, err := os.CreateTemp(filepath.Join(os.TempDir(), "nginx"), tempNginxPattern)
...
    err = os.WriteFile(tmpfile.Name(), cfg, file.ReadWriteByUser)
...
    out, err := n.command.Test(tmpfile.Name())
 
func (nc NginxCommand) Test(cfg string) ([]byte, error) {
    //nolint:gosec // Ignore G204 error
    return exec.Command(nc.Binary, "-c", cfg, "-t").CombinedOutput()
}

Listing 1. Fragment kodu wykorzystywany do testowania konfiguracji nginx (źródło: wiz.io/github.com)

Zapytanie AdmissionReview może zostać przygotowane np. z wykorzystaniem narzędzia kube-review. W oryginalnym blogpoście autorzy zwracają uwagę na to, że istnieje wiele pól, które są pod kontrolą atakującego.

Atak wymaga wykorzystania kilku podatności. Autorzy badania znaleźli kilka metod wstrzyknięcia własnych wartości do konfiguracji nginxa.

CVE-2025-24514

Ta podatność dotyczy parsera odpowiedzialnego za przetwarzanie adnotacji dotyczących adresów URL odpowiedzialnych za uwierzytelnianie. Brak odpowiedniego oczyszczenia i filtracji (proces zwany sanityzacją) danych wejściowych powoduje, że atakujący może dodać własne atrybuty, które trafią do tymczasowego pliku konfiguracyjnego nginxa. Ta podatność nie dotyczy kontrolera w wersji 1.12.0, gdzie zastosowano wyrażenia regularne do walidacji poprawności otrzymanych danych. 

proxy_http_version 1.1;
proxy_set_header Connection "";
set $target {{ changeHostPort $externalAuth.URL $authUpstreamName }};
{{ else }}
proxy_http_version {{ $location.Proxy.ProxyHTTPVersion }};
set $target {{ $externalAuth.URL }};
{{ end }}

Listing 2. Fragment konfiguracji z placeholderami na różne wartości dostarczana przez użytkownika (źródło: wiz.io/github.com)

Po wstrzyknięciu anotacji:

nginx.ingress.kubernetes.io/auth-url: "http://example.com/#;\ninjection_point"

konfiguracja serwera przyjmie postać przedstawioną na listingu 3. 

proxy_http_version 1.1;
set $target http://example.com/#;
injection_point
proxy_pass $target;

Listing 3. Wstrzyknięcie arbitralnych wartości przy pomocy auth-url annotation (źródło: wiz.io/github.com)

CVE-2025-1097

Następne CVE dotyczy wstrzyknięcia konfiguracji wykorzystujące do tego dziurawy parser authtls. Tym razem atakujący może wykorzystać pole Common Name (CN) do dodania własnych elementów konfiguracji. Pole to walidowane jest w sposób następujący:

unc CommonNameAnnotationValidator(s string) error {
    if !strings.HasPrefix(s, "CN=") {
        return fmt.Errorf("value %s is not a valid Common Name annotation: missing prefix 'CN='", s)
    }
    if _, err := regexp.Compile(s[3:]); err != nil {
        return fmt.Errorf("value %s is not a valid regex: %w", s, err)
    }
    return nil
}

Listing 3. Sprawdzenie poprawności Common Name (źródło: wiz.io/github.com)

CommonNameAnnotationValidator sprawdza, czy dostarczona wartość zaczyna się od liter CN, a następnie, czy spełnia założenia zawarte w wyrażeniu regularnym. Następnie wartość ta trafia do szablonu konfiguracji serwera WWW.

if ( $ssl_client_s_dn !~ {{ $server.CertificateAuth.MatchCN }} ) {
    return 403 "client certificate unauthorized";
}

Listing 4. Fragment szablonu zawierający wartość CN (źródło: wiz.io)

Aby konfiguracja nie została odrzucona podczas testów, należy pamiętać o odpowiednim obsłużeniu nawiasów zwykłych oraz klamrowych. Przykładowe wstrzyknięcie może wyglądać następująco: nginx.ingress.kubernetes.io/auth-tls-match-cn: „CN=abc #(\n){}\n }}\nglobal_injection;\n#” co zaowocuje poniższą konfiguracją:

set $proxy_upstream_name "-";  
if ( $ssl_client_s_dn !~ CN=abc #(  
){} }}
global_injection;  
# ) {  
return 403 "client certificate unauthorized"; }

Listing 5. Fragment konfiguracji z wartościami dodanymi przez atakującego. (źródło: wiz.io)

Eksploitacja tej konkretnej podatności wymaga jednak spełnienia dodatkowych warunków – do poprawnego działania wymagane jest jeszcze podatnie pasującego sekretu:  nginx.ingress.kubernetes.io/auth-tls-secret. Można to zrobić wykorzystując dostępne w klastrze przestrzenie nazw i załadować wartość skonfigurowaną już w klastrze. 

CVE-2025-1098

Ostatnią podatnością pozwalającą na wstrzyknięcie swojego kodu do konfiguracji jest mirror UID injection, za który odpowiada ten fragment kodu. Parser pozwala na wykorzystanie pola ing.field, które może zawierać dodatkowe elementy konfiguracji. Tym razem wartość ta nie jest poddawana żadnym sprawdzeniom i trafia prosto do konfiguracji nginxa. 

Atakujący, który wstrzyknął własne pola do konfiguracji kontrolera ingressu musi wykonać jeszcze jeden krok aby otrzymać zdalne wykonanie kodu – wykonanie go umożliwia CVE-2025-1974. 

CVE-2025-1974

Tak jak pisaliśmy na wstępie, plik konfiguracyjny jest testowany pod kątem poprawności. To sprawdzenie de facto wykorzystuje załadowanie konfiguracji, a to pozwala na wykorzystanie modułów nginx’a. Badacze wskazują tutaj na jeden szczególnie interesujący i nie do końca udokumentowany – ssl_engine. Jego działanie pozwala na załadowanie biblioteki współdzielonej, tak samo jak dyrektywa load_module, z tą różnicą, że nie musi się pojawić na samym początku konfiguracji tylko w jej dowolnym miejscu. Co znacznie ułatwia eksploitację. 

Pozostaje pytanie skąd wziąć plik .so, który pozwoli na wykonanie kodu. Tutaj z pomocą przychodzi nginx, którego instancja jest uruchomiona wewnątrz kontenera.

Rysunek 2. Procesy uruchomione w podzie obsługującym ingress nginx controller. (źródło: wiz.io)

Okazuje się, że serwer nginx pozwala na buforowanie żądania HTTP, którego wielkość przekracza skonfigurowaną wartość (domyślnie 8KB). Jednak zapisany plik jest natychmiast usuwany, co prowadzi do sytuacji wyścigu (tzw. race-condition), o bardzo niskim prawdopodobieństwie wygrania. 

Aby przedłużyć czas dostępności danych zbuforowanego zapytania, można wykorzystać sztuczkę polegającą na przekazaniu nagłówka Content-Length, który jest dłuższy od rzeczywistego rozmiaru danych. To spowoduje, że nginx będzie czekał na kolejne bajty, pozwalając na dostęp do zapisanego tymczasowo zapytania przy pomocy procFS

Ostatnią przeszkodą jest odgadnięcie PIDu oraz numeru deskryptora. PID jest potrzebny, ponieważ dostęp do pliku jest realizowany przez inny proces niż ten, który kontrolowany jest przez atakującego (dlatego /proc/self nie zadziała).

Wnikliwi Czytelnicy pewnie już mają w głowie pełny łańcuch eksploitacji, ale dla porządku go przedstawimy:

  1. Atakujący uploaduje zawartość biblioteki współdzielonej powodując zbuforowanie zawartości zapytania, a dodatkowo wykorzystuje zbyt długi content-length aby plik ten pozostał dłużej w systemie
  2. Wysyła nieuwierzytelnione zapytanie AdmissionReview do kontrolera, które wykorzystuje jedną z trzech technik wstrzyknięcia własnych dyrektyw
  3. Wstrzykuje dyrektywę ssl_engine, która pozwala na załadowanie biblioteki .so
  4. Wskazuje na bibliotekę .so zapisaną w punkcie pierwszym odgadując PID oraz deskryptor
  5. Wykonanie ataku — jeśli wszystkie gwiazdy ułożą się w odpowiedniej konfiguracji (czyli odgadnie wartości z punktu 4) to efektem jest RCE

Badacze podzielili się filmem obrazującym PoC:

Co należy zrobić?Najlepiej będzie dokonać aktualizacji Ingress NGINX Controller do wersji 1.12.1 lub 1.11.5 (lub nowszych), ponieważ wszystkie podatności zostały załatane przez developerów. Jednak nie zawsze istnieje taka możliwość, dlatego zalecaną akcją jest zablokowanie dostępu do admission controllera, tak aby tylko serwer API k8s miał do niego dostęp. Tymczasowe wyłączenie admission controllera jest również możliwe.

Ponadto zostały opublikowane szablony projektu nuclei, który może pomóc odkryć podatne instancje. Oprócz tego Wiz opublikował podatną konfigurację klastra pozwalającą na postawienie swojej instancji przy wykorzystaniu Terraforma. To dobry moment, żeby poćwiczyć eksploitację by zrozumieć istotę problemu :) 

~Big Hat Logan && evilcat

Spodobał Ci się wpis? Podziel się nim ze znajomymi:



Komentarze

Odpowiedz