عماد عابدینی
عماد عابدینی
خواندن ۱۲ دقیقه·۲ سال پیش

آسیب‌پذیری اپلیکیشن یکی از استارت‌آپ‌ های موفق داخلی

(این آسیب‌پذیری مربوط میشه به حدود 3 سال پیش، همون موقع گزارش شد و بعد از 2 هفته به طور کامل برطرف شد)

اجازه انتشار اسم شرکت داده نشد. بنابراین تمام تصاویری که به عنوان اسکرین‌شات از اپلیکیشن در این گزارش قرار داده شده تزیینی هستن و برای این گزارش آماده شدن (اسکرین شات نیستن)

این آسیب‌پذیری باعث امکان دسترسی کامل به دیتابیس + امکان تغییر رکوردهای دیتابیس + چندین مورد دیگه شد که در ادامه روش کار رو توضیح دادم...

داستان از اونجایی شروع شد که داشتم تو وب سایت این شرکت یه موضوعی رو پیگیری می‌کردم که اتفاقی متوجه شدم تو یکی از صفحات زمانی که صفحه رفرش میشه یک button هست که سریع حذف میشه و دیگه امکان کلیک رو به کاربر نمیده. (وب سایت SPA بود و با React نوشته شده بود)

معمولا این حالت زمانی پیش میاد که Componentی که قراره بر اساس سطح دسترسی به کاربر نشون داده بشه قبل از دریافت و بررسی Response مرتبط با سطح دسترسی، Render میشه و بعد که Response اومد اگه سطح دسترسی لازم رو نداشت تو Render مجدد دیگه به کاربر نشون داده نمیشه.

بگذریم... پس اومدم request رو نگه داشتم که بتونم اون button رو بررسی کنم. ( یک راه ساده دیگه‌ش هم اینه که بیای از Network throttling سرعت رو بیاری پایین)

کاری که این button انجام میداد هدایت کاربر به صفحه‌ای بود که فقط کاربرایی با سطح دسترسی مدیر باید بهش دسترسی داشته باشن (نه کاربرای معمولی)

البته بیشتر component های اون صفحه لود نمیشد ولی لینک دانلود یک فایل APK داخل فوتر اون صفحه بود. (این شرکت خودش یک اپلیکیشن داره که به صورت عمومی از مارکت‎‌ها قابل دانلوده ولی این اپلیکیشن اون اپلیکیشن نبود)

از سر کنجکاوی دانلودش کردم ببینم چیه، بعد از نصب و اجرا کردنش همون اول کار شماره موبایل میخواست، شمارمو وارد کردم ولی گفت شماره وجود ندارد و گزینه‌ای هم برای ثبت نام نداشت، پس میشد احتمال داد که شماره‌هایی مجاز به استفاده از این اپلیکیشن هستن که از قبل رجیستر شده باشن، این موضوع باعث شد بیشتر کنجکاو شم در موردش...

اول از همه اومدم کل String هایی که داشت رو بررسی کردم تا شاید اینجوری بتونم متوجه بشم این اپلیکیشن قراره چه کاری انجام بده.

برای استخراج String ها ابزار خیلی زیاد هست، ولی سریع ترین روش به نظرم اینه:

aapt d --values strings [APK_FILE]

بعد از بررسی Stringها دیگه مطمئن شدم این اپلیکیشن فقط مختص کاربر با سطح دسترسی مدیره، نه کاربرای معمولی. پس میشد احتمال داد که بشه کارای زیادی باهاش انجام داد...

خلاصه که بعد از دیدن Stringهایی که داشت دیدم ارزشش رو داره سرش وقت بذارم...

اولین روشی که به نظرم رسید این بود که ترافیک اینترنتش رو بررسی کنم تا شاید بشه با یکم تغییر روی response، لاگین رو دور زد (خیلی وقتا همین روش ساده هم جواب میده)، رفتم سراغ گرفتن ترافیکش...

از SSL Pinning استفاده کرده بود:

