در مقاله قبلی به بررسی نسبتا کامل حالت اول Blind SQL Injection یعنی Time based SQL Injection پرداختیم و در آن توانستیم نام کاربری کاربر دلخواهمان را به دست آوریم و همچنین گفتیم که استخراج سایر اطلاعات نظیر رمز عبور کاربران هم دقیقا مسیری مشابه حالت بررسی شده را دارد.
حال در این مقاله می خواهیم به بررسی حالت دوم Blind SQL Injection یعنی Boolean Based SQL Injection بپردازیم.
شیوه پاسخ سایت در حالت Boolean based SQL Injection
نکته بسیار مهمی که در 2 مقاله قبل روی آن تأکید داشتیم این بود که تفاوت اصلی 2 روش Boolean based و Time based در نحوه پاسخ دادن سایت به سوالاتی است که ما، مانند بازی بیست سوالی در قالب Query به سایت تزریق می کنیم. سایت مورد نظر هم اصولا در روش Blind یک جواب Yes/No به ما می دهد که خروجی این جواب در 2 قالب متفاوت ارائه می شود؛ در Time based از تأخیر ایجاد شده متوجه جواب Yes از طرف سایت می شویم و در روش Boolean based که الان به بررسی آن می پردازیم، جواب Yes به صورت پاسخی غیرمستقیم در خروجی سایت ظاهر می شود.
سایت زیر را در نظر بگیرید:
همانطور که مشاهده می کنید سایت بالا تنها یک ورودی به نام User ID از کاربر می گیرد؛ برای مشخص شدن خروجی سایت انواع ورودی ها را وارد می کنیم تا مشاهده کنیم دقیقا چند نوع پاسخ از سایت دریافت می شود.
برای مثال از عدد 1 شروع می کنیم و مشاهده می کنیم که با وارد کردن آن خروجی زیر حاصل می شود:
مقدار 1 را افزایش داده و مشاهده می کنیم که برای مقادیر 2و3و4و5 هم دقیقا همین خروجی ظاهر می شود:
اما با وارد کردن عدد 6 مشاهده می کنیم که خروجی سایت همان مقدار قبلی نیست و به جای آن پیام زیر مشاهده می شود:
نکته مهم اینجاست که همانطور که در مقالات قبلی هم گفتیم خروجی سایت تنها محدود به 2 حالت فوق هست و به ازای تمام مقادیر دیگر از قبیل رشته های بزرگ که شامل هر کاراکتری هم می شوند، تنها 2 خروجی بالا ظاهر می شود. برای مثال با وارد کردن رشته jka098lkjasdplkm
خروجی زیر حاصل می شود:
از تحلیل مثال های بالا متوجه می شویم که خروجی سایت شبیه Yes/No هست و با استفاده از همین خروجی اطلاعاتی که می خواهیم را به دست می آوریم.
با این حساب اگر Query زیر را وارد کنیم، نتیجه موجود در تصویر حاصل می شود:
1' and true#
و اگر Query زیر را تزریق کنیم نتیجه مانند تصویر بعدی خواهد شد:
1' and false#
از تحلیل 2 خروجی بالا متوجه می شویم که سوالمان را باید در Query بعد از and بپرسیم و اگر جوابش Yes باشد، خروجی اول حاصل می شود و اگر No باشد، خروجی دوم حاصل می شود.
برای بررسی دقیق تر مقدمات Blind SQL Injection و نحوه تشخیص این آسیب پذیری، به 2 مقاله قبلی از همین دوره مراجعه بفرمایید.
2روش کلی برای استخراج آسیب پذیری Boolean based
برای استخراج آسیب پذیری Boolean based، 2 روش کلی داریم که در هر کدام از Query ها و توابع متفاوتی برای استخراج اطلاعات استفاده می کنیم که در این مقاله به بررسی روش اول یعنی استفاده از توابع Ascii و Substring می پردازیم:
استفاده از توابع Substring و Ascii
در این روش از 2 تابع کاربردی Substring و Ascii استفاده می کنیم.
این 2 تابع مفاهیم بسیار ساده ای دارند.
تابع Substring به شکل زیر 3 ورودی(رشته اصلی، شماره کاراکتر شروع، طول زیررشته) می گیرد و در حقیقت زیر رشته ای از یک رشته بزرگ تر را بر می گرداند:
SUBSTRING(string, start, length)
برای مثال برای به دست آوردن 5 کاراکتر اول رشته SokanAcademy تابع را به شکل زیر مقداردهی می کنیم:
SUBSTRING(‘SokanAcademy’, 1, 5)
و مقدار Sokan در خروجی ظاهر می شود.
یا برای به دست آوردن کارکتر ششم تا دوازدهم آن تابع را به شکل زیر مقداردهی می کنیم:
SUBSTRING(‘SokanAcademy’, 6, 7)
و مقدار Academy در خروجی ظاهر می شود.
تابع بعدی تابع Ascii هست که در آن کد ASCII یک کاراکتر برگردانده می شود.
اگر به طور خلاصه بخواهیم بگوییم هر کاراکتر انگلیسی از قبیل حروف لاتین یا اعداد و انواع دیگر کاراکتر ها یک کد معادل دارند که به آن کد ASCII می گویند و مقدار آن مطابق جدول زیر تعیین می شود:
مثلا کد ASCII حرف a برابر 97 و کد ASCII حرف B برابر66 هست.
در روش اول از ترکیب این 2 تابع استفاده می کنیم؛ یعنی سوالاتمان از پایگاه داده را با استفاده از این 2 تابع می پرسیم و پاسخ آن را هم در خروجی سایت مشاهده می کنیم.
از اینجای کار به بعد را در قالب مثال پیش می رویم:
می خواهیم نام دومین کاربر ثبت شده در دیتابیس (ردیف دوم جدول کاربران) را به دست آوریم.
ابتدا باید موارد زیر را به دست آوریم و سپس سراغ استخراج نام کاربری کاربران سایت برویم:
- استخراج نام دیتابیس
- به دست آوردن تعداد جداول دیتابیس
- به دست آوردن نام جداول دیتابیس
- به دست آوردن نام فیلد های جدول مورد نظر(جدول مربوط به کاربران)
استخراج نام دیتابیس
طبق ترتیب بالا ابتدا باید نام دیتابیس را استخراج کنیم.
برای این کار ابتدا باید تعداد کاراکتر های آن را به دست آوریم؛ سپس تک تک حروف آن را به دست آوریم و نهایتا نام آن را حدس بزنیم.
برای به دست آوردن تعداد حروف دیتابیس Query زیر را تزریق می کنیم:
1' and (length(database())=3)#
و مشاهده می کنیم که خروجی بالا حاصل شد؛ همانطور که گفتیم این خروجی معادل پاسخ No به Query ماست و مشخص می کند که Query ما از نظر منطقی صحیح نیست؛ بنابراین نتیجه می گیریم که تعداد حروف نام دیتابیس 3 حرف نیست.
در ادامه مقدار 4 را برای طول نام دیتابیس امتحان می کنیم.
برای این کار Query زیر را تزریق می کنیم:
1' and (length(database())=4)#
با زدن کلید submit مشاهده می کنیم که خروجی بالا نمایش داده می شود؛ به این معنی که Query ما از نظر منطقی صحیح هست. بنابراین نتیجه می گیریم که نام دیتابیس سایت شامل 4 حرف است.
در مرحله بعد سراغ شناسایی تک تک حروف و ترتیب قرار گرفتن آن ها در نام دیتابیس می رویم که نهایتا بتوانیم نام دقیق دیتابیس را به دست آوریم.
از این مرحله به بعد برای به دست آوردن نام دقیق دیتابیس 2 تا راه داریم:
- از آنجا که طول نام دیتابیس را به دست آورده ایم، می توانیم به ترتیب هر کدام از کاراکتر ها را چک کنیم که برابر چه حرفی است و بعد از شناسایی کاراکتر چهارم، نام دقیق دیتابیس را به دست آوریم.
- ابتدا به صورت کلی چک کنیم که اصلا نام دیتابیس شامل چه حروفی است و بعد از آن از روی حروف به دست آمده نام دقیق دیتابیس را حدس بزنیم. برای این کار از Query زیر استفاده می کنیم:
1' and (ascii(substring((select database()),1,1))=97)#
در Query بالا چک می کنیم که آیا نام دیتابیس شامل حرف a هست یا خیر(97 کد ASCII حرف a هست) و به همین ترتیب تک تک حروف دیگر را هم چک می کنیم تا نهایتا به 4 حرف برسیم که ترتیب قرارگیری شان مشخص نیست و از روی این 4 حرف ترتیب شان را حدس بزنیم.
اما ما در اینجا روش اول را انتخاب می کنیم؛
از آنجا که تا اینجای کار متوجه شدیم نام دیتابیس شامل 4 حرف هست، در این مرحله حرف تشکیل دهنده هر کدام از این 4 جایگاه که به ترتیب کنار هم قرار گرفته اند را شناسایی می کنیم.
در ابتدا سراغ کاراکتر اول می رویم.
چک می کنیم که آیا این کاراکتر برابر حرف a هست یا خیر و برای این کار Query زیر را تزریق می کنیم:
1' and (ascii(substring((select database()),1,1))=97)#
مشاهده می کنیم که پیام بالا نمایش داده شد پس حرف اول نام دیتابیس مقدار a نیست.
به ترتیب سایر حروف را چک می کنیم و برای این کار صرفا مقدار 97 را که معادل کد ASCII حرف a هست را افزایش می دهیم و هنگامی که به حرف d می رسیم و Query زیر را تزریق می کنیم، متوجه می شویم که پیام User ID exists in the database نمایش داده می شود؛ بنابراین حرف اول مقدار d هست.(در Query زیر 100 کد ASCII حرف d هست)
1' and (ascii(substring((select database()),1,1))=100)#
سراغ شناسایی حرف دوم می رویم.
اگر به سینتکس تابع Substring
که در ابتدای مقاله توضیح دادیم دقت کرده باشید متوجه می شوید که برای شناسایی حرف دوم، همان Query های بالا را تزریق می کنیم و فقط پارامتر دوم تابع Substring که جایگاه حرف را مشخص می کند از 1 به 2 تغییر می دهیم.
بنابراین Query زیر را تزریق می کنیم:
1' and (ascii(substring((select database()),2,1))=97)#
همانطور که مشاهده می کنید پیام بالا نشان می دهد که حرف دوم مقدار a نیست.
بنابراین مقدار 97 را افزایش داده و تک تک حروف را چک می کنیم.
نهایتا وقتی که به مقدار 118 می رسیم یعنی کد ASCII حرف v پیام بالا به پیام زیر تغییر پیدا می کند:
پس حرف دوم مقدار v هست.
به همین منوال Query های لازم برای به دست آوردن حروف سوم و چهارم نام دیتابیس را هم تزریق می کنیم و نهایتا بعد از تزریق Query های فراوان متوجه می شویم که حرف سوم آن w و حرف چهارم آن a هست.
خروجی زیر نشان می دهد که حرف سوم w هست:
و نهایتا خروجی زیر حرف آخر را مشخص می کند که مقدار a هست:
درنتیجه نام دقیق دیتابیس برابر dvwa هست.
به دست آوردن تعداد جداول دیتابیس
در این مرحله کار بسیار ساده ای در پیش داریم.
Query زیر را تزریق می کنیم:
1' and ((select count(table_name) from information_schema.tables where table_schema=database())=1)#
مشاهده می کنیم که خروجی بالا حاصل می شود پس تعداد جداول بیشتر از 1 جدول است.
سپس Query زیر را امتحان می کنیم:
1' and ((select count(table_name) from information_schema.tables where table_schema=database())=2)#
با مشاهده خروجی بالا نتیجه می گیریم که تعداد جداول دیتابیس این سایت 2 عدد هست.
استخراج نام جداول دیتابیس
در این مرحله برای استخراج نام جداول دیتابیس از ترکیب 2 مرحله قبل استفاده می کنیم؛
Query اصلی که تزریق می کنیم شبیه Query مرحله قبل هست اما روندی که طی می کنیم شبیه به دست آوردن نام دیتابیس هست؛ یعنی ابتدا طول نام جدول را به دست می آوریم و سپس مرحله به مرحله تک تک حروف تشکیل دهنده آن را حدس استخراج می کنیم تا به نام دقیق جدول برسیم.
برای به دست آوردن طول نام جدول از Query زیر استفاده می کنیم:
1' and length((select table_name from information_schema.tables where table_schema=database() limit 1,1))=1 #
در حقیقت در این Query طول جدول دوم موجود در دیتابیس را به دست می آوریم.
با وارد کردن Query بالا نتیجه زیر حاصل می شود:
همانطور که انتظار هم می رود، طول نام جدول بزرگتر از 1 هست؛ بنابراین مقدار 1 را افزایش می دهیم و هر دفعه پیام بالا را مشاهده می کنیم تا اینکه به عدد 5 می رسیم و Query زیر را تزریق می کنیم:
1' and length(substring((select table_name from information_schema.tables where table_schema=database() limit 0,1),2))=5 #
مشاهده می کنیم که جواب Yes از طرف سایت نمایش داده می شود بنابراین تعداد حروف نام جدول دوم دیتابیس برابر 5 هست.
سپس سراغ استخراج نام دقیق جدول می رویم و برای این کار از Query زیر استفاده می کنیم:
1' and ascii(substring((select table_name from information_schema.tables where table_schema=database() limit 1,1),1,1))=97 #
با وارد کردن Query بالا نتیجه زیر حاصل می شود:
خروجی بالا نشان می دهد که حرف اول نام جدول دوم موجود در دیتابیس مقدار a نیست؛ پس عدد 97 را همین طور افزایش داده و هر دفعه نتیجه ای مشابه نتیجه بالا مشاهده می کنیم تا اینکه به عدد 117 می رسیم.
با وارد کردن Query زیر مشاهده می کنیم که نتیجه خروجی تغییر می کند:
1' and ascii(substring((select table_name from information_schema.tables where table_schema=database() limit 1,1),1,1))=117 #
اگر به جدول ASCII ابتدا مقاله مراجعه کنید، مشاهده می کنید که عدد 117 کد ASCII حرف u هست؛ پس حرف اول نام جدول دوم u هست.
همین روند را برای حروف بعدی ادامه می دهیم و برای شناسایی حروف دوم و سوم و ... باید پارامتر دوم تابع Substring را تغییر دهیم.
برای شناسایی حرف دوم مقدار پارامتر دوم تابع Substring را از 1 به 2 تغییر می دهیم و Query زیر را تزریق می کنیم:
1' and ascii(substring((select table_name from information_schema.tables where table_schema=database() limit 1,1),2,1))=97 #
مشاهده می کنیم که پیام بالا ظاهر می شود بنابراین حرف دوم مقدار a نیست.
روند قبلی را برای این حرف و حروف دیگر نام جدول ادامه می دهیم و بعد از تزریق Query های بسیار متوجه می شویم که حروف دوم تا پنجم جدول به ترتیب s و e و r و s هستند؛ بنابراین نام جدول دوم دیتابیس users هست.
همین روند را برای جدول اول دیتابیس هم طی می کنیم و بعد از تزریق Query های فراوان متوجه می شویم که نام جدول اول دیتابیس guestbook هست.
استخراج نام فیلد های جدول users
در این مرحله هم روند کار مشابه حالت های قبل هست.
ابتدا تعداد فیلد ها را به دست آورده و سپس نام آن ها و برای به دست آوردن نام آنها مانند به دست آوردن نام دیتابیس عمل می کنیم.
ابتدا تعداد فیلد های جدول را از طریق Query زیر به دست می آوریم:
1' and (select count(column_name) from information_schema.columns where table_name= ‘users’)=1 #
با افزایش مقدار 1 زمانی که به مقدار 8 می رسیم با پیام Yes از طرف سایت مواجه می شویم.
پس تعداد فیلد های جدول users برابر 8 هست.
در ادامه با همان روشی که نام دیتابیس را استخراج کردیم، با استفاده از Query زیر نام کامل تک تک فیلد های جدول users را استخراج می کنیم:
1' and (ascii(substring((select column_name from information_schema.columns where table_name='users' limit 0,1),1,1))=97)#
در حقیقت در Query بالا چک می کنیم که آیا کد ASCII حرف اول فیلد اول جدول users
برابر 97 است یا خیر و مقدار 97 را همینطور افزایش می دهیم تا به 117 می رسیم و با پیام زیر روبرو می شویم:
بنابراین کد ASCII حرف اول فیلد اول جدول users برابر 117 هست که معادل کاراکتر u هست.
روند بالا را با تزریق Query های فراوان به سایت برای به دست آوردن تک تک فیلد های جدول users
ادامه می دهیم و نهایتا متوجه می شویم که نام 8 فیلد جدول users
به ترتیب زیر هست:
User_id, first_name, last_name, user, password, avatar, last_login, failed_login
استخراج اطلاعات کاربران
به مرحله آخر و حساس کار رسیدیم.
در حقیقت همه مراحل قبل را طی کردیم تا در این مرحله بتوانیم اطلاعات کاربران را از جدول users استخراج کنیم.
شاید شما بگویید که در این مرحله password یکی از کاربران را استخراج کنیم. اما باید بگوییم که در بیشتر سایت ها password ها hash شده و خروجی hash آن ها که 32 بیت هست در دیتابیس ذخیره می شود؛ در نتیجه با این که استخراج password هش شده هم با این روش شدنی است ولی حقیقتا زمانبر و سخت هست و ما در این مرحله آخر سراغ استخراج first_name
کاربر دوم موجود در دیتابیس می رویم.
کار دشواری در پیش نداریم؛ کافی است از Query زیر استفاده کنیم:
1' and (ascii(substring((select first_name from users limit 1,1),1,1))=65)#
2 نکته مهم در Query بالا:
- مقدار جلوی limit عدد 1,1 هست به این معنا که ردیف دوم جدول را در نظر بگیر.
- از آنجا که حرف اول نام کاربر به احتمال زیاد از حروف بزرگ هست، به جای 97 از 65 شروع می کنیم که معادل A هست.
با تزریق Query فوق نتیجه ای حاصل نمی شود.
مقدار 65 را افزایش می دهیم تا به 71 برسیم و نتیجه زیر را مشاهده کنیم:
از خروجی بالا معلوم می شود که کد ASCII حرف اول first_name
کاربر دوم موجود در دیتابیس مقدار 71 هست که معادل کاراکتر G هست.
ادامه کار هم مشابه تمام حالات قبلی است و کافی است که ورودی دوم تابع Substring را افزایش دهیم تا حروف بعدی را هم به دست آوریم.
بعد از تزریق Query های مربوطه متوجه می شویم که first_name
کاربر دوم موجود در دیتابیس برابر Gordon
هست.
به همین ترتیب می توانیم تمام اطلاعات کاربران را استخراج کنیم.
انقدر زحمت هم نیاز نبود!
اگر روندی که برای به دست آوردن نام دیتابیس و تعداد جداول و نام جداول و بعد از آن نام فیلد های جدول کاربران و نهایتا به دست آوردن داده های موجود در جدول کاربران طی کردیم را در نظر بگیرید ممکن است کلا از استخراج اطلاعات به روش Boolean based blind SQL Injection منصرف شوید اما باید بگوییم که خیلی از این مراحل را با حدس هم می توانستیم پیش بریم و نیازی به استخراج تک تک آن ها نبود.
مثلا برای نام جدول کاربران همان اول معلوم بود که به احتمال زیاد برابر users هست.
فیلد های جدول کاربران هم نظیر id، password، name
هم با کمی سعی و خطا معلوم می شد که مثلا فیلدی به نام user_id
یا user
دارد.
بنابراین داشتن تجربه و بررسی اسامی رایج برای جدول کاربران، ممکن است در بعضی از مواقع سرعت شما را در این نوع از SQL Injection بالاتر ببرد.
در طرف دیگر قضیه نیز در نظر داشته باشید که یکی از مواردی که بهره برداری از این نوع آسیب پذیری را سخت و دشوار می کند استفاده از پیشوند های تصادفی یا نام گذاری سخت و سفارشی شده جدوال است. بدین ترتیب با افزایش زمان و تعداد درخواست ها برای یک هکر احتمال اینکه او در دام فایروال ها یا سیستم های مانیتورینگ بیوفتد افزایش پیدا می کند.