C, PHP, VB, .NET

Дневникът на Филип Петров


* Вмъкване на нежелан SQL код

Публикувано на 29 ноември 2008 в раздел ОСУП.

Вмъкването на нежелан SQL код е уязвимост на приложението, при което атакуващият взима пълен или поне частичен контрол над базата данни на клиента. Почти при всички случаи това се дължи на не добре филтрирани входни данни или лош контрол върху множеството от стойности. Всъщност този пробив в сигурността е подмножество на една по-обширна тема, а именно филтриране на входните данни. Преди да започнем ще отбележим основното „лекарство“ за борба с тези атаки, а именно: никога не вярвайте на данни подадени от потребителя и винаги правете обстоятелствена проверка върху тях.

Ще демонстрираме проблема с едно примерно приложение. Нека имаме база данни, в която сме създали таблица „users“. Тази таблица има три полета – id, user и pass. Те представляват съответно идентификационен номер на потребителя, неговото потребителско име и парола:

Примерна таблица от база данни

Примерна таблица от база данни

Нека в тази таблица сме записали един потребител с потребителско име „test“ и md5 криптирана парола „test“:

Примерен запис в базата данни

Примерен запис в базата данни

Сега ще реализираме скрипт, който автентикира потребителя (спрямо въведено във форма име и парола). За целта ще използваме вече реализирания код от предишна статия и само ще променим функцията checklogin():

<?php
	function security_start_session($ssl, $timeout, $maxtime, $ip){
		// ... security checks
		session_start();
		session_regenerate_id(true);
		return 1;
	}
	if (security_start_session(0, 6*60, 20*60, 1) != 1){
		echo "<br>session destroyed!";
		return;
	}
?>
<html>
<head>
	<title>WorldBank.dom login page</title>
</head>
<body>
<?php
	function showloginform(){
		echo '<form action="db.php" method="POST">';
		echo 'user:';
		echo '<input type="text" name="user">';
		echo '<br>pass:';
		echo '<input type="password" name="pass">';
		echo '<br>Set cookie: ';
		echo '<input type="checkbox" name="remember" value="true">';
		echo '<br><input type="submit" name="submit"';
	}

	function checklogin($postuser, $postpass){
                if(!is_string($postuser) || !is_string($post_pass)){
                   return 0;
                }
		$dbhost="localhost";
		$dbuser="username";
		$dbpass="password";
		$dbname="dbusername";
		$tblname="users"; // Table name 

		mysql_connect("$dbhost", "$dbuser", "$dbpass") or die("cannot connect");
		mysql_select_db("$dbname")or die("cannot select DB");

		// Криптираме подадената парола
		$postpass = md5($postpass);

		$sql="SELECT ID FROM $tblname WHERE user='$postuser' and pass='$postpass'";

		$result=mysql_query($sql);

		if ($result) $count = mysql_num_rows($result);
		else $count = 0;
                
                mysql_free_result($result);
                mysql_close();

		if($count==1) return 1;
		else return 0;
	}

	if(isset($_COOKIE['user']) && isset($_COOKIE['pass'])){
		if (checklogin($_COOKIE['user'], $_COOKIE['pass']) == 1){
			$_SESSION['authenticated'] = 1;
		}
		else{
			setcookie("user","", mktime(12,0,0,1, 1, 1970));
			setcookie("pass","", mktime(12,0,0,1, 1, 1970));
		}
	}

	if($_SESSION['authenticated'] == 1){
		echo "Welcome back. I remember your session...";
	}
	else{
		if( isset($_POST['submit'])){
			if (checklogin($_POST['user'], $_POST['pass']) == 1){
				echo "You are logged in successfully!";
				$_SESSION['authenticated'] = 1;

				if($_POST["remember"]=="true"){
					setcookie("user",$_POST['user'], time()+3600, "/tests/",
								"cphpvb.net", false);
					setcookie("pass",$_POST['pass'], time()+3600, "/tests/",
								"cphpvb.net", false);
				}
			}
			else{
				echo "Invalid username and password";
			}
		}
		else{
			showloginform();
		}
	}

?>
</body></html>

Нека тестваме формата. Въвеждаме username „test“ и парола „test“ и eстествено получаваме очакван резултат:

Форма за вход

Форма за вход

Успешен вход

Успешен вход

Ще изпробваме и грешна комбинация, за да се уверим, че проверката работи:

Несъществуващ потребител

