Сервер в кармане, или просто о сложном!

главная - Статьи - WWW (HTML, PHP и др.)

Атакуем БД сайта

Аатака на mysql

Спецвыпуск: Хакер, номер #052, стр. 052-084-1

Что может сделать взломщик используя SQL injection

С каждым днем все больше скриптов используют базы данных, все больше хостингов доверяют пароли своих клиентов SQL-базам, все больше популярных сайтов переходит на публичные форумы и движки, работающие с MySQL. Но далеко не все ясно представляют себе, насколько опасным может быть непродуманное использование MySQL в скриптах.

Как это есть?

Без знаний основ языка SQL трудно что-либо понять. Прежде всего разберемся, в чем заключается суть атаки типа SQL injection. К примеру, на атакуемом сервере стоит следующий PHP-скрипт, который на основе поля category_id делает выборку заголовков статей из таблицы articles и выводит их пользователю:

//подключаемся к MySQL

mysql_connect($dbhost, $dbuname, $dbpass) or die(mysql_error());

mysql_select_db($dbname) or die(mysql_error());

$cid=$_GET["cid"];

$result=mysql_query("SELECT article_id, article_title FROM articles where category_id=$cid"); // <- уязвимый запрос

while( $out = mysql_fetch_array( $result)):

echo "Статья: ".$out['article_id']." ".$out['article_title']."<br>";

endwhile;

//выводим результат в виде списка

В переводе с языка MySQL запрос звучит так: "ВЫБРАТЬ ид_статей, заголовки_статей ИЗ таблицы_статей ГДЕ ид_категории равно $cid". На первый взгляд все верно, по ссылке типа http://serv.com/read.php?cid=3 скрипт работает нормально и выводит пользователю список статей, принадлежащих категории 3.

Но что если пользователь - никакой не пользователь, а обыкновенный хакер? Тогда он сделает запрос http://serv.com/read.php?cid=3' (именно с кавычкой) и получит что-то вроде: Warning: mysql_fetch_array(): supplied argument is not a valid MySQL result resource in /usr/local/apache/htdocs/read.php on line 14.

Почему ошибка? Посмотрим, что запросил PHP у MySQL. Переменная $cid равна 1', тогда запрос принимает неверный с точки зрения MySQL вид: SELECT article_id, article_title FROM articles where category_id=1'. При синтаксической ошибке в запросе MySQL отвечает строкой "ERROR 1064: You have an error in your SQL syntax...". PHP не может распознать этот ответ и сообщает об ошибке, на основе которой хакер может судить о присутствии уязвимости типа SQL Injection. Очевидно, что злоумышленник получит возможность задавать переменной $cid любые значения ($cid=$_GET[cid]) и, следовательно, модифицировать запрос к MySQL. Например, если $cid будет равна "1 OR 1" (без кавычек в начале и в конце), то MySQL выдаст все записи, независимо от category_id, так как запрос будет иметь вид (..) where category_id=1 OR 1. То есть либо category_id = 1 (подойдут лишь записи с category_id, равными 1), либо 1 (подойдут все записи, так как число больше нуля - всегда истина).

Только что описанные действия как раз и называются SQL Injection - иньекция SQL-кода в запрос скрипта к MySQL. С помощью SQL Injection злоумышленник может получить доступ к тем данным, к которым имеет доступ уязвимый скрипт: пароли к закрытой части сайта, информация о кредитных картах, пароль к админке и т.д. Хакер при удачном для него стечении обстоятельств получит возможность выполнять команды на сервере.

Как атакуют?

Классический пример уязвимости типа SQL Injection - следующий запрос: SELECT * FROM admins WHERE login='$login' AND password=MD5('$password').

Допустим, он будет проверять подлинность введенных реквизитов для входа в админскую часть какого-нибудь форума. Переменные $login и $password являются логином и паролем соответственно, и пользователь вводит их в HTML-форму. PHP посылает рассматриваемый запрос и проверяет: если количество возвращенных от MySQL записей больше нуля, то админ с такими реквизитами существует, а пользователь авторизуется, если иначе (таких записей нет и логин/пароль неверные) - пользователя направят на fsb.ru.

