خلاصه!
در گام اول این نگاشته خواهیم دید که چطور بزرگترین سایت فروشگاهی کشور هم میتواند نسبت به SQL Injection آسیبپذیر باشد و در گام دوم تکنیک و روشهایی که برای اکسپلویت کردن آسیبپذیری مورد استفاده قرار دادیم را ارائه میدهیم.
داستان از کجا شروع شد؟
داستان از جایی شروع میشود که دیجیکلا برای روز چهارشنبه سوری آخر سال 97 و با هدف جذب مخاطب بیشتر، اقدام به برگزاری یک بازی آنلاین میکند.
جوایزی که به برندگان این بازی اختصاص مییابند در تصویر زیر قابل مشاهدهاند و برندگان بازی هم کسانی هستند که بتوانند بیشترین امتیاز را کسب کنند.
و اما بازی چیست؟
بازی دقیقا مشابه دایناسورِ مرورگر کروم است، با این تفاوت که به جای تیرکس از یک کاراکتر موتور-سوار استفاده شده است 🙂
کمی فنیتر
به پیشنهاد یکی از دوستان به بررسی چند و چونِ عملکرد این بازی کنجکاو شدم و پس از یک دور بازی کردن که اتفاقا امتیاز پایینی هم کسب کردم، متوجه قسمتی در صفحه شدم که بیشترین امتیاز کسب شده تا کنون را نمایش میداد.
طبیعتا اولین سوالی که برایم مطرح شد این بود که این امتیاز چطور به سرور ارسال و آنجا ذخیره میشود؟
به کمک نرمافزار Burp Suite به بررسی بستههای تبادلی پرداختم و اقدام به بازی مجدد کردم.
همانطور که تصویر فوق مشخص است، 2 پارامتر جالب با نامهای newscore و user_id در حال ارسال به سمت سرور هستند و در جواب آنها، یک مقدار True بازگردانده میشود.
با بررسی مختصر این پارامترها مشخص شد که:
- پارامتر user_id: نقش شناسهی کاربر را بر عهده دارد و بنابراین میتوانستم با تغییر مقدار آن امتیاز سایر کاربران را تغییر دهم (آسیبپذیری IDOR)
- پارامتر newscore: امتیاز پایان هر دوره بازی کردن کاربر را به صورت Base64 ارسال میکند و که از علامت تساوی (=) که در تصویر فوق Url Encode شده به مقدار 3D% است استفاده میکند.
(به منظور Decode میتوانید از Burp Suite Decoder یا از سرویسهای آنلاین استفاده کنید)
تا به اینجا ما یک آسیبپذیری پیدا کرده بودیم و آن هم تغییر امتیاز خود یا دیگر کاربران بود. اما آیا این تنها آسیبپذیری بود؟ خیر!
برداشتی که از کد سمت سرور برای آپدیت کردن امتیاز کاربر در ذهنم داشتم، چنین بود:
با این برداشت، شروع به آزمودن سرور برای SQL Injection کردم؛ اما مشکلی وجود داشت. کدی که سمت سرور نوشته شده بود تنها یکی از دو پاسخ true یا null را بازمیگرداند.
اینجا بود که تلاش را برای پیدا کردن آسیبپذیری Blind Sql Injection یا تزریق پایگاه داده کورکورانه ادامه دادم.
(به جهت حفظ محرمانگی، اطلاعاتی از پایگاهدادهی دیجیکالا نمیتوانم ارائه دهم. بنابراین از اینجا به بعد با فرض این که پایگاهدادهی ما MySql است پیش میرویم)
در مرحلهی بعد، برای بررسی آسیبپذیر بودن یا نبودن سرور، از تکنیک Time Based استفاده کردم. در این تکنیک با استفاده از دستورهای پایگاه داده ای مانند تابع ()Sleep میتوان تشخیص داد که کوئری ارسال شده، سمت سرور اجرا میشود یا خیر. لیستی از این تکنیک ها در گیت هاب یاشار ، قرار دارند.
مشکل بعدی؟
فرض کنیم که این آسیبپذیری وجود دارد. آیا اجرای دستور sleep روی بزرگترین وبسایت فروشگاهی کشور کار درستیست؟ مسلما خیر!
پس چه کنیم؟ مسئلهی دشواری شد! البته حتی اگر از روش sleep هم پیش میرفتیم باز چارهی گرهگشایی نبود! بیرون کشیدن اطلاعات از پایگاهداده با تکنیکهای فوق حقیقتا زمانبر بود.
خب، یکبار دیگر با هم مرور کنیم:
- آسیبپذیری ما از نوع Blind است و سرور تنها یک جواب True یا Null برمیگرداند.
- از تکنیک Time Based به جهت فضای مسئله نمیتوانیم استفاده کنیم.
برای حل این دو مشکل میتوانیم از خودِ پارامتر امتیاز برای دریافت اطلاعات استفاده کنیم! اما منظورم چیست؟
منظور این است که به جای ارسال امتیاز به سمت سرور، یعنی مثلا به جای newscore = 1000، یک دستور به پایگاهداده ارسال کنیم و سپس خروجی آن دستور را بریم داخل پروفایل خود، که “بالاترین امتیاز” را نمایش میدهد مشاهده کنیم!
از چه دستوری استفاده کنیم؟
newscore=user()
تابع ()user در پایگاهدادهی MySql کاربر فعلی دیتابیس را بازمیگرداند. بنابراین تابع مذکور را base64encode میکنیم و ارسال میکنیم.
پاسخی که دریافت کردیم:
چه اتفاقی افتاد؟ مقادیر که درست بودند، پس چرا پاسخ True دریافت نکردیم؟
این مشکلات زمانی اتفاق میافته که برنامهنویس عزیز، نوع ذخیرهسازی داده برای ستون newScore داخل دیتابیس را از نوع Integer تنظیم کرده باشد و به همین سبب اگر شما بخواهید یک رشته (مانند رشتهی “کاربر فعلی” دیتابیس) را در آن سلول ذخیره کنید، با خطا مواجه میشوید.
اگر خواستید همین پایگاهداده را خودتان هم برای تست پیاده سازی کنید، میتوانید پست Race Condition را مطالعه کنید و ببینید چطور میشود mysql را آماده و حملهی جذاب Race Condition را شبیه سازی کرد.
مشکل محدودیت فوق را چطور حل کنیم؟
از اونجایی که ما فقط میتونیم عدد را داخل سلول امتیاز قرار بدهیم، چرا نیاییم و اطلاعاتی که میخواهیم (نام کاربر دیتابیس) را تبدیل به عدد کنیم؟
برای این منظور میتوان از تابع ()ascii داخل MySql که کد عددی یه کاراکتر (یه حرف از کلمه) را بازمیگرداند استفاده کرد. در اینجا چون ما اینجا یک کلمه مثل root@localhost داریم، باید از تابع دیگری به همراهش استفاده کنیم که ()substring نام دارد. این تابع 2 تا ورودی دریافت میکند و برای اینکه از یک مبدا تا یک مقصد از رشتهای که دریافت کردهاست را استخراج کند، میتوانیم بگوییم حرف به حرف خروجی دستور ()user را تبدیل به کد ()ascii کند و سپس خود را به آن مقادیر آپدیت کنیم. در نهایت هم هر بار برای هر کلمه آپدیت شد باید امتیاز خود را از کد ascii به “حرف(text)” تبدیل کنیم و با کنار قرار دادن خروجی، پاسخ تابع دلخواهمان رو مشاهده کنیم.
در اسکریپت پایتون زیر، این مراحل را به صورت اتوماتیک انجام دادیم.
داخل قسمت payload هر چیزی میتواند باشد و بستگی به دیتابیس دارد. مثلا waitfor و باید مقدار delay رو هم مشخص کنید. waitfor delay ’00:00:10′
نتیجه
تا جایی که میتوانستم سعی کردم مطالب را ساده بنویسم تا انتقال مطلب به راحتی انجام شود. امیدوارم این پست، بار آموزشی خوبی برای شما داشته باشد و در نهایت فراموش نکنید که همیشه باید بیرون از جعبه فکر کنید 🙂
با وجود اینکه دیجیکالا هنوز پلن رسمی برای باگ بانتی ندارد، ولی bug-report راهکاری برای گزارش دادن باگ هاست.
موفق و پیروز باشید.
اینجا از تجربیات و باگ های که پیدا میکنم براتون مینویسم.
سلام. مقاله تون چرا مستندات فنی قابل اعتماد نداره؟صرفا چندتا عکس و تکه کد قرار دادین. اینا که چیزی رو ثابت نمیکنه.یه دمو میذاشتین بهتر بود(البته جاهای کلیدی رو تو دمو روشون خط میکشیدید).
سلام تقی جان. دوستی که مقاله رو نوشته خودش میاد جواب میده. فکر کنم دسترسی به نت نداره الان. من معمولا هر روز کامنت ها رو چک میکنم، ولی علت این که کامنت شما تا الان تایید نشده بود (و یه سری کامنت دیگه) این بود که منتظر میمونم نویسندهی هر پست، خودش بیاد تایید کنه جواب بده و این ممکنه یکم بیشتر طول بکشه 🙂 منتهی چون سوء تفاهم پیش اومده بود من از دید خودم الان جواب میدم. در مورد کامنت ها، به جز فحش و سیاسی و تبلیغ و اسپم، تقریبا همه چی تایید میشه. اما جواب کامنت شما: راستش توی مموری لیکز قرار نیست کسی بخواد با پستها چیزی رو به کسی ثابت کنه. هدف اول و آخر یاد دادن هست. حالا نکتههایی که توی پستها هست برای بعضی بدیهی و مقدماتی به حساب میاد و برای بعضی متوسط و برای بعضی سخت. شاید بودن دمو بهتر میبود، ولی نبودش هم برای بخش آموزشی مقاله، مشکلی ایجاد نمیکنه به نظرم. درسته؟
مطالب به اسانی قابل درک هس ولی من منظورم این بود اگه یه مستند واضح تر میبود بهتر میشد. مثلا کنارش یه دمو داشت. چون این اطلاعات که تو متن مقاله هس به راحتی میشه جعل کرد و گذاشت. اینطور نیس؟
به جهت این که بدون دمو میشه راحت مطلب دروغ منتشر کرد، باهات موافقم؛ ولی خب به نظرت اگه دروغ منتشر کنیم، دیجیکالا نمیآد به جهت خدشهدار کردن اعتبار امنیت برندش از ما شکایت کنه؟ از این گذشته، ساخت دموی فیک هم کار دشواری نیست، کافیه یه وب سرور بیاری بالا و یه پنل شبیه دیجیکالا درست کنیم و توی hosts دامنهی دیجیکالا رو به آیپی لوکال هوست مپ کنیم. یعنی حتی دمو هم نمیتونه راست بودن پست رو تضمین کنه. به خاطر همین و همچنین به خاطر این که تنها هدفمون آموزش هست و نه اثبات هک کردن، جاهایی که نویسندهها حال دمو نداشتند سختگیری نکردیم و به هم دیگه اعتماد داریم. ایشالا که کسی پست دروغی نگذاره.
به نظر من اصلا ثابت کردن مهم نیست . حتی جایی که آسیب پذیری هم داشته مهم نیست! (هر چند جالبه!) در کل مهم نگرش هست! اینکه از IDOR به SQLi رسیدن!!! این مدل رو شاید بشه توی خیلی از پروژه های تست نفوذ هم پیاده سازی کرد! بسیار بسیار عالی بود!!!
لذت بردم، عالی بود 🙂
بسیار جالب بود. سپاس
به نظر دیجیکالا باید این کار را برای جلوگیری انجام دهد:
۱- با کوکیها کاربر را چک کند.
۲- مقادیر ارسالی را اعتبارسنجی کند.
۳- در صورتی که کوکی با مقادیر ارسالی یکسان بود و مقدار امتیاز نیز به درستی (از سمت اپلیکیشن) ارسال شده بود، بهروزرسانی کند.
۴- ارسال شدن از سمت اپلیکیشن به روش و تکنیکی باید شناسایی شود و تنها یه Get یا Post ساده نباشد.
اگر مراحل بالا درست است، چه تکنیکی برای مورد چهارم بهتر است؟
شما رو ارجاع میدم به ۲ پست:
https://gamedev.stackexchange.com/questions/113281/prevent-cheating-in-html-javascript-game
https://stackoverflow.com/questions/8670612/what-are-the-best-practices-for-making-online-high-score-lists-in-javascript-bas
متشکرم اما در شرایط خارج از بحث بازی، به صورت کلی چیزی که عرض کردم درست است؟
سینا عالی بود من بهت افتخار میکنم
بسیار عالی و کامل
اونجا که گفته اسلیپ روی دیجیکالا صدا بزنیم کار درستی نیست، چرا؟ اگه مثلا دو ثانیه دیتابیس سایتو بخوابونیم میفهمن ؟
نمیفهمم چه چیر زدن دستورsleep غلطه?
دستور sleep بر روی کاربر تاخیر میندازه اصلا سمت سرور تاخیری نمیاندازه یکم توی نوشتن مطلبتون دقت کنید❤