
إثبات مفهوم ومختبر لـ GitLab CE webhook SSRF عبر نطاق 0.0.0.0/8 this-network، مما يتيح لـ Maintainer الوصول إلى loopback وقراءة الاستجابات من سجلات hook.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · gitlab-webhook-0net-ssrf
المقال التقني: https://abraxaslabs.tech/research/gitlab-webhook-0net-ssrf
GitLab CE 19.4.1 - GitLab Inc.
يحجب Gitlab::HTTP_V2::UrlBlocker العنوان 0.0.0.0 و:: و127.0.0.0/8 بشكل دقيق. لكنه لا يحجب بقية النطاق 0.0.0.0/8. يعامل Linux هذا النطاق على أنه "هذه الشبكة" ويوجّه 0.0.0.1 إلى المكدس المحلي. يمكن لمسؤول صيانة (Maintainer) في مشروع إنشاء webhook على http://0.0.0.1:<port>/ بينما يكون allow_local_requests_from_web_hooks_and_services معطّلًا. يخزّن GitLab 8 KB من الاستجابة في سجلات الـ hook.
يمكن لمسؤول صيانة مشروعه الخاص أن يجعل GitLab يرسل طلب POST إلى loopback عبر 0.0.0.1 ويقرأ جسم الاستجابة من سجل الـ hook.
| المعرّف | لا يوجد CVE بعد |
| CWE | CWE-918 |
| CVSS | عالٍ: 8.5 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N |
| المنتج | GitLab CE |
| المتأثر | 19.4.1-ce.0 (26212baacadb على شجرة v19.4.1-ee) |
| المصادقة | مسؤول صيانة موثّق (admin_web_hook) |
| الترخيص | GNU Affero GPL v3.0 |
| المختبر | 127.0.0.1 فقط |
POST /api/v4/projects/:id/hooks مع url=http://0.0.0.1:<port>/. ثم التشغيل أو الاختبار. ثم قراءة response_body من واجهة API لأحداث الـ hook. في نشر omnibus الكل-في-واحد، يتشارك Sidekiq نفس netns الحاوية مع Puma وWorkhorse والمصدّرات (exporters). لا ينبغي لمسؤول صيانة مشروعه الخاص أن يقرأ تلك البيانات.
يبقى http://127.0.0.1/ وhttp://169.254.169.254/ محجوبين. هذه ليست ثغرة IMDS عبر link-local. إنها ثغرة "هذه الشبكة".
من نفس المنتج، بقايا مشابهة: Gitea HTTPS import pin drop، Gitaly fetch omits resolved_address.
قرأت url_blocker.rb بعد إصلاحات SSRF في 19.4.1. الدالة validate_localhost تتحقق من "::" و"0.0.0.0" بشكل دقيق إضافة إلى Socket.ip_address_list. أما validate_loopback فهي ipv4_loopback? (127.0.0.0/8) وloopback الخاص بـ ipv6. تغطي الاختبارات (specs) الأسماء البديلة الثمانية/السداسية عشرية لـ 127.0.0.1. لكنها لا تذكر 0.0.0.1 أبدًا.
الـ webhook هو مصرف الجسم: WebHookService يرسل POST ويخزّن 8 KB. يملك مسؤول الصيانة صلاحية admin_web_hook.
شغّلت نسخة أصلية من gitlab/gitlab-ce:19.4.1-ce.0. الطلبات المحلية معطّلة. ومستقبِل (catcher) داخل netns الخاص بـ GitLab على 0.0.0.1:18080. إنشاء hook على http://127.0.0.1:18080/ وhttp://169.254.169.254/ : 422 Invalid url given. إنشاء hook على http://0.0.0.1:18080/ : 201. اختبار أحداث push : 201. يحتوي response_body في أحداث الـ hook على GITLAB-WEBHOOK-0NET-SSRF-WITNESS.
مسارات خاطئة مسجّلة مسبقًا: /users/sign_in مع Accept: application/json يعطي 404؛ كلمة المرور الأولية التي تحتوي على root تُرفض بسبب WeakPasswords؛ إضافة 0.0.0.1 إلى lo تضعها في Socket.ip_address_list فيرفض validate_localhost الحالة "الإيجابية" بـ 422؛ IP_FREEBIND مع ip route add local 0.0.0.1/32 dev lo يربط دون الانضمام إلى قائمة العناوين؛ علامة $ في كلمة المرور داخل compose تحتاج $$. reverse shell. مسرح. الدليل الحاسم هو 201 على 0.0.0.1، و422 على 127.0.0.1، والشاهد في السجل.
cd lab
./run.sh
استهدف فقط http://127.0.0.1:18420. يحتاج GitLab CE إلى عدة غيغابايت من الذاكرة العشوائية. مختبر مُصرَّح به فقط.
negative-a 127.0.0.1 blocked
negative-b link-local blocked
positive-hook id=1
SUCCESS GITLAB-WEBHOOK-0NET-SSRF webhook response_body from 0.0.0.1:18080 hook_id=1 GITLAB-WEBHOOK-0NET-SSRF-WITNESS
def validate_localhost(addrs_info)
local_ips = ["::", "0.0.0.0"]
local_ips.concat(Socket.ip_address_list.map(&:ip_address))
return if (local_ips & addrs_info.map(&:ip_address)).empty?
raise BlockedUrlError, "Requests to localhost are not allowed"
end
def validate_loopback(addrs_info)
return unless addrs_info.any? { |addr| addr.ipv4_loopback? || addr.ipv6_loopback? }
raise BlockedUrlError, "Requests to loopback addresses are not allowed"
end
ارفض 0.0.0.0/8 (و ::ffff:0.0.0.0/104) بالطريقة نفسها التي يُرفض بها 127.0.0.0/8.
url_blocker.rb · web_hook_service.rb · project_hooks.rbGNU Affero GPL v3.0. راجع LICENSE. مختبر loopback فقط. دون أي ضمان.