با این اسکریپت راحت Bypass شد:

https://codeshare.frida.re/@pcipolloni/unsal-android-ssl-pinning-bypass-with-frida

مشکل بعدی این بود که ترافیک به صورت رمز شده ارسال/دریافت میشد: (ترافیک اون یکی اپلیکیشن این شرکت که به صورت عمومی از مارکت قابل دانلود بود رو هم چک کردم، SSL Pinning داشت ولی ترافیک Encrypt نمیشد)

البته Encrypt کردن ترافیک یه چیز عادیه و خیلی از اپلیکیشن و بازی‌ها از این روش استفاده میکنن. (منظور اینکه Encrypt کردن ترافیک کار خیلی خاصی نیست)

اینجور وقتا یه روش اینه که نوع الگوریتم رو در بیاری که بتونی ترافیک رو Encrypt/Decrypt کنی، اومدم فایلشو Decompile کردم و توی سورس کدش دنبال همچین چیزایی گشتم:

Encrypt , Decrypt , AES, RSA, Encode, Decode &...

مشکلی که وجود داشت سورس کدش خیلی زیاد بود و کلی زمان میگرفت بخوام نتایجی که پیدا شده رو بررسی کنم:


روش بعدی که به نظرم رسید این بود که به جای لاگین، بخش بعدیش که احتمالا میشد وارد کردن OTP رو با adb بیارم بالا، یه نگاهی به Manifest انداختم ولی هیچ activity ای که اسمش به وارد کردن OTP بخوره پیدا نکردم... (البته اگه با Jetpack Compose نوشته میشد این روش کمکی نمیکرد)

پس دوباره برگشتم سر activity مرتبط با لاگین (اسمش دقیقا Login بود)، سورسش رو چک کردم، به نظر می‌رسید وارد کردن OTP هم توی همین activity لاگین انجام میشه (جفتشون توی یک layout بودن):


فایل layout مربوط به بخش لاگین رو چک کردم و دیدم بله، جفتشون رو توی یک layout در نظر گرفتن:


پس دیگه نمیشد با adb بیارمش بالا (چون با login توی یک activity بودن و برای اینکه از وارد کردن شماره به وارد کردن OTP تغییر کنه نیاز بود response ای که از سرور میاد بگه شماره وجود داره)

ولی بازم یه راه ساده دیگه وجود داشت، اینکه به جای بالا اوردن بخش وارد کردن OTP، مستقیم بخش اصلی رو بیارم بالا...

دوباره Manifest رو چک کردم دیدم یه activity داریم به اسم main و خب بدون بررسی سورس کد هم انتظار میرفت صفحه اصلی‌ای که قراره کاربر بعد از وارد کردن OTP واردش بشه همین main باشه...

اومدم main رو با adb بیارم بالا ولی crash کرد، ساده ترین دلیل که احتمالشم زیاد بود این بود که موقعی که OTP به صورت صحیح وارد میشه، یکسری مقادیر از response دریافت و به main ارسال میشن و چون الان بدون ارسال اون مقادیر میخواستم بیارمش بالا باعث crash شده، پس نیاز بود دوباره یه نگاهی به سورس کد بندازم:

همونطور که احتمالش رو میدادم، main نیاز به یکسری مقادیر داشت که چون اینارو نداشته باعث crash شدنش شده...

در واقع main میاد 3 تا مقدار رو از sharedPreference می گیره، پس احتمالا موقعی که OTP صحیح وارد میشه، اینارو از response ای که میاد میگیره و میریزه تو sharedPreference

باز برای اطمینان گشتم دنبال این بخش توی سورس کد، که میشد اینجا:

حالا میشد بیام همین مقادیری که نیاز داره رو دستی توی sharedPreferense اضافه کنم و یا موقعی که دارم main رو با adb میارم بالا، اینارم بهش بدم...