Как взломщик использует SQL Injection в этом случае? Все элементарно. Злоумышленнику требуется, чтобы MySQL вернул PHP-скрипту хотя бы одну запись. Значит, необходимо модифицировать запрос так, чтобы выбирались все записи таблицы независимо от правильности введенных реквизитов. Вспоминаем фишку с "OR 1". Кроме того, в MySQL, как и в любом языке, существуют комментарии. Комментарии обозначаются либо --комментарий (комментарий в конце строки), либо /*комментарий*/ (комментарий где угодно). Причем если второй тип комментария стоит в конце строки, закрывающий знак '*/' необязателен. Итак, взломщик введет в качестве логина строку anyword' OR 1/*, а в качестве пароля - anyword2. Тогда запрос принимает такой вид: SELECT * FROM admins WHERE login='anyword' OR 1/* AND password=MD5('anyword2'). А в переводе на человеческий язык: ВЫБРАТЬ все ИЗ таблицы_admins ГДЕ логин равен 'anyword' ИЛИ 1, а остальное воспринимается как комментарий, что позволяет отсечь ненужную часть запроса. В результате MySQL вернет все записи из таблицы admins даже независимо от того, существует админ с логином anyword или нет, и скрипт пропустит хакера в админку. Такая уязвимость была обнаружена, например, в Advanced Guestbook. Она позволяла войти в администраторскую часть не зная пароля и внутри нее читать файлы. Но SQL Injection этого типа обычно не позволяют злоумышленнику получить данные из таблицы.

Union и MySQL версии 4

Вернемся к скрипту получения заголовков статей. На самом деле он позволяет взломщику получить гораздо больше, чем список всех статей. Дело в том, что в MySQL версии 4 добавлен новый оператор - UNION, который используется для объединения результатов работы нескольких команд SELECT в один набор результатов. Например: SELECT article_id, article_title FROM articles UNION SELECT id, title FROM polls. В результате MySQL возвращает N записей, где N - количество записей из результата запроса слева плюс количество записей из результата запроса справа. И все это в том порядке, в каком идут запросы, отделяемые UNION.

Но существуют некоторые ограничения по использованию UNION:

1. число указываемых столбцов во всех запросах должно быть одинаковым: недопутимо, чтобы первый запрос выбирал, например, id, name, title, а второй только article_title;

2. типы указываемых столбцов одного запроса должны соответствовать типам указываемых столбцов остальных запросов: если в одном запросе выбираются столбцы типа INT, TEXT, TEXT, TINYTEXT, то и в остальных запросах должны выбираться столбцы такого же типа и в таком же порядке;

3. UNION не может идти после операторов LIMIT и ORDER.

Так как же UNION может стать пособником злоумышленника? В нашем скрипте присутствует запрос "SELECT article_id, article_title FROM articles where category_id=$cid". Что мешает хакеру, используя SQL injection, вставить еще один SELECT-запрос и выбрать нужные ему данные? Правильно: ничего!

Допустим, цель хакера - получить логины и пароли всех авторов, которые могут добавлять статьи. Есть скрипт чтения списка статей http://serv.com/read.php?cid=1, подверженный SQL injection. Первым делом хакер узнает версию MySQL, с которой работает скрипт. Для этого он сделает следующий запрос: http://serv.com/read.php?cid=1+/*!40000+AND+0*/. Если скрипт вернет пустую страницу, значит, версия MySQL >= 4. Почему именно так? Число 40000 - версия MySQL, записанная без точек. Если версия, которая стоит на сервере, больше или равна этому числу, то заключенный в /**/ код выполнится как часть запроса. В результате ни одна запись не подойдет под запрос и скрипт не вернет ничего. Зная версию MySQL, хакер сделает вывод о том, сработает фишка с UNION или нет. В случае если MySQL третьей версии, фишка работать не будет. В нашем случае MySQL >= 4 и злоумышленник все-таки воспользуется UNION.

