SQL Injection یا تزریق SQL، یک آسیب پذیری امنیتی وب است که مهاجم میتواند به واسطهی آن، در کوئریهایی که یک اپلیکیشن به دیتابیس خود میفرستد تداخل ایجاد کرده و آنها را دستکاری کند. این آسیبپذیری عموماً به مهاجم اجازه میدهد دادههایی را ببیند که در اصل قرار نبوده قادر به دیدن آنها باشد! این داده ممکن است شامل دادههایی باشد که متعلق به کاربران مختلف است، و یا دادههای دیگری که متعلق به خود اپلیکیشن هستند. در بسیاری از مواردی که آسیبپذیری تزریق SQL وجود دارد، مهاجم میتواند این دادهها را دستکاری یا حذف کند، و تغییراتی طولانیمدت و پایدار در محتوای اپلیکیشن یا رفتار آن ایجاد کند.
در بعضی شرایط، مهاجم میتواند شدت SQL Injection را بیشتر کرده و سروری که دیتابیس روی آن قرار گرفته یا باقی زیرساختهای بکاند را مورد تهاجم قرار داده یا دست به یک حملهی منع سرویس (DoS) بزند.
عواقب یک حمله SQL Injection موفق چیست؟
یک حملهی تزریق SQL موفق میتواند منجر به دسترسی غیرمجاز به دادههای حساس مانند پسوردها، مشخصات کارتهای اعتباری یا اطلاعات شخصی کاربران شود. بسیاری از نفوذهای اطلاعاتی بزرگ و معروف که در سالهای گذشته خبرساز شدهاند، نتیجهی حملات تزریق SQL بودهاند که خسارات و جریمههای بسیار زیادی را به جا گذاشتهاند؛
خسارات و جریمههایی که حتی ممکن است شما هم چیزهایی راجع به آنها شنیده باشید. در بعضی موارد یک مهاجم حتی میتواند با این حمله، یک بکدور (backdoor) پایدار در سیستمهای سازمان مستقر کند، که باعث آلودگی طولانیمدتی میشود و ممکن است تا مدتهای زیادی شناسایی نشود.
مثالهایی از تزریق SQL
آسیبپذیریها، حملات و تکنیکهای تزریق SQL، تنوع بسیار بالایی دارند، که حاصل از شرایط مختلف است. بعضی از رایجترین نمونههای تزریق SQL عبارتند از:
- دستیابی به دادههای پنهان: زمانی که میتوانید یک کوئری SQL را به گونهای دستکاری کنید که نتایج اضافه و بیشتری را به شما بازگرداند.
- اختلال در منطق اپلیکیشن: زمانی که میتوانید یک کوئری را به گونهای دستکاری کنید که باعث اختلال در منطق اپلیکیشن و تغییر رفتار آن و بروز رفتارهای ناخواسته شود.
- حملات UNION: حملاتی که در آنها میتوانید دادهها را از جدولهای (یا tableهای) مختلف در دیتابیس استخراج کنید.
- وارسی دیتابیس: زمانی که بتوانید اطلاعاتی راجع به نسخه و ساختار دیتابیس به دست آورید.
- تزریق SQL کور: زمانی که نتایج کوئری دستکاریشده در پاسخهای اپلیکیشن بازگردانده نمیشود، اما با استفاده از عبارات شرطی، تاخیر یا آشکارسازهای دیگر در کوئری، میتوان به صورت آزمون و خطا اطلاعات دیتابیس را خارج کرد.
دستیابی به دادههای پنهان (Retrieving Hidden Data)
یک اپلیکیشن خرید را در نظر بگیرید که محصولات را در دستهبندیهای مختلف نشان میدهد. هر کاربری که روی دستهبندی «هدایا» کلیک کند، مرورگر آن کاربر این URL را به عنوان ریکوئست ارسال میکند:
https://insecure-website.com/products?category=Gifts
اپلیکیشن با دریافت این ریکوئست، یک کوئری SQL به دیتابیس خود ارسال میکند تا اطلاعات محصولات این دستهبندی را از دیتابیس دریافت کند:
SELECT * FROM products WHERE category = ‘Gifts’ AND released = 1
این کوئری SQL از دیتابیس میخواهد که این اطلاعات را بازگرداند:
- تمام اطلاعات (*)
- از جدول محصولات (FROM products)
- که دستهبندی آنها هدیه است (WHERE category = ‘Gifts’)
- و منتشر شدهاند (AND released = 1)
شرط released = 1 برای این استفاده شده که محصولاتی که هنوز منتشر نشدهاند، نشان داده نشوند. میتوان حدس زد برای محصولاتی که منتشر نشدهاند، released برابر صفر است.
این اپلیکیشن هیچ راهکار دفاعی برای جلوگیری از حملات تزریق اس کیو ال استفاده نکرده است، به همین خاطر میتواند با ساختن ریکوئستی مانند ریکوئست زیر، به این اپلیکیشن حمله کند:
https://insecure-website.com/products?category=Gifts’–
این ریکوئست، باعث ارسال این کوئری SQL توسط اپلیکیشن میشود:
SELECT * FROM products WHERE category = ‘Gifts’–‘ AND released = 1
نکتهی کلیدی که باید در اینجا به آن دقت کرد، دنبالهی متشکل از دو خط فاصله، یعنی « — » است؛ این دنباله در زبان SQL علامت کامنت است و به این معناست که باقی کوئری باید به عنوان کامنت تفسیر شود، و به همین دلیل هر چیزی که در ادامهی آن بیاید اجرا نمیشود؛ این یعنی عملاً ادامهی کوئری حذف میشود، پس دیگر شامل قسمت AND released = 1 نخواهد بود. این باعث میشود تمام محصولات، حتی محصولاتی که منتشر نشدهاند، نمایش داده شوند.
مهاجم میتواند پا را یک گام فراتر گذاشته و کاری کند که اپلیکیشن تمام محصولات در تمام دستهبندیها را نشان دهد، حتی دستهبندیهایی که مهاجم از وجود آنها اطلاع ندارد:
https://insecure-website.com/products?category=Gifts’+OR+1=1–
این ریکوئست باعث میشود اپلیکیشن کوئری زیر را ارسال کند:
SELECT * FROM products WHERE category = ‘Gifts’ OR 1=1–‘ AND released = 1
این کوئری دستکاریشده تمامی آیتمهایی را برمیگرداند که برای آنها یکی از این دو شرط صدق کند: دستهبندی آنها هدیه (Gifts) باشد، یا 1 برابر 1 باشد؛ از آنجایی که شرط 1=1 همیشه درست است، کوئری تمام آیتمهای موجود در دیتابیس را برمیگرداند.
اختلال در منطق اپلیکیشن (Subverting Application Logic)
اپلیکیشنی را فرض کنید که به کاربران اجازه میدهد با واردکردن یک یوزرنیم و پسورد در آن لاگین کنند. مثلاً اگر یک کاربر یوزرنیم wiener و پسورد bluecheese را وارد کند، اپلیکیشن صحت این اطلاعات ورود را با این کوئری SQL بررسی میکند:
SELECT * FROM users WHERE username = ‘wiener’ AND password = ‘bluecheese’
اگر کوئری اطلاعات یک کاربر را برگرداند، در این صورت لاگین موفقیتآمیز خواهد بود. در غیر این صورت، لاگین ناموفق بوده است.
حال یک مهاجم میتواند به راحتی و بدون داشتن پسورد، با یوزرنیم هر کاربری که خواست به اپلیکیشن وارد شود. چگونه؟ کافی است مهاجم از علامت کامنت SQL، یعنی – استفاده کرده و شرط بررسی پسورد را از عبارت WHERE در کوئری حذف کند. برای مثال، واردکردن یوزرنیم administrator’— و خالیگذاشتن پسورد، باعث ارسال این کوئری میشود:
SELECT * FROM users WHERE username = ‘administrator’–‘ AND password = ”
این کوئری کاربری را برمیگرداند که یوزرنیم آن 1administrator باشد و در صورت وجود چنین کاربری (که به احتمال زیاد وجود دارد)، مهاجم را به حساب آن لاگین میکند.
دستیابی به دادههای جدولهای دیگر (UNION Attack)
در مواردی که نتایج یک کوئری SQL داخل پاسخهای اپلیکیشن برگردانده میشوند، مهاجم میتواند از آسیبپذیری SQL Injection برای دستیابی به دادههای موجود در جدولهای دیگر دیتابیس استفاده کند. این کار با استفاده از کلمهی کلیدی UNION انجام میشود؛ این کلمه کلیدی به شما اجازه میدهد یک کوئری SELECT اضافه اجرا کنید و نتایج آن را به کوئری اصلی الحاق کنید.
برای مثال، اگر یک اپلیکیشن کوئری زیر را اجرا کند که حاوی ورودی کاربر، یعنی «Gifts» است:
SELECT name, description FROM products WHERE category = ‘Gifts’
در این صورت مهاجم میتواند این ورودی را وارد کند:
‘ UNION SELECT username, password FROM users—
این کار باعث میشود اپلیکیشن در کنار نام و مشخصات محصولات، تمام یوزرنیمها و پسوردها را هم برگرداند.
وارسی دیتابیس (Examining the Database)
پس از شناسایی اولیهی وجود یک آسیبپذیری SQL، معمولاً بهتر کمی اطلاعات نیز راجع به خود دیتابیس به دست آوریم. این چنین اطلاعاتی معمولاً راه را برای اکسپلویت بیشتر هموار میکنند.
شما میتوانید اطلاعات مربوط به نسخه دیتابیس را کوئری کنید. این که برای هر دیتابیس چگونه میتوان نسخهی آن را کوئری کرد، بستگی به نوع دیتابیس دارد؛ یعنی برای هر دیتابیس روش کوئریکردن نسخه متفاوت است.
از همین مساله میتوان استفاده کرد و از روشهای مختلف نسخهی دیتابیس را کوئری کرد؛ و از روی این که کدام روش کوئری نسخهی دیتابیس را به درستی برمیگرداند، میتوان نوع دیتابیس را هم متوجه شد. برای مثال، برای کوئریکردن نسخهی یک دیتابیس اوراکل، باید دستور زیر را اجرا کرد:
SELECT * FROM v$version
علاوه بر این میتوانید تعیین کنید چه جدولهایی در دیتابیس وجود دارند، و هر کدام از این جدولها حاوی چه ستونهایی است. برای مثال، شما میتوانید روی اکثر دیتابیسها دستور زیر را کوئری کنید تا لیستی از جدولها را به شما برگرداند:
SELECT * FROM information_schema.tables
آسیبپذیریهای تزریق SQL کور (Blind SQL Injection)
نمونههای بسیار زیادی از SQL Injection، آسیبپذیریهای اصطلاحاً «کور» هستند. معنی این اصطلاح این است که اپلیکیشن نتایج کوئری SQL یا اطلاعات مربوط به هیچکدام از خطاهای دیتابیس را در پاسخهای خود نمایش نمیدهد.
البته با این وجود، همچنان میتوان آسیبپذیریهای کور را اکسپلویت کرد و به دادههای غیرمجاز دسترسی پیدا کرد، ولی معمولاً تکنیکهایی که برای این کار لازم هستند بسیار پیچیدهتر بوده و اجرای آنها دشوار است.
بسته به نوع آسیبپذیری و دیتابیس، میتوان از تکنیکهای زیر برای اکسپلویت آسیبپذیریهای تزریق اسکیوال استفاده کرد:
• میتوانید منطق کوئری را تغییر دهید تا یک تفاوت قابل تشخیص در پاسخ اپلیکیشن مشاهده کنید؛ تفاوتی که به صحت یا عدم صحت منطق کوئری بستگی داشته باشد. برای این کار ممکن است لازم باشد یک شرط جدید به یک منطق بولی (Boolean) اضافه کنید، یا بر اساس شرایط خاصی یک خطا مانند خطای تقسیم را به وجود آورید.
• میتوانید به گونهای کوئری را طراحی کنید که در صورت صدق کردن یک شرایط خاص، یک تاخیر زمانی در پردازش کوئری به وجود بیاید؛ آن گاه میتوانید بر اساس مدت زمانی که طول میکشد تا اپلیکیشن به ریکوئست شما پاسخ دهد، درست بودن یا نبودن شرایط را متوجه شوید.
• میتوانید با استفاده از تکنیکهای OAST، یک تعامل خارج از باند (out-of-band) با شبکه داشته باشید. این تکنیک بهشدت قدرتمند است و در بسیاری از شرایطی که تکنیکهای دیگر موثر نیستند، به
خوبی کار میکند. معمولا میتوانید داده را به صورت مستقیم از طریق کانال خارج از باند استخراج کنید؛ مثلا میتوانید داده را در یک درخواست DNS lookup برای دامنهای قرار دهید که در کنترل شماست.
چطور میتوان آسیبپذیریهای SQL Injection را شناسایی کرد؟
اکثر قریب به اتفاق آسیبپذیریهای SQL Injection را میتوان به سرعت و با اطمینان خاطر با استفاده از اسکنر آسیبپذیری وب Burp Suite پیدا کرد.
SQL Injection را میتوان به صورت دستی و با استفاده از مجموعهای نظاممند از تستهای گوناگون روی تمام درگاههای ورود داده به اپلیکیشن نیز پیدا کرد. این تستها معمولا شامل موارد زیر هستند:
- واردکردن یک کاراکتر quote تنها و بررسی رخداد خطا یا دیگر اتفاقات غیرمعمولی.
- واردکردن چند دستور با املای SQL که مقدار پایه (مقدار اصلی) فیلد ورود داده را ارزیابی میکنند، و سپس دستوراتی که مقدار متفاوتی را ارزیابی میکنند، و سپس بررسی وجود تفاوتهای بنیادی در پاسخهایی که اپلیکیشن در دو حالت ارسال میکند.
- وارد کردن شرایط بولی مانند OR 1=1 یا OR 1=2 یا and ، و بررسی تغییرات احتمالی در پاسخ اپلیکیشن.
- واردکردن پیلودهایی که به گونهای طراحی شدهاند که زمانی که داخل یک کوئری SQL اجرا میشوند، تاخیر زمانی ایجاد کنند، و بررسی تغییر مدت زمانی که پاسخ اپلیکیشن طول میکشد.
- واردکردن پیلودهای OAST که به گونهای طراحی شدهاند که زمانی که داخل یک کوئری SQL اجرا میشوند، باعث یک تعامل خارج از باند در شبکه شوند، و مانیتورکردن تعاملهای احتمالی
تزریق SQL در بخشهای مختلف کوئری
اکثر آسیبپذیریهای تزریق SQL برخاسته از کوئریهای WHERE و SELECT هستند. معمولا کارشناسان تست نفوذ باتجربه، درک خوبی از این نوع تزریق SQL دارند.
ولی آسیبپذیریهای SQL Injection عملا ممکن است در هر جایی در کوئری، و در انواع مختلف کوئری رخ دهند. رایجترین نقاط دیگر در کوئری که ممکن است SQL Injection رخ دهد عبارتند از:
- در دستورات UPDATE، داخل مقادیر بهروزرسانیشده یا عبارت WHERE
- در دستورات INSERT، در مقادیر واردشده
- در دستورات SELECT، در نام جدول یا ستون
- در دستورات SELECT، در عبارت ORDER BY
تزریق SQL مرتبه دو
تزریق SQL مرتبه یک زمانی رخ میدهد که اپلیکیشن ورودی کاربر را از یک ریکوئست HTTP دریافت میکند و حین پردازش آن ریکوئست، به طریقی غیرایمن آن ورودی را در یک کوئری SQL به کار میگیرد.
در تزریق SQL مرتبه دو ( که به آن تزریق SQL ذخیرهشده یا stored هم میگویند)، اپلیکیشن ورودی کاربر را از یک ریکوئست HTTP دریافت میکند و آن را برای استفاده در آینده ذخیره میکند. این کار معمولا با قراردادن ورودی در یک دیتابیس انجام میشود، اما در جایی که داده ذخیره میشود هیچ آسیبپذیری به وجود نمیآید. ولی بعداً، وقتی که اپلیکیشن دارد یک ریکوئست HTTP دیگر را انجام میدهد، دادهی ذخیرهشده را بازیابی کرده و آن را به شیوهای غیرایمن در کوئری SQL استفاده میکند، که همین ممکن است باعث ایجاد آسیبپذیری شود.
تزریق SQL مرتبه دو معمولا زمانهایی اتفاق میافتد که توسعهدهندگان از وجود آسیبپذیریهای SQL Injection آگاهی دارند، و به همین خاطر جایگذاری اولیهی ورودی در دیتابیس را به طریقی ایمن انجام میدهند. ولی بعدا وقتی که داده پردازش میشود، ایمن محسوب میشود، زیرا قبلا به روشی ایمن در دیتابیس جایگذاری شده است. در این مرحله، داده به گونهای غیرایمن استفاده میشود، زیرا توسعهدهنده به اشتباه آن را دادهی قابل اطمینان فرض کرده است.
عوامل وابسته به دیتابیس
برخی از ویژگیهای اصلی زبان SQL در بسترهای پرطرفدار دیتابیس به شیوهی یکسانی پیادهسازی شدهاند، و به همین خاطر تعداد زیادی از روشهای تشخیص و اکسپلویت آسیبپذیریهای SQL Injection، روی انواع مختلف دیتابیس دقیقا به یک شکل عمل میکنند.
با این وجود، تفاوتهای زیادی هم بین دیتابیسهای رایج وجود دارد. وجود این تفاوتها باعث میشود که بعضی تکنیکها برای یافتن و اکسپلویت SQL Injection، روی بسترهای مختلف، با هم متفاوت باشند. برای مثال:
• سینتکس دستور به هم چسباندن استرینگها
• کامنتها
• کوئریهای Batched (یا Stacked)
• APIهای خاص هر پلتفرم
• پیامهای خطا
چگونه از تزریق SQL جلوگیری کنیم؟
با استفاده از کوئریهای پارامتری یا parametrized (یا همان عبارات از پیش آمادهشده) به جای به هم چسباندن استرینگها در داخل کوئریها، میتوان از بسیاری از انواع SQL Injection جلوگیری کرد.
برای مثال کد زیر نسبت به SQL Injection آسیبپذیر است، زیرا ورودی کاربر به طور مستقیم در کوئری استفاده میشود:
String query = “SELECT * FROM products WHERE category = ‘”+ input + “‘”;
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(query);
این کد را میتوان به گونهای بازنویسی کرد که از تلفیق ورودی کاربر در ساختار کوئری جلوگیری شود:
PreparedStatement statement = connection.prepareStatement(“SELECT * FROM products WHERE category = ?”);
statement.setString(1, input);
ResultSet resultSet = statement.executeQuery();
هر وقت که ممکن است ورودی غیر قابل اطمینان در قالب داده داخل کوئری وارد شود، از جمله برای عبارت و مقادیر WHERE و همچنین در دستورات INSERT و UPDATE، میتوان از کوئریهای پارامتری استفاده کرد. البته از این نوع کوئری نمیتوان برای پردازش دادههای غیر قابل اطمینان در جاهای دیگر کوئری، مثلا نام جدولها یا ستونها، یا در عبارات ORDER BY استفاده کرد. برای ایمنکردن آن دسته از کارکردهای اپلیکیشن که دادههای غیر قابل اطمینان را در این بخشهای کوئری قرار میدهند، لازم است رویکرد دیگری پیش گرفته شود؛ مثلا میتوان لیست سفیدی از مقادیر ورودی مجاز ایجاد کرد، یا میتوان از دستورات و منطق دیگری برای ایجاد رفتار مورد نیاز استفاده کرد.
برای این که کوئری پارامتری در جلوگیری از SQL Injection موثر واقع شود، استرینگی که در کوئری استفاده میشود باید همیشه یک مقدار ثابت کدنویسیشده ( یا همان hard code شده) داشته باشد، و هیچوقت نباید حاوی هیچ دادهی متغیری از هیچ منبعی باشد. ممکن است وسوسه شوید که برای هر مورد تصمیم بگیرید که یک آیتم حاوی داده قابل اعتماد هست یا نه، و اگر آیتم قابل اعتماد بود، همچنان استرینگها را داخل کوئری به هم بچسبانید، ولی تصمیم این وسوسه نشوید و از این کار خودداری کنید. زیرا اشتباهکردن دربارهی منبع احتمالی داده به راحتی آبخوردن است، یا حتی ممکن است تغییراتی در قسمتهای دیگری از کد ایجاد شود که فرضهای قبلی را دربارهی قابل اطمینان بودن یک داده خاص از بین ببرد.