вторник, 20 декабря 2011 г.

Информационная безопасность.

A+ A-

  http://www.esc.lviv.ua/wp-content/uploads/2011/11/%D0%98%D0%BD%D1%84%D0%BE%D1%80%D0%BC%D0%B0%D1%86%D0%B8%D0%BE%D0%BD%D0%BD%D0%B0%D1%8F-%D0%B1%D0%B5%D0%B7%D0%BE%D0%BF%D0%B0%D1%81%D0%BD%D0%BE%D1%81%D1%82%D1%8C.jpg

Детективная история про SQL injection, местами blind

Доброго времени суток!

Не подумал бы писать статью об этом, т.к. думал, что тема довольно заезжена. Но, судя по этой статье аудитории интересно. Окончательно меня убедил в том, что писать надо вот этот комментарий.

Данная история произошла со «знакомым знакомого моего знакомого», но, для краткости буду писать цитатами с его слов, употребляя просто «я». Дело было полторы недели назад. Поехали.

Понадобилось мне изучить один европейский язык, в свете возможного переезда в одну европейскую страну. И нашёл я замечательный сайт, на котором предлагалось учить язык с помощью подкастов. Сами подкасты распространяются бесплатно, но можно купить PDF с записями уроков и упражнениями. Мне эти записи не очень нужны, но моя жена, в отличие от меня, вовсе не аудиал, а язык учить ей тоже надо. Прежде, чем что-то купить в интернете, я внимательно изучаю сайт продавца — не хочется, чтобы мои данные куда-то утекли. И в этом случае все оказалось более чем плохо. Желание покупать что-либо сразу отпало. Но и остаться без PDF неспортивно. В итоге я решил попробовать воспользоваться одной из найденных уязвимостей. Сразу скажу, что я принципиально не пользуюсь никакими автоматическими сканерами уязвимостей и принципиально не причиняю вреда пользователям ресурса — они же не виноваты, что хозяин ресурса коряво его написал. Поэтому моими инструментами являются соображалка и теоретические знания по причинам возникновения и использованию уязвимостей.

Начало

Первым делом я посмотрел на несколько демо-примеров доступных для загрузки PDF. Сначала пользователь отправлялся по адресу:

/guide.php?id=lesson_id

В этот момент проверяется имеет ли текущий пользователь право скачать заданный PDF. Если да — идет редирект по адресу:

/download.php?f=filename.pdf

Сразу же оказалось, что данный скрипт отдает указанный файл ничего не проверяя. Т.к. доступный пример для урока №1 имел имя файла 001.pdf я решил попробовать получить все файлы перебором. Если бы все было так просто, то и писать было бы не о чем. Но таким образом удалось добыть только первые 100 файлов. Остальные имели в своем имени timestamp времени создания и перебирать их стало невозможно, т.к. время создания отличалось на несколько месяцев.

Раскручиваем SQL injection

Довольно скоро обнаружилась банальнейшая SQL injection в GET параметре:

/some_script.php?id=123

Вроде бы дальше ее использование строится очень просто:
  1. Определить количество параметров в запросе
  2. Подобрать имена таблиц и полей (в случае MySQL 5.0 и выше — выбрать их из information_schema)
  3. Получить нужные имена файлов
  4. Скачать сами файлы

Но проблемы начались с первого пункта — определить количество полей в запросе не удалось. При любом количестве полей в UNION SELECT и при любом номере в ORDER BY n я получал сообщение «You have error in your syntax...»

На самом деле я совершенно случайно догадался в чем именно проблема — попробовав сделать GROUP BY 1. На это я получил ошибку «cannot group by cnt». Оказалось, что уязвимый параметр используется дважды (ну по крайней мере это предположение мне опровергнуть не удалось).

Сначала выбирается количество записей с указанным id:

SELECT count(*) FROM table where id=123

Если количество записей 0, то считается, что страница не найдена и происходит редирект на главную страницу. Если записей не 0, вытаскивается информация:

SELECT * FROM table where id=123

Теперь становится понятно, почему не удалось выяснить количество полей в запросе — их 2 и в одном из них всегда окажется неверное количество полей в UNION. Придумать способ, который позволил бы вставить разное количество полей в UNION в первый и второй запрос, мне не удалось. И в этот момент SQL injection стала blind. Мне не удалось подобрать имя таблицы с путями к файлам, но зато удалось подобрать имя таблицы с данными пользователей (MySQL 4.1).

Уважаемые разработчики, не делайте 2 запроса, там где можно сделать один! В этом случае можно было вместо SELECT count(*) проверить количество записей возвращенных запросом SELECT *

Теперь осталось придумать способ получать полезную информацию. Я сделал так:

/script.php?id=123 limit 0,0 union all select length(username)>4 from tablename limit 0,1--