Для начала взломщик составит верный UNION-запрос, то есть подберет действительное количество указываемых столбцов, которое бы совпало с количеством указываемых столбцов левого запроса (вспоминай правила работы с UNION). Хакер не имеет в распоряжении исходников скрипта (если, конечно, скрипт не публичный) и поэтому не знает, какой именно запрос шлет скрипт к MySQL. Придется подбирать вручную - модифицировать запрос вот таким образом: http://serv.com/read.php?cid=1+UNION+SELECT+1. И тут о своем присутствии объявит ошибка, так как количество запрашиваемых столбцов не совпадает. Хакер увеличивает количество столбцов еще на единицу: http://serv.com/read.php?cid=1+UNION+SELECT+1,2 - получает список статей из категории 1, а также в самом конце две цифры: 1 и 2. Следовательно, он верно подобрал запрос.

Посмотрим на модифицированный запрос от PHP к MySQL: SELECT article_id, article_title FROM articles where category_id=1 UNION SELECT 1,2. В ответ MySQL возвращает результат первого SELECT (список статей) и результат второго SELECT - число "1" в первом столбце и "2" во втором столбце (SELECT+1,2). Другими словами, теперь, подставляя вместо '1' и '2' реальные имена столбцов из любой таблицы, можно будет заполучить их значения.

Составив верный SELECT+UNION запрос, хакер постарается подобрать название таблицы с нужными ему данными. Например, таблица с данными пользователей будет, скорее всего, называться users, Users, accounts, members, admins, а таблица с данными о кредитных картах - cc, orders, customers, orderlog и т.д. Для этого злоумышленник сделает следующий запрос: http://serv.com/read.php?cid=1+UNION+SELECT+1,2+FROM+users. И если таблица users существует, то PHP-скрипт выполнится без ошибок и выведет список статей плюс '1 2', иначе - выдаст ошибку. Так можно подбирать имена таблиц до тех пор, пока не будет найдена нужная.

В нашем случае "нужная" таблица – это authors, в которой хранятся данные об авторе: имя автора, его логин и пароль. Теперь задача хакера - подобрать правильные имена столбцов с нужными ему данными, чаще всего с логином и паролем. Имена столбцов он станет подбирать по аналогии с именем таблицы, то есть для логина столбец, скорее всего, будет называться login или username, а для пароля - password, passw и т.д. Запрос будет выглядеть так: http://serv.com/read.php?cid=1+UNION+SELECT+1,login+from+authors.

Почему хакер не стал вставлять имя столбца вместо единицы? Ему нужна текстовая информация (логин, пароль), а в нашем случае в левом запросе SELECT на первом месте идет article_id, имеющий тип INT. Следовательно, в правом запросе хакер не может ставить на первое место имя столбца с текстовой информацией (правила UNION).

Итак, выполнив запрос http://serv.com/read.php?cid=1+UNION+SELECT+1,login+from+authors, взломщик находит список логинов всех авторов, а подставив поле password - список паролей. И получает желанные логины и пароли авторов, а админ сервера – подмоченную репутацию. Но это только в нашем примере Фортуна улыбнулась злоумышленнику так широко: он быстро подобрал количество столбцов, а в реальной жизни количество столбцов может достигать 30-40.

UNION и нюансы

Теперь рассмотрим некоторые ситуации, в которых использование UNION затруднено по тем или иным причинам.

Ситуация 1