Несъществуващ потребител

Неуспешен вход

Неуспешен вход

Време е да демонстрираме и SQL injection. Въвеждаме следната информация (в полето на паролата може да стои напълно произволна информация – в случая сме въвели буквата „а“):

SQL Injection

SQL Injection

Получава се изненада:

Пробив заради нефилтриран вход

Пробив заради нефилтриран вход

Нашата форма ни позволява да влезем със всеки един потребител записан в системата. Може би вече се досетихте откъде идва проблема, но все пак нека разгледаме самите SQL заявки. В първият пример въведохме име „test“ и парола „test“:

	SELECT ID FROM users WHERE user='test' and pass='098f6bcd4621d373cade4e832627b4f6'

Тук всичко е наред. Нека видим как изглежда заявката, когато направихме пробив в сигурността:

	SELECT ID FROM users WHERE user='test' OR '1'='1' and pass='0cc175b9c0f1b6a831c399e269772661'

Всъщност дори условието ‘1’=’1′ e ненужно (пробива ще работи и с ‘1’=’2′) – единственото важно нещо в случая е вмъкването на думата „OR“. Чрез логическото „или“ ние напълно изключваме проверката за валидност на паролата. На всичко отгоре не е изключено и вмъкването на код, който да унищожи или повреди цялата база данни (например име „test’;DROP TABLE users;…„).

Решението на проблема е очевидно – трябва задължително да филтрираме подадените данни от потребителя. Ние не трябва да позволяваме въвеждането на кавички или интервали. Едно възможно решение е да криптираме и username с MD5 – ние сме сигурни, че криптирането с MD5 не връща т.нар. „escape characters“. Това е първо погрешен подход и второ не е винаги удачно като решение. Нека направим друго – въведете политика за възможни символи в потребителското име. Най-популярна е например то да бъде съставено само букви и цифри и да е с дължина между 4 и 32 символа:

	...
	function checklogin($postuser, $postpass){
		if (!is_string($postuser) || !preg_match('/^[a-z\d_]{4,32}$/i', $postuser)){
			return 0;
		}
	...

По аналогичен начин се прави проверка за всички променливи, за които се прави проверка. Естествено съществуват и редица готови функции, които ни помагат за нашата цел. Например между PHP и MySQL съществуват функциите stripslashes (връща низ с премахнати от него обратно наклонени черти) и mysql_real_escape_string (премахва всички символи, които биха предизвикали SQL injection):

	...
	function checklogin($postuser, $postpass){
                if(!is_string($postuser) || !is_string($post_pass)){
                   return 0;
                }               
		$postuser = stripslashes($postuser);
		$postuser = mysql_real_escape_string($postuser);

	...

Фунцията md5() връща „сигурен“ за MySQL низ. Трябва ли обаче да вярваме на това? Какво ще стане ако някой успее да подмени тази функция (като например я предефинира)?

	...
	function checklogin($postuser, $postpass){
	        if(!is_string($postuser) || !is_string($post_pass)){
                    return 0;
                }
 	        $postuser = stripslashes($postuser);
		$postuser = mysql_real_escape_string($postuser);
		$postpass = md5($postpass);
		$postpass = stripslashes($postpass);
		$postpass = mysql_real_escape_string($postpass);
	...

Показаният пример е само малка част от възможните проблеми. В общи линии, за да се защитите следвайте следните правила:
1. Никога не записвайте „несигурни“ данни в базата данни!
2. Винаги филтрирайте данните приети от потребителя!

Тук бихме искали да отбележим, че премахването на нежелани символи е по-слаба защита отколкото филтрирането по допустимо множество! Ако например филтрирате e-mail адрес е по-добре да забраните всички символи, различни от буква, цифра, „@’, „.“, „-“ и „_“, отколкото просто да изтриете кавичките от входа.

В следващата статия ще покажем някои други възможности за SQL injection.

Задача 1: Напишете просто приложение от няколко страници, използващи автентикация с база данни. Реализирайте скрипт за „изход“ от приложението, в който се унищожава потребителската сесия и се изтриват евентуалните cookies.

Задача 2: В примера по-горе са използвани стари функции за достъп до MySQL база данни през PHP (mysql_connect, …). Препишете програмата така, че да използва по-новите функции, които работят с MySQL 5.x и по-нови версии (mysqli_connect, …).

 



Добави коментар

Адресът на електронната поща няма да се публикува


*