ولی فقط نوعشون (Typeشون) مشخص بود، اینکه طولشون چقدره و با چه فرمتی هستن مشخص نبود...هر دو روش رو با حالت های مختلفی که به نظرم رسید تست کردم ولی نشد و بازم crash میکرد...

دو تا راه داشتم، یا باید میومدم الگوریتمی که برای Encrypt/Decrypt ترافیک ارسالی/دریافتی استفاده کرده رو در بیارم تا بتونم Encrypt/Decryptش کنم، یا اینکه بیام سمت سرور رو چک کنم ببینم میشه یه شماره رو رجیستر کنم یا حداقل یه شماره رجیستر شده پیدا کنم یا نه...

یه نگاهی به APIش انداختم، خیلی راحت میشد StackTrace گرفت ازش: (بهش Improper Error Handling میگن)

سمت سرور با Java بود و ازونجایی که اون زمان Log4Shell تازه Public شده بود (فکر کنم هنوز یک هفته هم نشده بود)، این احتمال وجود داشت که نسبت بهش آسیب پذیر باشه، از طرفی هم میشد به آپدیت نبودن Weblogic هم امیدوار بود...

ولی ازونجایی که سرور فقط برای سرویس دهی به IP های داخلی (ایران) محدود شده بود اگه از هر کدوم از روش‌هایی که در دسترس داشتم استفاده می کردم بازم ریسک داشت (این شرکت اون زمان برنامه‌ای برای بانتی نداشت، در واقع اجازه اینکه بخوای روش تست امنیت بزنی رو نداشتی)، از طرفی ممکن بود WAF متوجه بشه و policy ها رو بیشتر کنن و خب اینجوری سمت اپلیکیشن هم کار سخت میشد، دیگه خلاصه دیدم فعلا وقت سمت سرور نیست و به همین خاطر دوباره برگشتم سر اپلیکیشن...

راه بعدی ای که به نظرم رسید این بود که یه OSINT روی کارکنان اون شرکت بزنم ببینم میتونم شماره موبایلی ازشون پیدا کنم یا نه، کلا 3 تا شماره پیدا کردم ولی بازم هر 3 تا رو میگفت این شماره وجود نداره...!

احتمالی که وجود داشت این بود که فقط شماره رجیستر نمیشه، شاید شماره با IMEI کنار هم رجیستر میشن یا شایدم واقعا اون 3 شماره رجیستر نشده بودن، خلاصه اینکه این روش هم جواب نداد...

دیگه وقتش بود بیام الگوریتم رمزنگاریش رو پیدا کنم،  یا باید میومدم سورس رو چک میکردم که این خیلی وقت گیر بود، یا اینکه سعی میکردم با frida و hook کردن، ترافیک رو Decrypt کنم، در این خصوص مطلب و آموزش خوب خیلی زیاد هست، مثلا میتونین یه نگاهی به این بندازین:

https://11x256.github.io/Frida-hooking-android-part-5

نتیجه کار به این صورت شد:

درخواست ارسالی:


درخواست ارسالی به صورت Encrypt شده:

پاسخ دریافتی بعد از Decrypt :

الگوریتم و کلید استفاده شده:

خب حالا که الگوریتم مشخص شده بود، باید یه اسکریپت می نوشتم که بتونم باهاش Encrypt/Decrypt کنم...

دو تا راه داشتم، یا اینکه یه Extender برای Burpsuite بنویسم و یا اینکه یه اسکریپت جدا برای خودم بنویسم...

من معمولا عادت دارم موقع گزارش آسیب پذیری، جدا از فایل گزارش (متن، تصویر و ویدیو)، اگه امکانش باشه برای اون آسیب پذیری یه برنامه/اسکریپت ساده هم می نویسم که خود اون نفر/شرکت/سازمان بتونه با یه کلیک به همون نتیجه ای که رسیدم برسه...، شاید به نظرتون وقت گذاشتن الکی باشه ولی به نظرم باعث میشه هم گزارش قشنگ‌تر در بیاد و هم اینکه همیشه بازخورد خوبی داشته برام این کار...

