C, PHP, VB, .NET

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


* Пример: login cookie с AES256-CBC криптиране в PHP

Публикувано на 18 март 2012 в раздел ОСУП.

От PHP версия 5.3.0 нататък в библиотеката на OpenSSL има включени две функции openssl_encrypt и openssl_decrypt, които ни позволяват да криптираме и декриптираме данни с различни алгоритми за симетрично криптиране. В този пример ще покажем как се използва AES256-CBC агоритъм, с който ще се криптират данни записани в потребителско cookie (по-конкретно името и паролата на потребителя). Целта ни е да защитим cookie по такъв начин, че дори да бъде откраднато, то хакерът няма да може нито да възстанови (декриптира) информацията от него, нито да го използва, за да се автентикира самия той. Ще използваме примерния скрипт за автентикация, който беше разглеждан в предишни статии (и на лабораторни упражнения), но модифициран да използва защитена директория „includes“ за важните данни, които не искаме да бъдат достъпни за „външния свят“. Примерът продължава да е с „ужасно зле форматиран код“, за което молим читателя за извинение (или вижте пример как когато се тръгне по лош път в самото начало, то проблемите се мултиплицират до края) :)

Файл .htaccess (в главната директория /…/htdocs) – за пренасочване към SSL:

	RewriteEngine On
	RewriteCond %{HTTPS} off
	RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI}

Файл cookie.php (намира се в главната директория /…/htdocs):

<?php
	session_start();
	// Генерираме ново session id и унищожаваме старото
	// за да предотвратим фиксиране на потребителска сесия
	session_regenerate_id(true);

	// Проверяваме дали не е прескочено времето за последна активност
	// или времето за максимален живот на сесия
	require_once("./includes/timers.php");

	// Функция, която отпечатва формата в html кода
	function showloginform(){
		echo '<form action="cookie.php" method="POST">';
		echo 'user:';
		echo '<input type="text" name="user">';
		echo '<br>pass:';
		echo '<input type="password" name="pass">';
		echo '<br />Remember me? ';
		echo '<input type="checkbox" name="remember" value="true" />';
		$_SESSION['AntiXSRFToken'] = uniqid(rand(), true);
		echo '<input type="hidden" name="AntiXSRFToken"';
		echo ' value="'.$_SESSION['AntiXSRFToken'].'" />';
		echo '<br><input type="submit" name="submit" value="Login"';
	}
	// Функция, която проверява за вярност на име и парола
	require_once("./includes/checklogin.php");

	// Проверяваме дали потребителя има записано COOKIE
	if(isset($_COOKIE['user'])&&isset($_COOKIE['pass'])){
		// Ако има, то трябва първо да декриптираме
		// информацията от него
		require_once("./includes/aesdecrypt.php");
		$cookieuser = aesdecrypt($_COOKIE['user']);
		$cookiepass = aesdecrypt($_COOKIE['pass']);
		if(checklogin($cookieuser,$cookiepass)===1){
			$_SESSION['authenticated']=1;
		}
		else{
			setcookie("user","", mktime(12,0,0,1, 1, 1970));
			setcookie("pass","", mktime(12,0,0,1, 1, 1970));
		}
	}

	// Ако вече сме автентикирани, то сървъра
	// е "запомнил" това в полето 'authenticated'
	if(isset($_SESSION['authenticated'])&&$_SESSION['authenticated'] === 1){
		echo '<html><head><title>WorldBank.dom login page';
		echo '</title></head><body>';
		echo "Welcome back. I remember your session...";
	}
	else{
	   // Ако сме попълнили формата и сме натиснали Submit
	   if( isset($_POST['submit'])){
		// Проверяваме дали формата не е попълнена от друг хост
		if(!isset($_POST['AntiXSRFToken']) ||
		   !isset($_SESSION['AntiXSRFToken']) ||
		   $_POST['AntiXSRFToken']!=$_SESSION['AntiXSRFToken']
		){
			session_unset();
			session_destroy();
			echo 'It looks like you filled the form on unauthorized host';
			echo '<br />You are probably a victim of XSRF attack';
			exit;
		}
		// Валидираме данните:
		if (checklogin($_POST['user'], $_POST['pass']) === 1){
		   if(isset($_POST['remember'])&&$_POST['remember']==="true"){
			// Криптираме данните, които ще се запишат в Cookie
			require_once("./includes/aesencrypt.php");
			setcookie("user", aesencrypt($_POST['user']), time()+3600);
			setcookie("pass", aesencrypt($_POST['pass']), time()+3600);
		   }
		   echo '<html><head><title>WorldBank.dom login page</title></head><body>';
		   echo "You are logged in successfully!";
		   $_SESSION['authenticated'] = 1;
		}
		else{
		   echo '<html><head><title>WorldBank.dom login page</title></head><body>';
		   echo "Invalid username and password";
		}
	   }
	   // В противен случай влизаме за първи път
	   // и трябва да се изкара формата:
	   else{
		showloginform();
	   }
	}
