
أداة استغلال PoC بلغة Python للثغرة CVE-2026-85706، وهي قراءة ملفات عشوائية دون مصادقة في GitLab CE/EE عبر تجاوز ترميز المسار في Workhorse، مع تحليل مكتوب وبدائل للتجاوز.
قراءة ملف محلي عشوائي بدون مصادقة في GitLab CE/EE. يؤثر على 18.7–19.1.7، 19.2.0–19.2.5، 19.3.0–19.3.1. تم إصلاحه في 19.1.8 / 19.2.6 / 19.3.2 (2026-09-10). CVSS 10.0، ويُقال إنه استُغل فعليًا في البرية. التقرير الأصلي بواسطة s3ntago وهذا المستودع مجرد تدوينتي + PoC.
نُشر هذا الـ PoC لأغراض تعليمية وبحثية دفاعية فقط، لمساعدة المسؤولين والباحثين على فهم الثغرة واختبارها. شغّله حصريًا ضد الأنظمة التي تملكها أو لديك تفويض كتابي صريح لاختبارها. إذا كان نسختك ضمن النطاق المتأثر، توقف عن القراءة وقم بالترقيع إلى 19.1.8 / 19.2.6 / 19.3.2 أولًا.
ثلاثة نقاط نهاية للمستودع (POST :id/repository/commits، POST/PUT :id/repository/files/:file_path) تقع خلف requestBodyUploader الخاص بـ Workhorse. يقرأ معالج Rails المسار على القرص مباشرةً من حقل الطلب الخام file.path ويقوم بـ File.open له قبل أي مصادقة. لا يعمل كمصادقة لأن round-tripper الموقّع الخاص بـ Workhorse يرفق JWT صالحًا بكل طلب يمرره، لذا أي شيء يمر إلى وكيل API العام يجتاز هذا الفحص.
require_gitlab_workhorse!Gitlab-Workhorse-Api-Requestالسبب الوحيد لعدم كون هذا LFI فوريًا للجميع هو أنه يُفترض أن يقوم Workhorse بإعادة كتابة الطلب أولًا. لكن regex المسار الخاص به يطابق المسار المُهرَّب (EscapedPath() بالإضافة إلى نسخة path.Clean لا تفك ترميز %XX أبدًا)، بينما يقوم Puma بفك ترميز %XX قبل توجيه Grape. لذا قم بترميز النسبة المئوية لأي حرف من مقطع ثابت (%63ommits، %72epository، %66iles)، أو أضف شرطة مائلة زائدة، أو ألحق .json الذي يفوته regex الخاص بـ Workhorse، وسيظل Rails يوجه إلى المعالج المصاب. عدم تطابق الترميز هذا هو مكان تجاوز الحماية. (متغيرات //، /./، %2F، ; كهذه لا تعمل لأن path.Clean يطبّع الأولين و Puma يرفض %2F.)
ثم فقط أرسل بيانات وصفية لرفع مزوّرة غير موقّعة كمعاملات استعلام:
POST /api/v4/projects/1/repository/%63ommits?file=&file.path=<ABSOLUTE_PATH>&file.size=1&Content-Type=application/x-www-form-urlencoded
file= الفارغ يفي بتحقق requires :file, WorkhorseFile (الفراغ يُحوَّل إلى nil). تحدث القراءة قبل المصادقة. الحصول على البايتات مرة أخرى هو الجزء الممتع: في فرع urlencoded يقوم المساعد بتشغيل Rack::Utils.parse_nested_query(File.read(path)) ويُدرج أخطاء المحلل في جسم الاستجابة 400. أي % غير متبوع برقمين سداسيين عشريين يرفع InvalidParameterError: invalid %-encoding (<raw file bytes>) ويعود محتوى الملف داخل رسالة الخطأ. فرع JSON (Oj) لا يسرب شيئًا، ولهذا يهم نوع المحتوى urlencoded هنا.
الإصلاح (master 0d9ce3e7، backports 1fe30154 / b43c8b26 / 0ff7b6b2) يضيف authenticate! إلى جميع نقاط النهاية الثلاث بالإضافة إلى خطوة /authorize المسبقة، ويثق فقط بـ UploadedFile المنتج من الوسيط للمسار/الحجم، ويتوقف عن إرجاع أخطاء المحلل. أُدخل الخطأ في ديسمبر 2025، ولهذا يبدأ النطاق المتأثر من 18.7.
./exploit.py --url http://localhost:8080 --file /etc/hostname
./exploit.py --url http://localhost:8080 --file /opt/gitlab/embedded/service/gitlab-rails/config/gitlab.yml
يتنقل السكربت عبر جميع أشكال التجاوز المُتحقق منها (مقاطع مُرمَّزة، شرطة مائلة زائدة، .json؛ نقاط نهاية commits + files، POST و PUT) ويصنّف كل استجابة حتى تتمكن من معرفة أين فشل الفحص في السلسلة. أهداف مُصرَّح بها فقط، بالطبع.
% غير متبوع بحرفين سداسيين عشريين.(.*) حتى آخر ) في الجسم). جيد ضد خطأ JSON الافتراضي، لكنه سيلتقط أكثر من اللازم إذا غلّف شيء ما الاستجابة في HTML. الإصلاح الصحيح هو تحليل حقل رسالة JSON.before تُرجع 404 بخلاف ذلك).