خلاصه که به همین خاطر تصمیم گرفتم به جای Extender ، اسکریپت خودم رو بنویسم که نخوام دو بار وقت بذارم. (برای نوشتنش میشد از Crypto.js استفاده کرد و کار وقت گیری نبود اصلا)

https://github.com/brix/crypto-js

ولی یکم که سرچ کردم دیدم از قبل Encryptor/Decryptor این الگوریتم نوشته شده (یه Gist بود)

البته یکسری بخش ها از کدش رو باید حذف میکردی تا درست کار کنه!!

تا اینجای کار همه چیز خوب پیش رفته بود و میشد خیلی راحت دیتای ارسالی/دریافتی رو Encrypt/Decrypt کرد، ولی بازم یه مشکلی وجود داشت، بعضی response ها رو نمیتونست Decrypt کنه!

بعد از بررسی سورس کد (بخش Encrypt دیتای ارسالی توی اپلیکیشن) متوجه شدم که بعد از Encrypt به صورت random به آخر بعضی از رشته ها میاد یه \r\n اضافه میکنه و موقع Decrypt هم اگه آخرش اینو داشت اول حذفش میکنه!! همین کارو توی اسکریپت بالا انجام دادم و مشکل حل شد و دیگه تموم response ها Decrypt میشدن...

خب حالا باید میرفتم با یه شماره لاگین کنم ببینم چه responseی میاد تا شاید بشه با تغییر response به مرحله وارد کردن OTP رسید ( نمیشد مستقیم برم صفحه اصلی، چون همونطور که توضیح دادم main به responseی که بعد از صحیح وارد کردن OTP میومد نیاز داشت)، بعد از وارد کردن شماره، responseی که میومد به این صورت بود:

میشد احتمال داد که برای شماره های رجیستر شده resultCode مقدارش فرق میکنه، چون بعید بود بیان برای این موضوع از msg استفاده کرده باشن...

پس باید میرفتم ببینم resultCode اگه چند باشه میره صفحه بعد، باز یه نگاهی به سورس انداختم:

همونطور که مشخصه اگه برابر با 1 باشه میره روی حالت اینکه OTP رو وارد کن، پس اومدم 200 رو به 1 تغییر دادم و بخش وارد کردن OTP اومد بالا:

یه مقدار تستی برای OTP وارد کردم، responseی که میداد این بود:

دوباره نیاز بود سورس رو بررسی کنم، چون همونطور که بالاتر سورس کد رو دیدیم بعد از وارد کردن OTP صحیح، یکسری مقادیر از response دریافت و توی sharedPreference ریخته میشه و main میاد ازون مقادیر استفاده میکنه...

نتیجه ای که بعد از بررسی سورس کد این بخش گرفتم این بود که اگه OTP صحیح باشه یه object به اسم response هم بهش اضافه میشه که همون 3 مقداری که قراره توی sharedPreference ریخته بشن رو از این قسمت برمیداره...


پس اومدم resultCode رو دوباره 1 کردم و به response یه object به اسم response که اون 3 مقدار رو هم داخلش داشت اضافه کردم، ازونجایی که نمیدونستم طول و فرمت اون 3 مقدار به چه صورتیه باید حالت های مختلف رو تست میکردم، برعکس دفعه قبلی که نشد ولی این بار بعد از چندین بار تست کردن حالت های مختلف بالاخره صفحه اصلی اپلیکیشن اومد بالا...

بازم یه مشکلی بود، روی هر بخشی کلیک میکردی crash میکرد بدون اینکه هیچ data ای ارسال/دریافت بشه!

دوباره سورس رو بررسی کردم، مشکلی که وجود داشت این بود که بعد از اولین کلیک توی main بازم یکسری مقادیر توی sharedPreference ریخته میشدن، 2 تا ازین مقادیر با اون 3تایی که خودم بهش داده بودم ارتباط داشت و این احتمال وجود داشت که طول و یا فرمتی که برای اون 3 مقدار در نظر گرفته بودم درست نبوده و این وسط یه مشکلی پیش اومده...

