الفوز بثغرات حالة السباق في الويب باستخدام single-packet attack

معظم الشروحات تخبرك أن race condition هي “نافذة زمنية بين الفحص والاستخدام”. صحيح، لكنه عديم الفائدة — لأن سبب فشل هجمات السباق لديك ليس أنك لا تفهم المفهوم. السبب أن النافذة أصغر مما هي عليه دقة الإنترنت.

نافذة السباق النموذجية — الفجوة بين قراءة التطبيق لـbalance وكتابته لـbalance - amount — تُقاس بـالميكروثواني إلى بضعة مللي ثوانٍ. أما التذبذب بين طلبَي HTTP تُرسلهما في اللحظة نفسها، بحلول وقت وصولهما وجدولتهما على الخادم، فيتراوح عادةً بين 1 و10 مللي ثوانٍ، وأسوأ عبر الإنترنت العام. فتصل طلباتك “المتزامنة” في صفٍ أنيقٍ صغير، ويعالجها الخادم واحدًا تلو الآخر، ويصمد الحد، فتستنتج أن نقطة النهاية آمنة. إنها ليست كذلك. أنت فقط خسرت السباق أمام التذبذب الشبكي قبل أن يكون للخادم رأي أصلًا.

حِرفة استغلال سباقات الويب بأكملها هي معركة لجعل N من الطلبات تصل في اللحظة نفسها. اربح هذه المعركة، ويسقط الحد “غير القابل للاستغلال” من الضربة الأولى.

لماذا لم تكن مزامنة البايت الأخير كافية تمامًا

الحيلة القديمة في HTTP/1.1 — مزامنة البايت الأخير (last-byte synchronization) — تقرّبك من الهدف: افتح نحو 20 اتصالًا، وأرسل كل طلب باستثناء بايته الأخير، ثم أطلق كل البايتات الأخيرة معًا. إنها تعمل، وكانت لسنوات هي أحدث ما توصلت إليه التقنية. لكن فيها عيبًا لا يمكن إصلاحه: تلك 20 اتصال TCP منفصلًا. لكلٍّ منها نافذة ازدحام خاصة به، وتوقيت مسار خاص، وجدولة نواة خاصة. وحتى مع احتجاز البايت الأخير، يظل الوصول متشتتًا عبر بضعة مللي ثوانٍ — وهو ما يكفي لخسارة نافذة بحجم ميكروثانية. تحوّل مزامنة البايت الأخير سباقًا احتماله 1 من 1000 إلى ربما 1 من 10. أفضل، لكنه لا يزال هشًّا، ولا يزال “لا يتكرر إلا من جهاز EC2 في المنطقة نفسها”.

single-packet attack: packet واحد، بلا تذبذب

إليك العنصر الأولي الذي غيّر المعادلة، من أبحاث James Kettle بعنوان Smashing the State Machine. يدمج HTTP/2 طلبات كثيرة عبر اتصال واحد (multiplexing). فبدلًا من التسابق عبر 20 اتصالًا، عليك أن:

  1. ترسل من 20 إلى 30 طلبًا عبر اتصال HTTP/2 واحد، لكن تحتجز الإطار (frame) الأخير من كل طلب (وهو الجزء الذي يخبر الخادم “هذا الطلب مكتمل”).
  2. عندما تُجهَّز كلها، أطلق كل الإطارات الأخيرة معًا — ولأنها صغيرة جدًا، فإن جميع الـ20-30 تتّسع داخل packet واحد من TCP بحجم نحو 1,500 بايت (بحجم MTU شبكي واحد).
  3. يُسلَّم ذلك الـpacket الواحد ذرّيًا (atomically). فيرى الخادم كل الـ20-30 طلبًا تصبح “مكتملة” في اللحظة نفسها ويجدولها معًا.

أصبح التذبذب الشبكي الآن غير ذي صلة، لأنه لا يوجد “بين الطلبات” — بل يوجد packet واحد. اختفت العشرة مللي ثوانٍ من ضجيج الإنترنت التي كانت تُسلسل طلباتك. السباق الذي كان يعمل على localhost فقط صار يعمل الآن من حاسوبك المحمول عبر WiFi، بموثوقية، وفي محاولة واحدة. هذا هو الفرق بين “قابل للاستغلال نظريًا” ولقطة شاشة في تقريرك.

هناك مأزق يلتهم كثيرًا من المحاولات الأولى: تسخين الاتصال (connection warming). إذا كان packet هجومك هو أول شيء على الاتصال أيضًا، فإنه يلتهم رحلات مصافحة TLS الذهاب والإياب وبطء بدء TCP (slow-start)، وتتشتت الطلبات من جديد. أطلق حفنة من الطلبات الاستهلاكية أولًا (بضعة طلبات GET /) لإكمال المصافحة ورفع نافذة الازدحام، ثم جهّز الهجوم. تجاهل هذا وستُقسِم أن التقنية لا تعمل.

