در این پست قصد دارم درباره ی آسیبپذیری جالبی که از یکی از سرویسهای آفیس مایکروسافت پیدا کردم براتون توضیح بدم. office365 دارای سرویسهای مختلفی هستش که هر کدوم یکسری امکانات مختلف ارائه میده
یک سرویسی که توجه منو جلب کرد، سرویس forms بود که برای ساخت فرم یا کوییز و اشتراک گذاریه فرم بین یوزرهای مختلف انجام میشد. میتونید جزییات بیشتر رو داخل مستندات مایکروسافت ببینید :
اپلیکیشنهای تحت وب مایکروسافت مثل office365 و تمام سرویس های مرتبط آن، azure active directory و … از OData برای دسترسی به یک منبع داده و عملیات CRUD در بستر وب استفاده میکنند که سرویس forms هم به همین صورت هستش و با یک نگاهی به ساختاره درخواستی که به سمت سرور ارسال میشه ، میتونیم متوجه بشیم که از آخرین نسخه ی OData یعنی ورژن چهار داره استفاده میکنه :
مفاهیم OData
OData یا Open Data protocol یک پروتکل مستقل از پلتفرم و به صورت Rest-style هست که دسترسی به یک منبع داده از طریق وب رو برای ما فراهم میکنه(معادل SQL یا ODBC در بستر وب) . OData از ایده ی زبان SQL استفاده میکنه. به این صورت که میگه من کاری ندارم که چه نوع کلاینتی(مثل دات نت ، جاوا و …) به من درخواست ارسال کرده و یا منبع دادهی سایت چی هستش، درخواست رو از سمت کلاینت دریافت میکنم و بر اساس نوع کاری که کلاینت از من داره، عملیات CRUD رو بر روی دیتا انجام میدم و نتیجه رو به کلاینت برمیگردونم :
در ادامه برای درک بهتره این رایت آپ یکسری مفاهیم از این پروتکل رو براتون توضیح میدم و یکسری معادلسازی انجام میدم . هر OData یک فایل به نام metadata داره که یک data model از سیستم برای ما نشون میده(میتونید معادل information_schema در نظر بگیرید)
به صورت کلی داخل metadata، لیست entity typeها ( معادل جدول ها در دیتابیس های رابطه ای) و property ها (معادل ستون های جدول ها) به همراه رابطه ی بین entity type های مختلف رو شامل میشه. هر entity type یک entity key داره که معادل کلید در جداول دیتابیس های رابطهای میشه در نظر گرفت(در OData در تگی به نام propertyRef مشخص میشه )
فرض کنیم یک entity type به نام Customers داریم که شامل سه تا property به نام ID ،name ،email هست . این entity type دو رکورد به صورت زیر داره:
name email ID
Ali ali@gmail.com 1
Borna borna@gmail.com 2
که در اینجا ID به عنوان یک شناسه ی یکتا و entity key در نظر گرفته میشه. اگه کلاینت رکورد یوزری رو بخواد که ID=2 داره، درخواست زیر رو باید ارسال کنه :
customerApi/Customers(2)
دقت کنید که ورودیه Customers یک entity key هستش که در این مثال ID بود. مثال بالا تمام اطلاعات یوزر با آیدی 2 رو برمیگردونه.
مشابه SQL ، میتونیم از دستورات مختلف برای انجام عملیات روی دیتا استفاده کنیم. در همین مثال بالا اگه بخوایم ایمیل یوزری رو بگیریم که آیدیه 2 داره ، درخواست زیر رو باید ارسال کنیم :
customerApi/Customers(2)?$select=email
select$ یک query option هستش (معادل SELECT در SQL ) . عبارت بالا در SQL به صورت زیر هست(دوستانی که با OData آشنایی داشته باشند، میدونن که میتونیم در درخواست بالا به جای قرار دادن entity key در Customers، از filter$ استفاده کنیم و در هر دو حالت به یک خروجیه یکسان برسیم)
SELECT email FROM Customers WHERE ID=2;
مفاهیمی که تا اینجا توضیح دادم برای درک ادامه ی این رایت آپ کافیه. به غیر از select ، میتونید از Query option های مختلف استفاده کنید. مثلا میتونید فرمت خروجیه دیتا رو با استفاده از format$ مشخص کنید که فرمت خروجی JSON باشه یا XML. میتونید با استفاده از top$ ، محدودیت در رکورد دریافتی بذارید (معادل limit). پیشنهاد میکنم برای جزییات بیشتر و آشنایی با query option های دیگه، به این لینک مراجعه کنید که منبع اصلی OData هست .
بررسی سناریوهای مختلف حمله
یک نکتهای که من همیشه در نظر میگیرم اینه که ابتدا سعی میکنم یک دید کلی نسبت به سیستم یا ساختار سیستم بدست بیارم و بعد شروع کنم به تست تک تک مولفههای سیستم. در این تارگت ابتدا سعی کردم به metadata دسترسی پیدا کنم چون میدونستم تارگت داره از OData استفاده میکنه و بهتره که یک آشنایی کلی با ساختاره OData این سیستم پیدا کنم. با ارسال یک درخواست به مسیر زیر تونستم به metadata تارگت دسترسی پیدا کنم:
http://forms.office.com/formapi/api/$metadata
فرم نمایش بالا که به صورت یک سند XML هست، زیاد برام جالب نبود. بنابراین برای اینکه بتونم ارتباطات بین entity type های مختلف رو بهتر ببینم از وبسایت زیر استفاده کردم. این وبسایت بیشتر توسط توسعه دهندگان api و برنامهنویسان استفاده میشه. این وبسایت یه قابلیت خوبی که داره اینه شما لینک حاوی metadata رو بهش میدید و به صورت visual ارتباط بین entity typeهای مختلف رو بهتون نشون میده:
قابلیت خوب دیگه ی این وبسایت اینه شما یک entity type رو انتخاب میکنید به همراه property هایی که میخواید . اگه اجازهی دسترسی به entity type رو داشته باشید میتونید اطلاعات مختلفی رو ازش بیرون بکشید:
بعد از این که یک دید کلی نسبت به این تارگت بدست آوردم، سعی کردم دنبال entity typeهایی بگردم که میتونن شامل اطلاعات مهمی مثل اطلاعات یوزرهای سیستم باشند. یک entity type توجه منو جلب کرد و اونم forms بود. این entity type شامل اطلاعات فرم ساخته شده توسط یوزر و همینطور ایمیل یوزر بود:
ایمیل یوزر باعث شد به این فکر کنم که آيا میتونم یک راهی پیدا کنم که به ایمیل یوزرهای دیگهی سیستم هم دسترسی پیدا کنم؟ برای همین شروع کردم به تست های مختلف مثل IDOR که ببینم با عوض کردن یوزر آیدی میتونم به ایمیل یوزر دیگه دسترسی پیدا کنم یا نه که دیدم نمیتونم. تست CORS هم اینجا جواب نمیداد چون با بررسی درخواست ارسالی دیدم حتی اگه هدر Origin در سمت سرور به درستی validate نشه، بازم برای اکسپلویت کردن نمیتونم درخواست CORS ارسال کنم چون اینجا نیاز به چندین هِدِر دیگه داشتم.
بعد از تست کیسهای مختلف، نهایتا با frame کردن این مسیر و پیاده سازیه یک سناریوی حمله، میشد به ایمیل یا اطلاعات یوزرای دیگه رسید ولی مشکل اینجا بود که سناریوی من نیاز به تعامل مستقیم با یوزر داشت(که مثلا قربانی وارد سایت هکر بشه و …) که همین باعث میشد impact این آسیب پذیری پایین بیاد.
بعد از اینکه ریپورتم رو برای مایکروسافت ارسال کردم، چون impact آسیب پذیری در سطح low بود، آسیب پذیری قابل قبول بود و اسمم هم در Hall of fame مایکروسافت ثبت شد ولی خبری از بانتی نبود.
مایکروسافت بر خلاف برنامه های بانتیه دیگه ای که داخل هکروان هست یا کمپانی های دیگه قوانین سخت تری داره و برای آسیب پذیری هایی که low یا medium باشند بانتی به هکر نمیده و در بعضی از مواقع هم ممکنه آسیب پذیریه شمارو قبول نکنه (مثلا ممکنه ربط بدن به دیزاین اپلیکیشن و یا دلایل دیگه) که همین باعث میشه کار با مایکروسافت صبر و حوصلهی زیادی بخواد.
من بعد از اینکه بانتی نگرفتم، سعی کردم با یک دید دیگه اینبار به تارگت نگاه کنم. تارگت اجازه نمیداد که من با عوض کردن آیدی به اطلاعات یوزر دیگه برسم و همینطور تست کیسهای دیگه هم جواب نمیداد. پس باید دنبال یک راه دیگه باشم که بتونم به ایمیل یک یوزر بدون تعامل مستقیم یوزر دسترسی پیدا کنم چون در این حالت impact آسیب پذیریه من بالا میره و قطعا میتونم بانتی بگیرم . یک functionality از سیستم برام جالب بود که برای اشتراک گذاریه فرم استفاده میشد که اگه یوزری به نام A بخواد فرمی رو با یوزر B به اشتراک بذاره ، مراحل زیر باید انجام بشه:
- کاربر A یک فرم یا کوییز ایجاد میکنه و یک لینک از سمت سرور برای فرم کاربر ساخته میشه
- کاربر A لینک ساخته شده در مرحله ی قبل رو برای کاربر B ارسال کرده و کاربر B بعد از پر کردن فیلدها، فرم رو تایید میکنه
- هنگام تایید فرم توسط کاربر B، اطلاعات وارد شده برای کاربر A فرستاده میشه و کاربر A در بخش response اکانت خودش میتونه پاسخهای کاربر B رو مشاهده کنه
در مرحله ی سوم درخواست زیر به سمت سرور ارسال میشه :
درخواستی که در بالا میبینیم ، ساختاره زیر رو داره:
formapi/api/<ownerTenantID>/users/<ownerID>/forms(<formID>)/responses
تمام پارامترهای ownerTenantID ، ownerID ،formID مربوط به کاربری هست که فرمش رو با ما به اشتراک گذاشته پس نیازی به پیدا کردن هیچکدوم نیست و موقعی که فرم یوزر رو تایید میکنیم، در این درخواست به مقداره هر سه پارامتر دسترسی داریم.
در اینجا سناریویی که به سرم زد این بود من همه ی شناسه های لازم از کاربر مثل ownerID و … رو دارم. پس چرا نیام از select$ استفاده کنم و ایمیل یوزر رو هم از سرور بگیرم؟ برای اینکار از select$ استفاده کردم و درخواست زیر رو ارسال کردم :
اما بعد از ارسال درخواست بالا با ارور 404 روبرو شدم و دیدم سرور اجازه ی دسترسی به createdBy که ایمیل یوزر داخلشه در forms رو بهم نمیداد. بعد از این سعی کردم دنبال راه دیگهای باشه که بتونم به مقداره این فیلد دسترسی پیدا کنم و یک فکری به ذهنم رسید:
آیا entity type دیگه ای میتونه وجود داشته باشه که فیلد createdBy رو داشته باشه؟ و همینطور جنس entity key یکسانی مثل forms داشته باشه (یا همون formID)؟
سوالی که ممکنه مطرح بشه اینه چرا باید entity type دیگه ای که دنبالش میگردیم، entity key مشابه با forms داشته باشه یا به صورت سادهتر formID رو باید بهش بدیم؟ اگه entity type دیگهای که میخوایم createdBy رو داشته باشه ولی جای formID یک entity key دیگه داشته باشه، بازم نمیتونیم به هدف برسیم؟
فرض کنید entity type که مد نظره ما هست اسمش X باشه. X دارای فیلد createdBy هستش که هدف ما هم دسترسی به مقداره این فیلده چون ایمیل یوزر داخلش هست. از طرفی X یک entity key به نام accountID داره که یک رشتهی کاملا رندوم هست و آیدی اکانته آفیس یوزره و از هیچ راهی نمیتونیم accountID یوزره دیگه ای رو پیدا کنیم. برای اینکه ما به ایمیل یوزر دسترسی پیدا کنیم درخواست زیر رو باید ارسال کنیم:
formapi/api/<ownerTenantID>/users/<ownerID>/X(<accountID>)$select=createdBy
مشکل اصلیه ما در درخواست بالا مقداره accountID هستش چون نمیدونیم accountID یوزر چی هست ولی اگه به جای accountID ما مقداره formID رو داشته باشیم (که از طریق فرم یوزر دیدیم میتونیم به این مقدار دسترسی داشته باشیم) به راحتی میتونیم این درخواست رو ارسال کنیم.
من ابتدا سعی کردم دنبال یک entity type با شرایط گفته شده بگردم که فیلد createdBy رو داشته باشه. بعد از بررسی رابطه ی بین entity type های مختلف و درخواستهایی که از سمت کلاینت ارسال میشد، موفق شدم یک entity type به نام runtimeForms پیدا کنم که فیلد createdBy رو داشت و حتی entity key یکسانی مثل forms داشت! یعنی formID که تمام شرایطی که میخواستم رو داشت و نیازی نبود دنبال مقدار یا پارامتره خاصی بگردم .
تنها کاری که باید انجام میدادم این بود به جای forms، از runtimeForms استفاده کنم و بعد برای دسترسی به ایمیل از select$ استفاده کنم تا ببینم سرور ایمیل یوزر رو برام برمیگردونه یا نه. با ارسال درخواست زیر، موفق شدم بالاخره به ایمیل یوزر دسترسی پیدا کنم:
با این سناریو میتونستم به صورت مستقیم و بدون تعامل یوزر به ایمیلش دسترسی پیدا کنم. کافیه یوزر فرمش رو با من به اشتراک بگذاره یا حتی از طریق گوگل میشد فرم های کاربران زیادی رو دید که از این سرویس استفاده میکنند و بعد میتونستم به ایمیل هر یوزری دسترسی پیدا کنم. بعد از ارسال این آسیبپذیری به مایکروسافت، موفق شدم بانتی بگیرم :
در این رایت آپ سعی کردم تا حد امکان مفاهیم رو ساده توضیح بدم تا روند پیدا کردن این آسیبپذیری رو درک کنید و اینکه همیشه حالتهای مختلف یک سیستم رو بررسی کنید تا به نتایج جالبی برسید.
عالی بود
خیلی جالب بود آقا برنا تبریک میگم?
ممنونم لطف دارید
جالب بود ولی کاش ساده تر توضیح میدادید
ممنونم از نظرتون.سعی کردم تا حد امکان ساده توضیح بدم.مفاهیم اول این مقاله به همراه رفرنسی که معرفی کردم رو بخونید تا درک ادامه ی موضوع براتون راحت تر باشه
سلام جناب نعمت زاده ممنون از پست جالبتون یه سوال داشتم
چرا باید اکسپلویت موقع تایید فرم انجام بشه؟؟در یک درخواست جدا نمیتونیم اکسپلویت رو انجام بدیم یا شبیه idor تبدیلش کنیم؟
سلام. در رایت آپ هم توضیح دادم اگه دقت کنید.موقع تایید فرم باید اجرا بشه به این دلیل که ما دسترسی به شناسه های دیگه ی یوزر رو نداریم و حتی قابل حدس زدن هم نیستند.اگه این پارامترها قابل حدس زدن بودند یا حتی اگه الگوی خاصی داشتند،بازم به سختی میشد اکسپلویت رو اجرا کرد به این دلیل که شما باید یک جایگشتی از سه پارامتر رو پیدا کنید که دقیقا به یوزری که مد نظر شما هست مپ شه یا به صورت ساده تر اگه یوزر شما A باشه باید هر سه پارامتر مربوط به یوزر A باشه و اگه مقدار یک پارامتر متفاوت باشه بازم نمیشه اکسپلویت رو اجرا کرد