بعد از 5-6 بار تغییر دستی sharedPreference بالاخره مشکل حل شد

تو این مرحله دیگه میتونستم راحت وارد بخش اصلی اپلیکیشن بشم. تو صفحه اصلی میشد تغییرات نسبتا زیادی رو روی رکوردهای هر کاربر ایجاد کرد.

بعد از انجام چند تا تست به این نتیجه رسیدم که خب WAF که به خاطر Encrypt شدن دیتای ارسالی/دریافتی کلا چیزی رو نمی گیره و مهم تر اینکه ورودی ها کلا Sanitize نمیشدن! (احتمالا خیالشون از رمزنگاریشون راحت بوده دیگه حوصله Sanitize نداشتن)  و از طرفی هم هیچ محدودیتی روی تعداد request ارسالی وجود نداشت...!!

پس همه چیز آماده بود برای گرفتن کل دیتابیس (که البته فقط 200 تا رکورد گرفتم، هدف گرفتن کلش نبود)

برای گرفتن شماره های از قبل رجیستر شده هم یه اسکریپت پایتونی نوشتم که هم میشد بهش لیست شماره بدی و یا اینکه بگی random شماره تست کنه و یا یه بخشی از شماره رو بهش بدی و یکی یکی اضافه کنه بره جلو...

یه delay هم براش در نظر گرفتم که اگه یه موقع نیاز شد ازش استفاده کنم (که نیاز نشد)

نتیجه کار:

خب این شد از لیست کل شماره های رجیستر شده... (که البته اینم فقط 20 تاشو گرفتم)

اینجا به نظرم رسید یه اسکریپت بنویسم و کل ورودی ها رو بررسی کنم...

نتیجه کار شد یه اسکریپت پایتونی که یه لیست payload بهش میدادی و روی کل ورودی ها تست میکرد...

(برای payload من معمولا از PayloadsAllTheThings استفاده میکنم + payloadهایی که از اینور اونور جمع میکنم)

https://github.com/swisskyrepo/PayloadsAllTheThings

چند تا آسیب پذیری‌ دیگه هم داشت که برای اینا دیگه اسکریپت ننوشتم و فقط توی ویدیو ای که همراه با گزارش آسیب‌پذیری براشون ارسال کردم اینا رو هم توضیح دادم: (هر جور خواستم ویدیوش رو Edit کنم که بشه داخل این گزارش هم اوردش نشد و مشخص میشد کدوم شرکته)

  • یکی از Route ها Mass Assignment داشت، میشد پارامترهایی که حتی مدیر هم اجازه تغییرشون رو نداره رو تغییر داد.
  • اونجایی که میخواست OTP رو ارسال کنه، میشد بخشی از متن sms رو تغییر داد. (اینم خیلی ساده بود، 11 کاراکتر مرتبط با SMS Retriever API داخل Request بود و اگه تغییرش میدادی با تغییر ارسال میشد.)
  • هیچ محدودیتی روی تعداد sms ی که قرار بود OTP رو ارسال کنه وجود نداشت! (اسکرین شات زیر مربوط به Turbo Intruder، یکی از Extender های BurpSuiteه)


اینجا دیدم سمت اپلیکیشن دیگه کافیه و بهتره یکمم سر وب سرور کار کنم، چون از طرفی از سرور Log4Shell داشتم و همین تا حد خوبی کافی بود برای شروع کار...

سر فرصت writeup آسیب‌پذیری های سمت سرورش رو هم تو یه پست دیگه آماده میکنم.

ممنون که وقت گذاشتین این گزارش رو خوندین و امیدوارم که براتون جالب بوده باشه...





آسیب پذیریامنیتاندرویدمهندسی معکوسبرنامه نویسی
Security Researcher | Full Stack Developer
شاید از این پست‌ها خوشتان بیاید