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