?>
</body></html>

Файл checklogin.php (намира се в директория /…/htdocs/includes/):

<?php
	function checklogin($user, $pass){
                if(!is_string($user) || !is_string($pass){
                       return 0;
                }
		require_once("salt.php");
		require_once("dbinfo.php");

		//$user = mysql_real_escape_string($user);
		if(!preg_match('/^[a-zA-Z\d_]{4,32}$/i', $user)){
			return 0;
		}
		if(!preg_match('/^[a-zA-Z\d_]{4,32}$/i', $pass)){
			return 0;
		}
		$pass = SALT.$pass;

		mysql_connect($dbhost, $dbuser, $dbpass) or die("Cannot connect to database");
		mysql_select_db($dbname) or die("Cannot select database");

		$sql = "SELECT id FROM users WHERE user = '$user' ";
		$sql .= "AND pass = SHA2(CONCAT('$pass',CAST(token AS CHAR)),256)";

		$result = mysql_query($sql);

		if($result){
			if(mysql_num_rows($result)==1){
				mysql_free_result($result);
				mysql_close();
				return 1;
			}
			else{
				mysql_free_result($result);
				mysql_close();
				return 0;
			}			

		}
		else{
			echo 'Cannot execute SQL query';
			mysql_close();
			return 0;
		}
	}
?>

Файл salt.php (намира се в директория /…/htdocs/includes/):

<?php
	// Това е SALT, който използваме, за да подсигурим паролите
	// когато те се записват в базата от данни (salt от първи тип)
	// Колкото по-дълъг е този низ, толкова по-добре (въпреки заетата памет)
	define('SALT', 'adsa34343SFhsjfdsF');
?>

Файл timers.php (намира се в директория /…/htdocs/includes/):

<?php
	// Таймерът за последна активност в случая има смисъл
	// да поставим максимално време за попълване на формата
	// Това принуждава хакера да поддържа формата активна
	// ако се опитва да прави session fixation
	$timeout = 5 * 60; // 5 минути таймер за последна активност
	$curtime = (int)time();
	if (isset($_SESSION['last_active'])){
		if ($curtime-$_SESSION['last_active'] > $timeout){
			echo 'Session timeout';
			session_destroy();
			return;
		}
	}
	$_SESSION['last_active'] = $curtime;

	// Максимален живот за потребителска сесия
	// Ако в php.ini max_session_time е по-къс
	// то долния код няма значение. По-добре е
	// да настройвате максимален живот на сесия
	// през php.ini, а не чрез долния код.
	$maxSessionTime = 5 * 60 * 60; // 5 часа
	if(! isset($_SESSION['initial_time'])){
		$_SESSION['initial_time'] = (int)time();
	}
	if($curtime-$_SESSION['initial_time'] > $maxSessionTime){
		echo 'Maximum session time exceeded';
		session_destroy();
		return;
	}
?>

Файл dbinfo.php(намира се в директория /…/htdocs/includes/):

<?php
	$dbhost = "localhost";
	$dbuser = "dbusername";
	$dbpass = "dbpassword";
	$dbname = "databasename";
	$tblname = "users";
?>

Файл aesencrypt.php (намира се в директория /…/htdocs/includes/):

<?php
   function aesencrypt($string){
 	// в aessalt.php сме записали произволен низ
	require_once("aessalt.php");
	// $pass е ключът, с който криптираме
	// Не е задължително да хеширате ключа.
	// Ако правите по-сложни операции внимавайте
	// защото работим с Environment Variable, т.е. хубаво е
	// да се направи валидиране на данните от нея
        $pass  = AESSALT;
        $pass .= $_SERVER['HTTP_ACCEPT'];
        $pass .= $_SERVER['HTTP_ACCEPT_LANGUAGE'];
        $pass .= $_SERVER['HTTP_ACCEPT_ENCODING'];
        $pass .= $_SERVER['HTTP_CONNECTION']);
	// ripemd160 е бърз и засега приет за сигурен алгоритъм за хеширане
	// В нашия случай обаче сигурност при хеширането въобще не е нужна
	// Затова тук може да използвате дори md5
	$pass = substr(hash('ripemd160',$pass), 5, 16);
	// 16 битов низ, който се използва за "initiatialization vector"
        // ВНИМАНИЕ: така заключваме сесия по IP и User Agent. В общия
        //           случай по добре да НЕ го правим.
	$iv = $_SERVER['REMOTE_ADDR'].AESSALT;
	$iv .= $_SERVER['HTTP_USER_AGENT'];
	$iv = substr(hash('ripemd160',$iv),5,16);
	// използваме AES256 CBC алгоритъм
	return openssl_encrypt($string,'aes-256-cbc',$pass,false,$iv);
   }