ما بعد تجاوز الحدود: كيف تبدو الثغرات الجيدة فعلًا

“استرداد كوبون مرتين” هو الدرس التمهيدي. أما النتائج التي تؤتي ثمارها فهي هذه الأشكال الثلاثة:

تجاوز الحد على نقطة نهاية واحدة (Limit-overrun). الكلاسيكية، ولا تزال حاضرة حيثما يوجد المال:

  • بطاقات الهدايا / رصيد المتجر. طبّق بطاقة واحدة بقيمة 50 دولارًا على 10 عربات في packet واحد ← رصيد قيمته 500 دولار، لأن العشر عربات جميعها تقرأ “balance: $50” قبل أن تنقص أيٌّ منها الرصيد.
  • عمليات السحب / التحويلات. اسحب 100 دولار من رصيد 100 دولار خمس مرات — كل طلب يقرأ الرصيد قبل أن يُسجَّل أي خصم. إنفاق مزدوج، بكل بساطة.
  • حدود معدل 2FA / OTP. نقطة نهاية تسمح بـ"3 محاولات إدخال رمز، ثم قفل" تفحص عدّاد المحاولات قبل زيادته. أرسل 30 تخمينًا في packet واحد فتقرأ الثلاثون جميعها “attempts: 0” وتجتاز البوابة — تحصل على 30 محاولة لكل نافذة بدلًا من 3. وهذا غالبًا هو كل الفرق بين هجوم القوة الغاشمة (brute-force) الذي يستحيل تنفيذه وذاك الذي يُنجَز خلال استراحة الغداء.

التصادمات متعددة النقاط. الصنف الخفي عالي الخطورة: طلبان مختلفان يُوقَّتان للتصادم. المثال النموذجي هو التحقق من البريد الإلكتروني — أطلق confirm-email (لعنوان تتحكم به) وchange-email (إلى عنوان الضحية) معًا، فتنتهي إلى حالة يكون فيها بريد الضحية موسومًا بأنه مؤكَّد على حسابك. عملية الفحص والتنفيذ تمتد عبر نقطتَي نهاية، فلا ينقذها القفل على نقطة نهاية واحدة.

الحالات الفرعية الخفية. نقطة Kettle الأعمق: تمرّ عمليات كثيرة عبر حالة وسيطة لم يكن يُفترض أن تكون قابلة للرصد أبدًا — صف مستخدم موجود لكنه لم يكتمل، وجلسة موثّقة لكن نطاقها لم يُحدَّد بعد، وملف أُنشئ لكن لم تُضبَط قائمة التحكم بالوصول (ACL) له بعد. تتيح لك حالة السباق أن ترصد أو تتصرف أثناء تلك الحالة الفرعية. وهذه لا تُعلن عن نفسها بوصفها “حدودًا”، وهذا بالضبط سبب بقائها. تجدها عبر إجراء سباق بين قراءة وكتابة ومراقبة أن تُرجع القراءة شيئًا لا يُفترض أن يوجد بعد.

الكشف: عُدّ الأثر الجانبي، لا الاستجابات 200

الفخ الذي ينتج نتائج سلبية كاذبة ونتائج إيجابية كاذبة: الحكم بناءً على حالة HTTP. تفصيلان سينقذانك:

  • جدارٌ من 200 قد يكون كذبة. بعض الخوادم الخلفية تقبل كل الثلاثين طلبًا، ثم يرفض قيدُ فرادة (unique constraint) في قاعدة البيانات النسخ المكررة بصمت — فترى ثلاثين استجابة 200 ولا ثغرة. تحقق من الأثر الجانبي الفعلي: هل تحرّك الرصيد مرتين حقًا؟ هل أُنشئ صفّان فعلًا؟ رمز الاستجابة ليس هو النتيجة.
  • فوز واحد هو اكتشاف. لست بحاجة إلى نجاح الثلاثين جميعها. إذا أرجع 29 منها “الكوبون مستخدَم بالفعل” وحطّ استردادٌ واحد إضافي، فقد كُسر الحد — كل ما احتجته هو أن يُكسَر مرة واحدة. لا تتجاهل التشغيل لأن معظمه كان رفضًا.

أما ثغرات الحالة الفرعية غير المرئية، فـقِس معياريًا أولًا: أرسل دفعة وسجّل التوزّع الطبيعي للاستجابات، ثم ابحث عن الشاذّة — تلك الاستجابة الوحيدة ذات الطول المختلف، أو الحالة المختلفة، أو الكائن الذي لا يُفترض أن يكون مرئيًا. الشذوذ هو تسرّب الحالة الفرعية.

