На главном сервисе моей основной работы произошел сбой. Надо сказать, весьма критичный для нашей компании.
Разбор логов показал, что была проведена 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 либо изучить те места в которые идет атака.
Вот пожалуй и все меры после которых ваш сайт уже будет не так просто взломать.
Желаю Вам удачи, в нашей опасной профессии)