Левый запрос возвращает лишь числовое значение. Что-то вроде SELECT code FROM artciles WHERE id = $id. Что будет делать хакер? Средства MySQL позволяют проводить различные действия над строками, к примеру, выделение подстроки, склеивание нескольких строк в одну, перевод из CHAR в INT и т.п. Благодаря этим функциям хакер имеет возможность выудить интересующую его информацию по одному символу. К примеру, требуется достать пароль из таблицы admins, используя приведенный выше запрос. Чтобы получить ASCII-код первого символа пароля, сделаем следующий запрос к скрипту: http://127.0.0.1/read.php?cid=1+union+select+ASCII(SUBSTRING(password,1,1))+from+admins. Функция SUBSTRING(name,$a,$b) в MySQL выделяет $b символов из значения столбца name начиная с символа под номером $a. Функция ASCII($x) возвращает ASCII-код символа $x. Для получения последующих символов следует просто менять второй параметр функции SUBSTRING до тех пор, пока ответом не будет 0. Подобный способ был использован в эксплойте для одной из версий phpBB.

Ситуация 2

SQL Injection находится в середине SQL-запроса. Например: SELECT code FROM artciles WHERE id = $id AND blah='NO' AND active='Y' LIMIT 10. Для правильной эксплуатации хакер просто откомментирует идущий следом за Injection код, то есть к вставляемому коду добавит /* или --. Пробелы в запросе взломщик может заменить на /**/, что полезно в случае если скрипт фильтрует пробелы.

Ситуация 3

Случается и такое, что в PHP-коде подряд идет несколько SQL-запросов, подверженных Injection. И все они используют переменную, в которую злоумышленник вставляет SQL-код. Например (опускаю PHP):

$result=mysql_query("SELECT article_id, article_title FROM articles where category_id=$cid");

//php code here

$result=mysql_query("SELECT article_name FROM articles where category_id=$cid");

//тут вывод результата

Это довольно неприятно для хакера, так как для первого запроса SQL Injection пройдет нормально, а для второго UNION - уже нет, так как количество запрашиваемых столбцов отличается. И если программист, писавший код, предусмотрел остановку скрипта в случае ошибки типа "... or die("Database error!")", то эксплуатация обычными методами невозможна, так как скрипт остановится раньше, чем будет выведет результат.

Ситуация 4

Скрипт выводит не весь результат запроса, а, например, только первую запись. И если хакер будет прямо пользоваться UNION, то скрипт выдаст только первую запись из ответа MySQL, а остальное отбросит, в том числе результат SQL Injection. Для того чтоб преодолеть все препятствия и на этом этапе, хакер передаст левому запросу такой параметр для WHERE, чтобы в ответ на него MySQL не вернул ни одной записи.

Например, есть такой запрос: SELECT name FROM authors WHERE id=$id. После SQL Injection он будет выглядеть следующим образом: (..) id=1 UNION SELECT password FROM authors. Но PHP-скрипт выведет только первую запись, поэтому вставляемый код следует модифицировать: (..) id=-12345 UNION SELECT (..). Теперь в ответ на левый запрос MySQL не вернет ничего, а в ответ на правый - желанные для хакера данные.

Ситуация 5

Скрипт не выводит результат запроса. Например, есть скрипт, который выводит какие-либо статистические данные, например, количество авторов, принадлежащих к определенной группе. Причем количество записей он считает не с помощью MySQL-функции COUNT, а в самом скрипте. Скрипт шлет MySQL такой запрос: SELECT id FROM authors where category_id=$cid.

Допустим, скрипт возвращает что-то вроде "Найдено десять авторов в данной категории". В этом случае злоумышленник будет эксплуатировать SQL injection, конечно же, методом перебора символов! Например, хакеру надо получить пароль автора с id = 1, для чего потребуется перебирать каждый символ пароля. Но как получить символ, если PHP не выводит ничего из того, что возвратил MySQL?

Рассмотрим такой запрос: SELECT id FROM authors where category_id=-1 UNION SELECT 1,2 FROM authors WHERE id=1 AND ASCII(SUBSTRING(password,1,1))>109. Результатом запроса будет одна запись, если ASCII-код первого символа пароля больше 109, и ноль записей, если больше, либо равна. Итак, методом бинарного поиска нетрудно найти нужный символ. Почему хакер использует знаки "больше/меньше", а не "равно"? Если взломщику надо получить 32-символьный хэш пароля, ему придется делать примерно 32*25 запросов! Метод бинарного поиска позволяет сократить это число в два раза. Само собой, делать запросы хакер будет уже не руками, а с помощью скрипта, автоматизирующего перебор.

