
PoC استغلال وشرح تفصيلي لـ 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).