Новая альтернатива Benchmark'y или эффективный blind SQL-injection В статье Вам будет показан новый и универсальный способ обходиться без использования benchmark в blind sql-inj в так называемых в народе "неюзабельных" бажных запросах как UPDATE, DELETE, REPLACE, UPDATE и прочих. Найденный Альтернативный способ позволяет именно ПОЛУЧАТЬ требуемую информацию из базы данных, а не банально модифицировать записи. Будут описаны все особенности известных способов атак SQL-inj, применимых к перечисленным выше операторам. А также мы подробно рассмотрим аспекты практического использования бенчмарка при написании эксплойтов. Примечание: в статье приведены примеры для mysql, однако опиcанное сработает практичеcки на всех субд(с поправкой на синтаксис запросов, разумеется). [1] INTRO Программисты умнеют, багов в селект-запросах становится всё меньше, а проблема инъекций в INSERT,UPDATE,REPLACE,DELETE и прочих остаётся актуальной. Часто вижу вопросы на форумах: "..подскажите, что мне делать, если - 'mysql error INSERT INTO table_name VALUES(...'" Видя такую ошибку, новичок в хаке закроет браузер, любитель постит сотый вопрос на форумах... В ответ как правило тишина, либо советы которыми он не в силах воспользоваться, поскольку раскрутка blind sql, ввиду объёма запросов, требует применения автоматизации, то есть умения писать эксплоит. Способы давно известны и подробно расписаны 1dt.w0lf ещё за 14 ноября 2004, не читал её только ленивый: http://www.securitylab.ru/contest/212099.php Данную статью можно считать её продолжением. [2] INSERT и другие описаны известные атаки на insert\update и тд, кто знает - пропускаем Что можно из этого выжать? 1) (случай общий)Модификация данных без возможности просмотра результата Идеальный случай - sql-inj в имени таблицы. Здеcь возможно всё: Code: INSERT INTO [COLOR=Red][SQL][/COLOR] VALUES ('stat','stat2'); Добавление нового пользователя: Code: INSERT INTO [COLOR=Red]mysql.user (user, host, password) VALUES ('newadmin', 'localhost', PASSWORD('passwd'))/*[/COLOR] VALUES ('stat'); 2) DoS-атака Но подобные sql-inj в 99% присутствуют в имени столбца, что не позволяют изменить модифицируемую таблицу на полезную нам. Code: INSERT INTO table VALUES ('stat','[COLOR=Red][SQL][/COLOR]'); А раз так, то кроме порчи\засорения\dosa бд мы модификацией данных ничего не добьемся. SQL: Code: INSERT INTO table VALUES ('stat','[COLOR=Red]bla'),('test', 'demo[/COLOR]'); DoS: Code: INSERT INTO table VALUES ('stat','[COLOR=Red]bla' and BENCHMARK(10000000,BENCHMARK(10000000,md5(now()))) ) /*[/COLOR]'); 3) Разделение запросов в MSSQL, PostgreSql, Oracle Что же объединяет эти три популярные субд? Такая замечательная вещь, как поддержка разделения запросов, используя точку с запятой, перевод каретки и прочее. Таким образом мы можем добавить абсолютно ЛЮБОЙ sql-запрос, отделив его ";" от основного. Что можно полезного вставить - ищем в гугле под конкретную бд. 4) Модификация данных с возможностью просмотра результата Здесь уже не важно, где инъекция - в имени таблицы или столбца. Мы можем увидеть результат выполнения запроса, пускай и косвенно. А значит провести атаку не проблема - чисто технические особенности выяснения результата. Пример: пусть веб-приложение ПУБЛИЧНО ведёт статистику посещений и есть уязвимость в столбце с User-Agent. Тогда, результат выполнения sql-inj мы можем наблюдать на паге вывода статистики посещаемости. 5) Использование BENCHMARK Мы используем BENCHMARK, что позволяет по времени ответа сервера определить результат выполнения запроса. Этот запрос будет для if положителен и ответ от сервера придёт сразу: Code: INSERT INTO table VALUES ('stat','[COLOR=Red]1' and 1=if(ascii(lower(substring((select users from mysql.user limit 1),1,1)))>=1,1,benchmark(999999,md5(now()))) )/*[/COLOR] ,'stat2'); А этот - отрицателен и на исполнение бенчмарка уйдёт время, потому ответ от сервера придёт сразу: Code: INSERT INTO table VALUES ('stat','[color=red]1' and 1=if(ascii(lower(substring((select users from mysql.user limit 1),1,1)))>=254,1,benchmark(999999,md5(now()))) )/*[/color] ,'stat2'); Разумеется, вместо INSERT может стоять любой другой оператор, в том числе и SELECT, ведь с ним тоже бывают cложные случаи инъекций. [3] Bencmark для хакера особенности эксплуатации benchmark, кто знает - пропускаем Применение бенчмарка это своего рода переход ко второму измерению. От измерения разности получаемой информации к измерению разности времени на исполнение запроса. Если кто-то сомневается, что под бенчмарк вообще пишут эксплойты - пробегитесь по багтракам и уверьтесь в обратном - они есть, правда не много. В реализации сплойт на бенчмарке чуть-чуть сложнее чем для слепого посимвольного брута. Особенности benchmark: - пожалуй самая важная деталь - бенчмарк создаёт серьёзную нагрузку на процессор сервера. Причём эта нагрузка длиться практически постоянно во время работы эксплойта. Администратор может не смотреть логи обращений\ошибок, но вполне может поинтересоваться, почему сервер притормаживает и в top->P "mysqld" занимает первое место... - время работы эксплойта тем больше, чем большую длину записи мы хотим получить. На моей практике для перебора 32-х символьного хеша уходит больше часа. Иногда несколько часов - по обстоятельствам, описанным ниже. - взломщику и серверу требуется широкий надежный канал. От этого зависит качество и стабильность брута. Я не говорю, что на диалапе у вас ничего не получится. Но глюков будет больше, особенно, если вы, с целью экономии времени, выберите слишком маленький параметр... - параметр измерения производительности, то есть количество итераций, в нашем примере - 999999 benchmark(999999,md5(user()) По личному опыту, с момента написания статьи 1dt.w0lf'ом это число изменилось почти на порядок вместе с ростом производительности современных серверов. В эксплойте желательно делать автоподстройку этого параметра для универсальности, добиваясь приемлемого времени ответа.... - и соответственно, подобрав число в benchmark'e нужно задать таймаут ответа - timeout Это будет среднее арифметическое от времени выполнения true\false запроса. - Замечу: при написании эксплойта учитывайте, что по статистике общее число "неверных" запросов превышает общее число верных, а значит нужно поставить бенчмарк-задержку так, чтобы она срабатывала при ВЕРНОМ ответе. Вот так: Code: if( ?, true, false ) 1 = if( 1=1 , benchmark(999999,md5(user()), 1 ) Тем самым мы сэкономим время и снизим нагрузку на сервер. - не забывайте, что серверу необходимо дать отдых после каждого бенчмарка, дать восстановиться так сказать. Иначе следующий запрос может иметь непредвиденное время выполнения и даст вам ошибочные данные. Об этой маленькой детали часто забывают, поскольку тестят двиг локально, либо непонятно о чём думают, релизя свой код без предварительного тестирования. Сам лично долго удивлялся, непонимая, почему первый символ брутится правильно, но дальше идёт мусор - сервер просто давился бенчмарком. Желательно выбирать время задержки в расчете на 1-1,5 больше, чем время исполнения бенчмарка. Да, это существенно замедлит брут, но обеспечит качество результата. Если обратится непосредственно к коду, то символьный брутер 1dt.w0lf'а без труда модифицируется с учетом бенчмарка: необходимо - изменить sql под бенчмарк Code: $http_query = $path."?Cat=&page=1&like=".$username."' AND 1=if(ascii(substring(CONCAT(U_LoginName,CHAR(58),U_Password),".$s_num.",1))".$ccheck.",benchmark(999999,md5(user()),1)/*"; - timeout - таймаут. Но вот возвращать sub check($) теперь должна результат в зависимости от того, уложился ли сервер в таймаут, или нет. Code: $mcb_reguest = LWP::UserAgent->new(timeout=>$timeout) or die; - sleep() После каждого ложного запроса, а значит подвергнутого пытке бенчмарком, дадим серверу отдохнуть N секунд. пусть восстановится. вот и всё. [4] Альтернатива benchmark'y ну вот, собственно, самое сладкое Копаясь с бенчмарком меня как и любого из Вас одолевало желание найти замену этому геморойному, но единственному способу. И способ был найден! На самом деле разность выводимой инфы и времени - не единственные факторы. Давайте вернёмся к началу и вспомним, с чего начинается любая sql-inj? Конечно с ковычки в запросе! ...и выводе сообщения ошибки соответственно. Но подойдем к этому логически - мы ставим ковычку, mysql проверяет запрос, находит в нём ошибку - сообщает нам. Следите за мыслью: мускул сначала проверяет на корректность, затем либо проводит запрос и выводит результат, либо выводит еррор. А теперь зададим себе вопрос - может ли быть иначе? Ну, разумеется может, в том то всё и дело. Может возникнуть ситуация, когда проверку на корректность запрос пройдет, выполнится,.. но результат даст нам сообщение об ошибке. Тогда наша задача сводится к простейшей ПРОВАКАЦИИ такого запроса + необходимо связать параметры в запросе таким образом, чтобы мы могли манипулировать результатом запроса и соответственно получать полезные данные. Ставим задачу: Срыв запроса неявным условием и как следствие - намеренный вызов ошибки, которая и поможет отличить true\false query. Эта мысль приходит в голову и я начинаю вспоминать различные ошибки бд. Сначала попытался используя IF сорвать запрос путем подстановки неверных имён таблиц\столбцов\типов данных: Code: select if(1=1,null,blaaaah); Однако mysql, сцуко, умный и проверяет тип данных перед выполнением на корректность, что меня обломало. You have an error in your SQL syntax Попытка изменять имя не столбца, а таблицы также не увенчалась успехом. Code: select null from if(1=1,users,blaaaah); Кстати, видимо алгоритм парсинга запроса в мускуле на корректность предполагает имя таблицы как константы, а здесь она не задана явно. Он проверяет существование столбца в заведомо известной таблице и если таблица не определена, например я через if её задаю, то мускул шлёт меня подальше. Принцип проверки в других базах данных я не проверял, так что возможно такие простые, а главное, универсальные трюки там прокатят. Какие ещё ошибки могут возникнуть при выполнении запроса? Самой популярной после "You have an error in your SQL syntax" является пожалуй "The used SELECT statements have a different number of columns" Но здесь проверка так же происходит до выполнения. "Operand should contain 1 column(s)" - опять же нельзя по той же причине. "warning: mysql_fetch_array..." - можно, но php-ошибка специфическая, под конкретный двиг. ..а следующая по популярности в моём рейтинге идёт "Subquery returns more than 1 row".... Да, да, он самый, вездесущий лимит. Вот то, что нам нужно. Мускул проверяет корректность данных, но не может проверить число возвращаемых ответов, не сделав самого запроса. Встаёт вопрос, как запрос надо задать, чтобы вероятно вызвать такую ошибку? Требуется спровоцировать два условия: когда ошибка 100% выведется и 100% не выведется. Предположим, что нам заведомо известны имена таблиц и столбцов в уязвимом приложении. Пусть пароль хешируется md5. Тогда длинна любого пароля = 32 символа. А длина id или логина наверняка будет меньше, не правда ли?... length(id)=(1-5..) и length(password)=(32) Тогда наш эксплоит будет выглядеть следующим образом: false: Code: ...123' and 1=(select null from users where length(if(ascii(substring((select password from users where uid=1),1,1))>254,password,uid))>5)/* Первый символ пароля наверняка меньше чем ascii(254). Значит if возвратит length(id)>5, а так как столько пользователей наверняка нет(ну можно циферку и побольше брать), то селект возвратит NULL. В результате мы ничего не увидим, будто и нет никакой инъекции. true: Code: ...123' and 1=(select null from users where length(if(ascii(substring((select password from users where uid=1),1,1))>1,password,uid))>5)/* Поскольку первый символ пароля наверняка больше ascii(1), то условие истинно Значит if возвратит length(password)>5, что приведёт к выводу N результатов в селект запросе. Но в условие может быть сравнен только один результат 1=(*),... что спровоцирует ошибку "Subquery returns more than 1 row" !!! Как всё просто, не правда ли? Уверен, что поискав найдутся и другие подобные ошибки, проявляющиеся именно после выполнения query. Особенности "more than 1 row": 1) важнейшее: стандартная скорость посимвольного брута при полной заменяемости бенчмарка, экономим время и не грузим сервер. 2) цена этой производительности - масса ерроров в логах mysql, что может привлечь внимание администратора. 3) единственно, требуется знать некоторые данные, как то - имена столбцов и таблиц, чтобы провоцировать ошибку "more than 1 row" Возможно, вы найдете более совершенный и универсальный способ, в отличии от моего. Удачи! [5] Links Ссылки. Публичные эксплойты, использующие benchmark blind SQL-inj : 08 марта, 2007 г. PHP-Nuke <= 8.0 Final (INSERT) Blind SQL Injection Exploit (mysql) 08 марта, 2007 г. PHP-Nuke <= 8.0 Final (HTTP Referers) Remote SQL Injection Exploit 08 марта, 2007 г. PHP-Nuke <= 8.0 Final (INSERT) Remote SQL Injection Exploit 21 февраля 2007 г. Blind sql injection attack in INSERT syntax on PHP-nuke <=8.0 Final 1 декабря 2006 г. Invision Gallery 2.0.7 SQL Injection Vulnerability 8 сентября 2006 г. PHPFusion <= 6.01.4 extract()/_SERVER[REMOTE_ADDR] sql injection exploit 25 июля 2006 г. SQL-Injection in Shop-Script PRO & Shop-Script Premium all version 3 мая 2006 г. sBlog SQL Injection and Path Disclosure Vulnerability 24 марта 2004 г. MS Analysis v2.0 module for PhpNuke MS Analysis 10 февраля 2004 г. SQL injection in Php-Nuke 7.1.0
Сразу скажу, я обещал пару месяцев назад статью для .uT!L!Te. Статья будет отдана ему для публикации где-то там. Как только он свой езин выпустит, либо по наступлении апреля - я спускаю её в паблик.
езин вышел (http://www.pyccxak.com/goch_1/), а значит статью пора спускать. Наслаждайтесь оригиналом - форматирование текста не урезанное, читать приятнее =)
статья отличная. ток ещё ошибки подправить надо: говорилось что следовательно После каждого Верного запроса, а значит подвергнутого пытке бенчмарком, дадим серверу отдохнуть N секунд. пусть восстановится.
================================= Как совершенно верно заметил podkashey мне в асю - "CREATE, DROP" притянуты зауши, так как не держат после себя условий. Потому насчет этих функций я прогнал - извините. поправил. Ну, расстраиваться собственно неочем - крейт с дропом используются в основном только при инсталяции. ================================= Как совершенно верно заметил товарищ ZaCo, Что же объединяет эти три популярные субд? Такая замечательная вещь, как поддержка разделения запросов точкой с запятой... - "...вообще говоря, ';' не так чтобы и разделитель двух запросов... два запроса можно разделить исполльзуя ЛЮБОЙ разделитель в том числе пробел\перевод каретки табуляция и тд..." поправил. ================================= Всего через два часа после публикации алгоритма podkashey нашел более универсальный метод провакации основаный на union. select 1 union select 2 куда это вставить, сами подумайте =) а плюсы можно ставить ему же =) ================================= Это сказал ему я =))) Спасибо за редактирование и правку грамматики, для меня это было не столь важно. На самом деле нет ничего удивительного в том, что актуальность материала начинает доходит до всех спустя некоторое время. Материал специфисеский и осознать его полезность сразу может лишь тот, кто не по наслышке знаком с реализацией сабжа. Фактически, это маленький прорыв в sql-inj, своего рода полезнейшая фича, без которой было очень неудобно.
Insert, Update, Replace При попытке присвоить колонке значение NULL, которая не может быть равна NULL, вызывает ошибку: (Column '[column]' cannot be null) Пример: INSERT INTO table (`a`,`b`,`c`) VALUES ('1',if(1=1,NULL,'2'),'3')
Можно еще добавть, что с помощью таких выражений возможна инъекция после order by, т.к. в order by тоже возможно использование выражений (вот в limit помоему нельзя) возможен вариант: SELECT * FROM `table1` order by (select+if(2=1,1,(select 1 union select id))) в данном случае выдаст ошибку , т.к 2=1 - false если подставить выражение 1=1 ошибки не будет пример SELECT * FROM `table1` order by (select+if(substring(version(),1,1)=4,1,(select 1 union select id))) если версия мускула 4 ошибки не будет, если не 4 то будет ошибка. ну и класика жанра: SELECT * FROM `table1` order by -id*(substring(version(),1,1)=4) если версия мускула 4, то сортировка пойдет по полю ид в обратном порядке, т.к. -id*1 (aka true) будет -id, а -id*0 (ака false) будет 0, ну и с бенчмарком тоже самое из всех предложенных вариантов вариант с иф - бенчмарк самый универсальный, но самый долгий
статья стоящая, жаль что была проигнорирована большинством. добавлю к сообщению Scipio - твой способ подходит исключительно для операции select, предлагаю вариант, который может использоваться в любых логических условиях и без использования подзапросов: Code: select username from users where "x" regexp concat("x{1,25", if(@@version<>5, "5}", "6}")) /*в случае else строка выражения выйдет за максимальный предел квантификатора*/ естественно методов "провокации" в регекспах довольно много, но достаточно только одного.
чето у меня не выдает ошибки ни как если вместо @@version использую колонку типа: просто пустое значение возвращает ( что я делаю не так?
если подставить выражение 1=1 ошибки не будет пример SELECT * FROM `table1` order by (select+if(substring(version(),1,1)=4,1,(select 1 union select id))) если версия мускула 4, то сортировка пойдет по полю ид в обратном порядке, т.к. -id*1 (aka true) будет -id, а -id*0 (ака false) будет 0, ну и с бенчмарком тоже самое из всех предложенных вариантов вариант с иф - бенчмарк самый универсальный, но самый долгий
некоторые альтернативные запросы вызывающие more1row показаны в докладе(слайд 25) http://www.slideshare.net/sumsid1234/owasp-au-rev4?type=powerpoint
ну как бы работает, или ты не об этом ? Code: mysql> select 1 from information_schema.tables where (select 1,2,3)=1; ERROR 1241 (21000): Operand should contain 3 column(s)
Дык суть то не в том, чтобы ошибка нарисовалась, а в том чтобы она в if'е сработала. А эта ошибка отображается всегда, и в случае true и в случае false.
В продолжение темы насчет ошибки "Operand should contain n column(s)": http://websec.wordpress.com/2009/11/26/mysql-table-and-column-names-update-2/ В посте идет речь о новом способе получения имен колонок, если недоступна БД information_schema. Сначала необходимо определить количество колонок с помощью способа, который упоминался выше: Code: ' AND (SELECT * FROM USER_TABLE) = (1)-- - -> Operand should contain 7 column(s) Теперь используя UNION и 1%0 можно извлечь имена колонок через ошибку: Code: ' AND (1,2,3,4,5,6,7) = (SELECT * FROM USER_TABLE UNION SELECT 1%0,2,3,4,5,6,7 LIMIT 1)-- - -> Column 'usr_u_id' cannot be null Последовательно перебирая колонки, можно получить имя каждой, если она имеет параметр NOT NULL. Кроме того, как заметил автор, этот баг был исправлен в MySQL 5.1. По запросу видно, что будет работать только с MySQL >= 4.1, хотя и это надо проверить.
Кстати, если взглянуть на старый метод с benchmark'ом, я не очень понимаю, зачем надо было использовать именно его, ведь можно использовать конструкцию с функцией sleep(). Code: IF(1=2, 1, SLEEP(1)) Так нагрузки не будет на сервер.