مرحباً قرائي الأعزاء، اليوم سوف نتحدث عن أحد الثغرات التي كثيراً ما نقابلها كمختبرين إختراق لتطبيقات الويب، ألا و هي ال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 كما حدث في المثال السابق، مع العلم بأن نجاح الهجوم في هذه الحالة يعتمد كل الاعتماد علي الشرطين المذكورين سابقاً في المثال السابق.
أرجو أن تكونوا قد استفدتم من هذه المقالة، لا تترددوا في ترك أسئلتكم و إستفساراتكم إذا ما كان هناك جزئية غير واضحه .