المطبّات التي تكلّفك الثغرة

  • غياب تسخين الاتصال ← يكون packet الأول بطيئًا ومتشتتًا. سخّنه.
  • مفاتيح idempotency. إذا كان العميل يرسل مفتاحًا فريدًا لكل إجراء واحترمه الخادم، انهارت الثلاثون نسخة المكررة لديك إلى واحدة. احذف المفتاح أو نوّعه وأعِد المحاولة.
  • القيود ضجيجٌ لا أحكام. يمكن لفهرس فرادة أن يهزم هذا السباق بينما لا يوجد أي قيد على نقطة نهاية مختلفة على الكائن نفسه. السباق الفاشل معلومة عن مسار كود واحد، لا شهادة بخلوّ التطبيق من الثغرات.
  • حداثة الحالة. الكوبون المخصص فعلًا للاستخدام مرة واحدة لن “يؤكّد” شيئًا بعد أول استخدام مشروع — ابدأ كل تشغيل من كائن نظيف غير مستخدَم.

لماذا يُدافَع عنه (وكيف)

من أجل قسم المعالجة في تقريرك: الإصلاح ليس أبدًا “أضِف فحصًا”. إنه الذرّية على طبقة البيانات (atomicity at the data layer)SELECT … FOR UPDATE، أو قيد فرادة (unique constraint)، أو إنقاص ذرّي (UPDATE … SET balance = balance - 1 WHERE balance >= 1)، أو القفل التفاؤلي (optimistic locking) بعمود إصدار. أي شيء ينفّذ قراءة-ثم-كتابة في كود التطبيق، عبر جملتين، فهو يتسابق مع نفسه. تُغلق مفاتيح idempotency صنف الإرسال المكرر.

المغزى

حالات السباق ليست نادرة — بل نادرًا ما تُعاد تجربتها، لأن تقنية جعل الطلبات تحطّ في اللحظة نفسها كانت، حتى وقت قريب، غير موثوقة بما يكفي جعل الناس يختبرون نقطة نهاية الكوبون الواضحة، فيفشلون، ويمضون. يزيل single-packet attack هذه الهشاشة. وهذا يعيد تأطير كل حدّ، وكل “يمكنك فعل هذا مرة واحدة فقط”، وكل تدفّق متعدد الخطوات في التطبيق بوصفه مرشّحًا يمكنك اختباره في packet واحد وثانيتين. لطالما كانت الثغرات موجودة؛ أما الآن فقد صار التوقيت مجانيًا.

يتحدث Crusader بلغة HTTP/2، فيستطيع تجهيز single-packet attack مباشرة — يسخّن الاتصال، ويحتجز الإطارات الأخيرة، ويطلقها معًا، ويقارن النتائج. اقرنه بأعمال التحكم في الوصول في العثور على ثغرات IDOR وBOLA — كلاهما ثغرة في منطق الأعمال لن يعثر لك عليها أي ماسح ضوئي أبدًا. حمّل Crusader مجانًا ووجّهه إلى أي حدّ يُقسم التطبيق أنه مُطبَّق.

الأسئلة الشائعة

ما هو single-packet attack؟
تقنية تضع من 20 إلى 30 طلب HTTP/2 كاملًا داخل packet واحد من TCP. ترسل كل طلب لكن تحتجز إطاره (frame) الأخير، ثم تطلق كل الإطارات الأخيرة معًا، فتصل الطلبات إلى الخادم في اللحظة نفسها. هذا يزيل التذبذب الشبكي الذي لولاه لتسلسلت الطلبات وأُغلقت نافذة السباق. قُدّمت هذه التقنية في أبحاث PortSwigger على يد James Kettle وتعمل عبر HTTP/2 وHTTP/3.
لماذا تحتاج حالات السباق الموثوقة إلى HTTP/2؟
عبر HTTP/1.1 لا يمكنك سوى تقريب التزامن باستخدام مزامنة البايت الأخير عبر عدة اتصالات TCP منفصلة، ويظل التذبذب بين تلك الاتصالات يشوّش وصولها بمقدار ملّي ثوانٍ. أما HTTP/2 فيدمج طلبات كثيرة عبر اتصال واحد (multiplexing)، فتستطيع حزمها في packet واحد ويكون الوصول متزامنًا فعلًا. أما هجمات HTTP/1.1 فترتد إلى الأسلوب الأقدم والأقل موثوقية.
ما هو تسخين الاتصال (connection warming) ولماذا يهمّ؟
هو إرسال بضعة طلبات استهلاكية قبل الهجوم كي تكتمل مصافحة TLS ويصبح الاتصال جاهزًا على جانب الخادم. بدونه، تؤخّر رحلات المصافحة الذهاب والإياب وبطء بدء TCP (slow-start) packet الهجوم بشكل غير متساوٍ وتعيد إدخال التشتت الزمني الذي وُجد single-packet attack أصلًا لإزالته.
جرّب Crusader مجانًا →