?>

Файл aesdecrypt.php (намира се в директория /…/htdocs/includes/):

<?php
	function aesdecrypt($string){
		// Възстановяваме същия ключ
		require_once("aessalt.php");
                $pass = AESSALT;
                $pass .= $_SERVER['HTTP_ACCEPT'];
                $pass .= $_SERVER['HTTP_ACCEPT_LANGUAGE'];
                $pass .= $_SERVER['HTTP_ACCEPT_ENCODING'];
                $pass .= $_SERVER['HTTP_CONNECTION']);
		$pass = hash('ripemd160',$pass);
		// IV също трябва да е същия както преди
		$iv = $_SERVER['REMOTE_ADDR'].AESSALT;
		$iv .= $_SERVER['HTTP_USER_AGENT'];
		$iv = substr(hash('ripemd160',$iv),5,16);
		// Отново използваме AES256 CBC алгоритъм, но този път декриптираме
		return openssl_decrypt($string,'aes-256-cbc',$pass,false,$iv);
	}
?>

Файл aessalt.php (намира се в директория /…/htdocs/includes/):

<?php
	define('AESSALT', 'fdsahf#@643432hd');
?>

Файл .htaccess (намира се в директория /…/htdocs/includes/):

order deny,allow
deny from all

Последния (.htaccess) файл се използва, за да инструктира Apache да не позволява външен достъп до файловете в директорията. Това може да бъде подобрено още повече като се добавят и съответните права за достъп на файловете и директорията като цяло. Все пак внимавайте – скриптът cookie.php трябва да има достъп до тези файлове, защото ги използва.

Ето и нашата тестова таблица users:

mysql> desc users;
+-------+------------------+------+-----+---------+----------------+
| Field | Type             | Null | Key | Default | Extra          |
+-------+------------------+------+-----+---------+----------------+
| id    | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| user  | varchar(32)      | NO   | UNI | NULL    |                |
| pass  | char(64)         | NO   |     | NULL    |                |
| token | decimal(17,16)   | NO   |     | NULL    |                |
+-------+------------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)

mysql> SELECT * FROM users;
+----+-------+------------------------------------------------------------------+--------------------+
| id | user  | pass                                                             | token              |
+----+-------+------------------------------------------------------------------+--------------------+
| 14 | ivan  | bef2b1830d6de8ee68338fdef8be747f01193f842f709b2be40440dbf4bad4be | 0.5487598651542885 |
| 15 | petar | 3fbfe8b2dcb419e181a674bc26064d2376b44276874cb4eee0d4f7d9a9822906 | 0.1348247035730860 |
| 16 | maria | 7975163475dfe60d48cf56db38276aee5ec12ed9f94aebe9884f1bb16281177b | 0.0278439531362094 |
+----+-------+------------------------------------------------------------------+--------------------+

* Паролите в базата данни са записани с SHA256 алгоритъм, който е наличен чрез вградена функция SHA2(„string“,256) в MySQL.

Изпробвайте примерния скрипт и вижте съдържанието на cookie, което записва. Опитайте се да използвате същото cookie на друг компютър. Потърсете грешки в сигурността на кода.

Задача 1: В горния пример ще видите, че ако двама различни потребители имат една и съща парола и ако използват един и същи компютър, то криптираната парола в cookie за тях ще бъде еднаква. Помислете как може да се направи така, че да бъде различна.

Задача 2: Лимитирането на акаунт по IP адрес понякога е прекалено рестриктивно – мобилните устройства си сменят IP-тата при преминаване от една мрежа в друга. Помислете за други характеристики, които могат да се използват.

Задача 3: Добавете фиксиране на login cookie с fingerprint на браузъра взет чрез JavaScript, както беше описано в следните две статии:

 



7 коментара


  1. Иван каза:

    По задачата бих казал в IV към солта да се добавя освен IP адреса на посетителя и неговия username.

  2. Правилно решение. Може и към $pass… и/или към двете. Изобщо варианти всякакви. Този е добър. Може да се вземе и някаква друга специфична за браузъра характеристика на потребителя.

  3. Бива… хммм… ами всъщност сложих ги – в предишния коментар :) Хайде и още един:

    Колизии при hash алгоритмите

    Добавих още една „задача“ към статията.

  4. Иван каза:

    Ако напишишеш две думи какво са Rainbow tables и защо е необходима солта, статията ще вдигне още едно ниво на security :)

  5. Иван каза:

    Супер, ама все пак може да сложиш линковете в статията. Ще стане по-събрано и уикипедийско някак си :)

  6. Промених скриптовете така, че да използват по-сигурни хеш алгоритми вместо вече „пробития“ md5

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

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


*