Что мы здесь видим:
  • 123 limit 0,0 — т.к. count(*) всегда вернет нам ровно одну запись и мы не узнаем, что именно было возвращено нашей частью запроса, нужно убрать его из результата
  • union all select length(username)>4 from tablename limit 0,1-- — если длина имени пользователя больше 4, то условие верно, MySQL вернет единицу, а затем ошибку, при попытке выполнить второй запрос. Если условие неверно — вернет 0 и произойдет редирект. Ну и '--' для комментария в конце

Таким образом по HTTP заголовку можно понять, верное ли условие мы передали. Сначала определяем длину имени пользователя, затем побуквенно двоичным поиском вытаскиваем само имя ( lower(substr(username,1,1)) in ('a','b','c') ). Затем побуквенно вытаскиваем пароль. Но оказывается, что пароль хеширован в md5. И хотя хэширование без соли, но пароли администраторов сайта все равно подобрать не удалось (в rainbow tables нет, а брутфорсом на нетбуке заниматься не хотелось; да и неспортивно это).

После некоторых раздумий было решено идти другим путем. Т.к. в базе оказалось более 60000 пользователей я предположил, что у многих из них популярные пароли. А дальше нужно было всего лишь побуквенно вытащить имена пользователей у которых хэш пароля равен md5('password') — их оказалось больше 100 и среди них были люди купившие нужные PDF. И они любезно согласились ими со мной поделиться.

Все это было сделано с помощью очень простого скрипта, который отправлял HEAD-запрос (а зачем нам тело страницы?) и смотрел заголовок ответа. Если 200 — условие верно, если 302 — неверно.

Заключение

Зачем все это написано? Чтобы показать, что нужно знать суть и причины возникновения уязвимостей, а не заучивать способы их использования. Все способы использования SQL injection, которые я видел в интернете, предлагали определить количество полей через ORDER BY 5 или UNION SELECT 1,2,3… И человек, не захотевший подумать, ушел бы с сайта ни с чем.

Кроме того, я слегка горд своим обходным маневром вместо взлома хэша. Ну и не так давно высказывался скептицизм по поводу существования таких уязвимостей в современном интернете и по поводу практического применения blind SQL injection.

P.S. Все совпадения с реальностью являются случайными. Голоса знаменитостей сымитированы, причем убого.
19 декабря 2011, 20:51
21

комментарии (17)

+3
Raz0r #
Все можно было сделать гораздо быстрее, использовав вместо перебора двоичным поиском, метод с выводом результата запроса прямо в текст ошибки: rdot.org/forum/showpost.php?p=1913&postcount=8
0
Graphite #
Очень интересный способ, спасибо. Тем не менее на указанном моим знакомым сайте получаю «The used SELECT statements have a different number of columns»

Или я что-то не понимаю, или в 4.1.21 не работает так
0
Raz0r #
должно работать в этой версии, попробуйте также такие варианты: rdot.org/forum/showpost.php?p=18212&postcount=15
0
Graphite #
Странно, ни один не сработал. Хотя я понимаю, что должны работать. Давайте в личку скину сайт? Может это я что-то не так делаю просто?
+1
Graphite #
Все работает, это просто я спать хочу уже
0
Fedcomp #
я не могу вникнуть почему значение поля выводится, объясните?
0
Raz0r #
все из-за этого: bugs.mysql.com/bug.php?id=8652
This problem happens because in a GROUP BY query a RAND expression can be evaluated
several times for the same row, every time returning a new result.
+2
andrewsch #
> Уважаемые разработчики, не делайте 2 запроса, там где можно сделать один!
> В этом случае можно было вместо SELECT count(*) проверить количество записей
> возвращенных запросом SELECT *

Меня это дело тоже всегда бесило — то, что надо посылать два запроса. Но иначе-то нельзя (в типовых базах данных, с которыми я работал), так как нельзя вытаскивать все 100500 записей — сервер не резиновый — нужно вытаскивать по-странично. А знать общее количество нужно — чтобы прокрутку или страничную навигацию правильно проинициализировать.
+1
youlose #
А как же SQL_CALC_FOUND_ROWS?
0
datacompboy #
Это вызывает фактически выборку всех строк, включая все сложные join'ы мержи и прочие радости.
Тогда как count(*) можно сделать с сильно упрощенным вариантом запроса.
0
youlose #
Ну в этом случае тоже надо постоянно следить чтобы данные упрощённого запроса совпадали, в общем, везде свои нюансы. Просто «андреич» так категорично заявил… неверно это…
0
guyfawkes #
Но ведь и SELECT FOUND_ROWS() придется сделать, или не так?
0
youlose #
хах, и то верно
+1
andrewsch #
Дык это в мускуле. А в постгресе? А еще в какой базе?
0
Mavim #
а смысл для одной таблицы? Запросов все равно 2, а также интересные сравнения.
0
humbug #
Процедуры, не?
+1
Graphite #
Ну я имел в виду конкретный случай — выборка одной записи по primary key. Зачем делать count(*) если точно знаешь, что запись либо одна, либо ни одной.

0 коммент.:

Отправить комментарий

Последние комментарии

Twitter Delicious Facebook Digg Stumbleupon Favorites More

 
blogger