شرح ثغرات JSON Based CSRF بأمثلة عملية

مرحبا متابعينا الأعزاء، اليوم سوف نتحدث عن أحد الثغرات التي كثيرا ما نقابلها كمختبرين اختراق لتطبيقات الويب، ألا و هي الـJSON Based CSRF. كثيرا ما تعتمد  الـ web applications علي تقنية JSON (اختصارا لمصطلح “JavaScript Object Notation”) لإرسال و استقبال الطلبات من و إلي السيرفر ، و قد تكون هذه الـweb applications معرضة لثغرة مثل CSRF. في هذه الحالة، استغلال الثغرة يكون أكثر تعقيدا من استغلالها في الحالات العادية.
لنفترض مثلا أننا كنا نقوم باختبار اختراق موقع معين، و اكتشفنا الطلب التالي:
POST /edit/1/username HTTP/1.1
Host: example.com
Content-Type: application/json
Content-Length: 19
{“username”:”test”}
و كل ما يقوم به الطلب هو إرسال الـusername الجديد و قيمته test إلي الـserver. قد يكون جليا لك أنه لا يوجد أي نوع من أنواع الـtokens في الطلب، مما يسمح لنا بالقيام بهجمات CSRF و تغيير الـusername للضحية بدون علمه إذا ما تم زيارة صفحة تحت تحكمنا. الخطوة التالية هي تحضير ملف HTML للقيام بالهجوم، و لكن كيف سنقوم بذلك  في الحالات العادية.
يتم إرسال الـparameters و قيمها في صورة parameter=value، و المقابل لذلك في كود الـHTML هو التالي:
<input type=”hidden” name=”username”’ value=”test” />
و لكن في هذه الحالة ما يرسل هو في صورة {“parameter”:”value”}، فماذا يمكننا فعله ؟

المحاولة الأولى:


يمكننا محاولة التلاعب بكود الـHTML لإرسال طلب مشابه للطلب السابق، و لكن مع بعض الاختلافات كالآتي:
k4n0j2d8

الكود السابق يستخدم form لعمل الـrequest، و يجب ملاحظة أن قيمة الـenctype للـform الموجودة هي text/plain، و ذلك لمنع المتصفح من القيام بعملية URL encoding للمدخلات قبل إرسال الطلب.
في الـform يوجد input ليس له قيمة، و لكن قيمة الـname فيه هي {“username”:”test”}، الآن لنرسل الطلب في المتصفح و نري ما يحدث (يمكن رؤية الطلب المرسل عن طريق استخدام أي proxy tool مثل Burp Suite):
POST /edit/1/username  HTTP/1.1
Host: example.com
Content-Type: text/plain
Content-Length: 24
={“username”:”hacked”}
ما الذي تم تحديداً؟ و لماذا هناك علامة = بعد الـpayload المرسل؟
كما حددنا للـform في كود الـHTML، تم إرسال الطلب و تحديد الـContent-Type كـtext/plain، و كما نري يتم إرسال الـpayload بدون عمل URL encoding. و لكن تم إرسال علامة =، و ذلك لأننا حددنا الـpayload المرسل كقيمة للـname في الـinput، و ما يفعله المتصفح هو أنه يأخذ قيمة الـname و يتبعها بعلامة = ثم يتبعهما بقيمة الـvalue، و التي هي فارغة في هذه الحالة.
الآن يمكن أن نقول أن الهجوم قد نجح حينما يتوفر الشرطين التاليين:
  • الـweb application لا يقوم بالتأكد من أن قيمة الـContent-Type header هي application/json
  • الـJSON parser يتجاهل القيم الزائدة في محتوي الطلب، مثل علامة الـ = في هذه الحالة

المحاولة الثانية:

لحسن الحظ، هناك حل أكثر عملية من الحل السابق، دعونا ننظر لكود الـHTML المعدل أدناه:

upotc2ah
في الكود المعدل قمنا بتغيير قيمة الـname و الـvalue بطريقة تسمح لنا بإرسال قيمة JSON سليمة، و في نفس الوقت تسمح لنا باستغلال الثغرة بطريقة سليمة و ناجحة. التالي هو الطلب المرسل للserver في هذه الحالة:
POST /edit/1/username  HTTP/1.1
Host: example.com
Connection: close
Content-Type: text/plain
Content-Length: 39
{“username”:”hacked”,”ignorable”:”=”}
ماذا تم في هذه الحالة؟
كما ذكرنا سابقا، المتصفح يرسل قيمة الـname ثم = ثم قيمة الـvalue، ففي هذه الحالة كل ما فعلناه هو أننا أضفنا قيمة زائدة اسمها ignorable عبر إضافة علامة “,” في الـJSON المرسل، و قيمتها في هذه الحالة هي =. هذه الطريقة لا تعمل في كل الحالات، حيث إنه يجب أن تكون إضافة parameters جديدة للطلب المرسل للـserver مسموحة من قِبَل الـweb application، و أن تكون قيَم هذه الـparameters متجاهلة من ناحية الـbackend code.
لعلك الآن تفكر في القيام بنفس الهجوم و لكن عن طريق استخدام XMLHttpRequest، لنتطرق لهذا الأمر و نري ما سيحدث. سنقوم بتعديل كود الـHTML ليحتوي علي الكود الآتي:
xss

في هذه الحالة تمت كتابة كود Javascript يقوم بالتالي:
  • خلق object جديد نوعه XMLHttpRequest و اسمه xhr، هذا الـobject هو ببساطة ما يقوم بإرسال الطلب للـserver لاحقا
  • تعديل xhr عن طريق تغيير قيمة attribute يدعي withCredentials لـ true، و في هذه الحالة سيتم إرسال الـcookies الخاصة بالموقع المصاب مع الطلب المرسل
  • استخدام دالة open لإرسال طلب POST للـURL المذكور
  • إضافة request header للطلب اسمه Content-Type و قيمته application/json حتي يتم تقليد الطلب الأصلي بدقة
  • و أخيرا، استخدام دالة send لإرسال الطلب فعلياً عن طريق تحويل الـpayload لـJSON object عبر استخدام دالة JSON.stringify ثم تمرير الـpayload الناتج لدالة send السابق ذكرها

أخيرا، يتم إرسال الطلب أوتوماتيكياً حين فتح الصفحة في أي متصفح، اﻵن سنفتح الصفحة و نستخدم Burp Suite أو أي أداة proxy حتي نتمكن من رؤية الطلب المرسل:
kchdaio2

لماذا تم إرسال طلب OPTIONS بدلاً من طلب POST؟
حينما يتم إرسال طلب بأي request method في المتصفحات الجديدة باستخدام XMLHttpRequest، يتم التأكد أولا من قيمة الـContent-Type header، إذا لم تكن تساوي text/plain يتم إرسال طلب OPTIONS قبل الطلب الأصلي، يعرف هذا النوع من الطلبات بـpre-flight requests، و يتم عادةً كمرحلة من مراحل التأكد من جدارة الموقع (في هذا السياق يسمي Origin) بإرسال هذا الطلب.
إذا احتوي رد الـserver علي الـpre-flight request على الـAccess-Control-Allow-Origin header و كانت قيمته هي * (بمعني أي Origin) أو كانت قيمته هي الـdomain المرسل للـpreflight-request (في حالتنا هو null و ذلك لأننا أرسلناه من الكمبيوتر الشخصي لدينا وليس من server)، يتم إرسال الطلب الأصلي فقط في هذه الحالة. يتم تجاهل الطلب الأصلي إن لم تكن هذه هي الحال.
فماذا إذا يمكننا أن نفعل إذا لم يكن الرد هو الرد المطلوب؟
كما فعلنا في المثال السابق، في هذه الحالة يجب إرسال الطلب بعد تغيير الـContent-Type header إلي text/plain ثم إرسال الطلب مجدداً، الصورة التالية هي للطلب بعدما غيرنا الـheader المذكور مسبقاً:

u2vtkj2h

كما نرى ، تم إرسال طلب POST كما أردنا و بدون إرسال pre-flight request كما حدث في المثال السابق، مع العلم بأن نجاح الهجوم في هذه الحالة يعتمد كل الاعتماد على الشرطين المذكورين سابقاً في المثال السابق.

أرجو أن تكونوا قد استفدتم من هذه المقالة.

================================