На главном сервисе моей основной работы произошел сбой. Надо сказать, весьма критичный для нашей компании.

Разбор логов показал, что была проведена SQL инъекция вредоносного эксплойта в один из SQL запросов на сайте.

Дело осложнялось тем, что до меня сайтом заведовали люди, которые собственно на нем и учились программировать. Море кода, на который мой взгляд даже не падал. И где-то в нем, была дырка.

О том, как я решил эту проблему и пойдет речь в данной статье.

Для начала, небольшой ликбез, что такое SQL инъекция.

 

 

 

Что такое SQL инъекция

Возьмем некий псевдо код

if( intval($_GET['id']) )
    mysql_query('select * from news where id="'.$_GET['id'].'"';

Здесь есть проверка на то, что  intval должен вернуть целое число, и если в id лежит не число, то условие не сработает и запрос не будет исполнен.

И это верно работает, если сделать такой запрос

http://sitename.ru/index.php?id=new

то условие не выполнится: intval('new') вернет 0.

Кажется, что этого достаточно. Но вопрос, что произойдет при следующем запросе

http://sitename.ru/index.php?id=39new

Удивительно, но условие сработает. intval в этом плане, очень крутая функция. В отличии к примеру от JavaScript'овского parseInt, который вернет в таком случае NaN. Она спокойно переварит такие входные данные, и возьмет из них только число (заметим, что intval('new39') вернет 0.)

Отсюда вытекает дыра в безопасности. Через нее, можно вставить любой SQL запрос. Вплоть до delete from news

Это очень распространенная ошибка в web'деве. Подвержены ей не только проекты начинающих программистов. Если Вы используете CMS с открытым исходным кодом, то такая вероятность тоже есть. Ведь злоумышленник легко может изучить исходники этой системы или воспользоваться опубликованной другими хакерами информацией и найти ее слабое место.

Говорят, таким атакам подвержены даже смартфоны и подобные девайсы. Большинство из них, в своей операционной системе данные хранят в SQLLite, а следовательно и сопутствующие неприятности связанные с SQL инъекциями возникнуть могут. Вероятно какой-нибудь samsung galaxy s iv этому не подвержен, но не стоит забывать, что старые устройства никуда не уходят, и подвержены риску.

Методы борьбы с SQL инъекциями

Самым верным способом противодействия данного рода атакам - это экранирование всего и вся. Т.е. все переменные, которые вставляются в код пропускаются через функцию mysql_real_escape_string

mysql_query('select * from news where id="'.mysql_real_escape_string($_GET['id'])).'"';

такой запрос ни чем серьезным, кроме как ошибки в запросе, не грозит. В данном конкретном примере, мы видим, что id может быть исключительно целым числом, поэтому еще более верным решением будет такой код

mysql_query('select * from news where id="'.intval($_GET['id']).'"';

Я не дублирую код для проверки, но его тоже ни в коме случае опускать не стоит. Проще проверить переменную, чем заставлять mysql сервер выполнять лишний запрос.

Рано или поздно такой метод вставки войдет у Вас в привычку. В моем сервисе, как раз такая ошибка и была сделана. В код, просто шла переменная из get запроса.

Со своим кодом разобрались. Тут мы хозяева положения. В более менее крупном проекте, используется больше количество сторонних библиотек или приложений: ckeditor, ckfinder, jquery плагины, сборщики, упаковщики, даунлоудеры, системы статистики, phpmyadmin и т.д.

И у них тоже есть дырки. Править их руками возможности разумеется нет, а если и есть то так делать нежелательно.

Гораздо проще, защитить ресурс в целом. К примеру, в моей любимой CMS Danneo, это реализовано на уровне API и в GET и POST запрос, просто нельзя вставлять ключевые слова для SQL запросов.

/**
 * badcount
 */
$badcount = 0;
/**
 * badops
 */
$badops = array("UNION",
                "OUTFILE",
                "FROM",
                "CREATE",
                "SELECT",
                "WHERE",
                "SHUTDOWN",
                "UPDATE",
                "DELETE",
                "CHANGE",
                "MODIFY",
                "RENAME",
                "RELOAD",
                "ALTER",
                "GRANT",
                "DROP",
                "INSERT",
                "CONCAT",
                "cmd",
                "exec",
                "\([^>]*\"?[^)]*\)",
                "<[^>]*body*\"?[^>]*>",
                "<[^>]*script*\"?[^>]*>",
                "<[^>]*object*\"?[^>]*>",
                "<[^>]*iframe*\"?[^>]*>",
                "<[^>]*img*\"?[^>]*>",
                "<[^>]*frame*\"?[^>]*>",
                "<[^>]*applet*\"?[^>]*>",
                "<[^>]*meta*\"?[^>]*>",
                "<[^>]*style*\"?[^>]*>",
                "<[^>]*form*\"?[^>]*>",
                "<[^>]*div*\"?[^>]*>"
                );
/**
 * foreach REQUEST
 */
foreach ($_REQUEST as $params => $inputdata) {
    for ($i = 0; $i < sizeof($badops); $i++) {
            if (is_string($inputdata) && preg_match('/'.$badops[$i].'/i',$inputdata)) {
                 $badcount = 1;
            }
    }
}

if( $badcount  ){
     header('HTTP/1.1 500 Internal Server Error');
     throw new Exception('Divizion by zerro');
    exit();
}

Вставляем этот код в начало входного скрипта вашей системы и от 99% атак вы защищены. Конечно, такой код налагает определенные ограничения на названия переменных, и если Ваша система очень большая, то вероятно половину проверок вы в нем закомментируете.

Помимо прочего, этот код защищает Вас от других атак, css и т.п.

В результате, если в запросе были запрещенные слова, код вернет ошибку 500 и выкинет исключение. Это не самое лучшее решение. Если весь Ваш код эскейпит переменные, и с этим все в порядке, то будет не лишним оповестить админа о попытках инъектирования, а самому злоумышленнику отдавать к примеру 404.

К примеру, в моем классе надстройке на mysql, я просто дополнил метод escape и если в него попадает данные, которые содержат запрещенные слова, отправляется письмо на ящик администратора сайта, ему присылается ip пользователя, переменная на которую сработала защита.

....
function myIP(){
	$ipa = explode( ',',@$_SERVER['HTTP_X_FORWARDED_FOR'] );
	$ip = isset($_SERVER['HTTP_X_REAL_IP'])?$_SERVER['HTTP_X_REAL_IP']:(isset($_SERVER['REMOTE_ADDR'])?$_SERVER['REMOTE_ADDR']:(!empty($ipa[0])?$ipa[0]:'127.0.0.1'));
	return preg_match('#^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$#',$ip)?$ip:'127.0.0.1';
}

public $checkAttack = true;
public $attackBuffer = array();
public $stopAttack = false;

function isItAttack( $id ){
	if( $this->checkAttack and !in_array($id,$this->attackBuffer) ){
		$hook = 0 ;
		if(	
			(preg_match('/[\']/', $id) and $hook=1) or
			(preg_match('/(and|null|not)/i', $id) and $hook=3) or
			(preg_match('/(union|select|from|where)/i', $id) and $hook=4)or
			(preg_match('/(group|order|having|limit)/i', $id) and $hook=5) or
			(preg_match('/(into|file|case)/i', $id) and $hook=6) or
			(preg_match('/--|#|\/\*/', $id) and $hook=7)
		){
			$this->attackBuffer[] = $id;
			mail('mail@ya.ru','Attack rutaxi.ru',"Site was be attacked - \n\n".
				"value:".$id."\n\n".
				"hook:".$hook."\n\n".
				"ip:".$this->myIP()."\n\n".
				"request:".$_SERVER['REQUEST_URI']."\n\n".
				"REQUEST:".var_export($_REQUEST,true));
			if( $this->stopAttack ){
				header('HTTP/1.1 500 Internal Server Error');
				throw new Exception('Divizion by zerro');
				exit();
			}
			return true;
		}
	}
	return false;
}
function escape($value){
	$this->isItAttack($value);
	return mysql_real_escape_string($value,$this->handle);
}
...

такой скрипт своевременно предупреждает меня, о попытках взлома моего ресурса. Воспользовавшись информацией из письма, можно заблокировать зловредный ip либо изучить те места в которые идет атака.

Вот пожалуй и все меры после которых ваш сайт уже будет не так просто взломать.

Желаю Вам удачи, в нашей опасной профессии)

Оставлять комментарии могут только зарегистрированные пользователи