MySQL версии 3

Несмотря на отсутствие в третьей версии оператора UNION, и из нее хакер сможет вытащить то, чем интересуется. В осуществлении этого замысла помогут подзапросы и перебор символов, но описание этого метода займет еще пару листов (которых мне не дали). Поэтому ищи статьи на эту тему на www.rst.void.ru (автор 1dt.w0lf) и www.securitylab.ru (автор Phoenix).

Как защищаться?

Правило №1. Фильтруй входные данные. Кавычку заменяй на слеш-кавычку('), слеш - на слеш-слеш. В PHP это делается или включением magic_quotes_gpc в php.ini, или функцией addslashes(). В Perl: $id=~s/(['\])/\$1/g;. И на всякий случай: $id=~s/[a-zA-z]//g; - для числовых параметров.

Правило №2. Не дай кому не надо внедрить SQL-код! Заключай в кавычки все переменные в запросе. Например, SELECT * FROM users WHERE id='$id'.

Правило №3. Отключи вывод сообщений об ошибках. Некоторые программисты, наоборот, делают так, что при ошибке скрипт выводит сообщение самого MySQL, или, еще ужасней, - ВЕСЬ SQL-запрос. Это предоставляет злодею дополнительную информацию о структуре базы и существенно облегчает эксплуатацию.

Правило №4. Никогда не разрешай скриптам работать с MySQL от root. Ничего хорошего не выйдет, если хакер получит доступ ко всей базе.

Правило №5. Запускай публичные скрипты от отдельного пользователя с отдельной базой. Неприятно будет, если какой-нибудь кидди, воспользовавшись 0day-дырой в форуме, получит доступ к базе с СС твоих клиентов.

Правило №6. Отключи MySQL-пользователю привилегию FILE - не дай хакеру записать в файл что-то вроде <?system($_GET[cmd])?> через MySQL.

Правило №7. Не называй таблицы и базы данных в соответствии с их назначением, чтоб утаить от чужих глаз настоящие названия. В публичных скриптах часто предоставляют возможность установить prefix для названия таблиц - устанавливай самый сложный. Если кто-нибудь и найдет SQL injection, то не сможет ее эксплуатировать.

Чаще всего уязвимости оставляют в тех запросах, параметры которых передаются через hidden формы в HTML и через cookies, видимо, из-за того, что они не видны пользователю и не так привлекают внимание злодеев.

Часто забывают про SQL Injection в функции Reply, о поиске сообщений пользователя в форумах, в репортах различных сервисов. В 80% WAP-сервисов SQL injection находят по десять штук в каждом скрипте (наверное, админы думают, что туда только через сотовые ходят). На самом деле многие недооценивают SQL Injection. Известен случай, когда обычная SQL injection в скрипте репорта привела к реальному руту на трех серверах и дампу гиговой базы. А всего-то SQL Injection…

Статьи по теме

inattack.ru/group_article/34.html
www.rst.void.ru/papers/sql-inj.txt
www.securitylab.ru/49424.html
www.securitylab.ru/49660.html

Все чаще администраторы получают возможности убедиться в том, что знания по безопасности запросов к MySQL не менее важны, чем эффективное использование этих запросов.

Защитить свою базу от хакеров можно – нужно только грамотно следовать определенным правилам по нейтрализации подобных атак.

С помощью UNION хакер может легко узнать пользователя, базу данных и версию MySQL, для чего используются функции user(), database() и version() соответственно. Взломщик просто сделает запрос типа SELECT user().

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

Чем ты больше знаешь о том, как ломают, тем проще предотвратить взлом.

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

По материалам сайта http://forum.pyccxak.com/

Авторизуйтесь для добавления комментариев!


    забыли пароль?    новый пользователь?