it-swarm.com.ru

SQL-инъекция, которая обходит mysql_real_escape_string ()

Есть ли возможность внедрения SQL-кода даже при использовании функции mysql_real_escape_string()?

Рассмотрим этот пример ситуации. SQL построен в PHP следующим образом:

$login = mysql_real_escape_string(GetFromPost('login'));
$password = mysql_real_escape_string(GetFromPost('password'));

$sql = "SELECT * FROM table WHERE login='$login' AND password='$password'";

Я слышал, как многие люди говорили мне, что подобный код все еще опасен и его можно взломать даже с использованием функции mysql_real_escape_string(). Но я не могу думать ни о каком возможном подвиге?

Классические инъекции, как это:

aaa' OR 1=1 --

не работай.

Знаете ли вы о возможных инъекциях, которые могли бы пройти через код PHP выше?

566
Richard Knop

Рассмотрим следующий запрос:

$iId = mysql_real_escape_string("1 OR 1=1");    
$sSql = "SELECT * FROM table WHERE id = $iId";

mysql_real_escape_string() не защитит вас от этого. То, что вы используете одинарные кавычки (' ') вокруг переменных внутри запроса, защищает вас от этого. Следующее также является опцией:

$iId = (int)"1 OR 1=1";
$sSql = "SELECT * FROM table WHERE id = $iId";
340
Wesley van Opdorp

Краткий ответ да, да, есть способ обойти mysql_real_escape_string().

Для очень скрытых краев!

Длинный ответ не так прост. Он основан на атаке продемонстрировано здесь .

Атака

Итак, начнем с показа атаки ...

mysql_query('SET NAMES gbk');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

При определенных обстоятельствах это вернет более 1 строки. Давайте рассмотрим, что здесь происходит:

  1. Выбор набора символов

    mysql_query('SET NAMES gbk');
    

    Чтобы эта атака работала, нам нужно, чтобы кодировка, ожидаемая сервером для соединения, кодировала ', как в ASCII, т.е. 0x27 и , чтобы иметь некоторый символ, чей последний байт представляет собой ASCII \ т.е. 0x5c. Как оказалось, в MySQL 5.6 по умолчанию поддерживается 5 таких кодировок: big5, cp932, gb2312, gbk и sjis. Мы выберем gbk здесь.

    Теперь очень важно отметить использование SET NAMES здесь. Это устанавливает набор символов на сервере . Если бы мы использовали вызов функции API C mysql_set_charset(), у нас все было бы в порядке (в версиях MySQL с 2006 года). Но подробнее о том, почему через минуту ...

  2. Полезная нагрузка

    Полезная нагрузка, которую мы собираемся использовать для этой инъекции, начинается с последовательности байтов 0xbf27. В gbk это недопустимый многобайтовый символ; в latin1 это строка ¿'. Обратите внимание, что в latin1 и gbk, 0x27 сам по себе является буквальным символом '.

    Мы выбрали эту полезную нагрузку, потому что, если бы мы вызвали на нее addslashes(), мы вставили бы ASCII \, т.е. 0x5c, перед символом '. Таким образом, мы получим 0xbf5c27, который в gbk представляет собой двухсимвольную последовательность: 0xbf5c, за которой следует 0x27. Или, другими словами, допустимый символ, за которым следует неоткрытый '. Но мы не используем addslashes(). Итак, к следующему шагу ...

  3. mysql_real_escape_string ()

    Вызов API C в mysql_real_escape_string() отличается от addslashes() в том, что он знает набор символов соединения. Таким образом, он может выполнить экранирование правильно для набора символов, который ожидает сервер. Однако до этого момента клиент думал, что мы все еще используем latin1 для соединения, потому что мы никогда не говорили об этом иначе. Мы сказали серверу , что используем gbk, но клиент по-прежнему считает, что это latin1.

    Поэтому вызов mysql_real_escape_string() вставляет обратную косую черту, и у нас есть свободно висящий символ ' в нашем "экранированном" контенте! Фактически, если бы мы посмотрели на $var в наборе символов gbk, мы бы увидели:

    _ 'OR 1 = 1/*

    Что именно то, что атака требует.

  4. Запрос

    Эта часть просто формальность, но вот обработанный запрос:

    SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
    

Поздравляем, вы только что успешно атаковали программу с помощью mysql_real_escape_string()...

Плохо

Становится хуже. PDO по умолчанию эмулирует подготовленные операторы с MySQL. Это означает, что на стороне клиента он в основном выполняет sprintf через mysql_real_escape_string() (в библиотеке C), что означает, что следующее приведет к успешному внедрению:

$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

Теперь стоит отметить, что вы можете предотвратить это, отключив эмулированные подготовленные операторы:

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

Это обычно приводит к истинно подготовленному утверждению (то есть к данным, отправляемым в отдельном пакете от запроса). Однако имейте в виду, что PDO будет молча отступление эмулировать утверждения, которые MySQL не может подготовить изначально: те, которые могут быть перечислены в руководстве, но будьте осторожны, чтобы выбрать подходящий версия сервера).

Гадкий

Я сказал в самом начале, что мы могли бы предотвратить все это, если бы использовали mysql_set_charset('gbk') вместо SET NAMES gbk. И это правда, если вы используете версию MySQL с 2006 года.

Если вы используете более раннюю версию MySQL, то ошибка в mysql_real_escape_string() означало, что недопустимые многобайтовые символы, такие как символы в нашей полезной нагрузке, обрабатывались как одиночные байты для экранирования , даже если клиент был правильно проинформирован о кодировке соединения , и поэтому эта атака все равно будет успешной. Ошибка была исправлена ​​в MySQL 4.1.2 , 5.0.22 и 5.1.11 .

Но хуже всего то, что PDO не выставлял API C для mysql_set_charset() до 5.3.6, поэтому в предыдущих версиях он не мог предотвратить эту атаку для каждого возможная команда! Теперь он отображается как параметр DSN .

Спасительная Грация

Как мы уже говорили, чтобы эта атака работала, соединение с базой данных должно быть закодировано с использованием уязвимого набора символов. utf8mb4 не является уязвимым и все же может поддерживать каждый символ Unicode: так что вы можете использовать его вместо этого - но он был доступен только с MySQL 5.5.3. Альтернативой является utf8 , который также не уязвим и может поддерживать весь Unicode Базовая многоязычная плоскость .

В качестве альтернативы вы можете включить режим SQL NO_BACKSLASH_ESCAPES , который (среди прочего) изменяет действие функции mysql_real_escape_string(). Если этот режим включен, 0x27 будет заменен на 0x2727, а не 0x5c27, и, таким образом, экранирующий процесс не может создавать допустимые символы в любой из уязвимых кодировок, где они ранее не существовали (то есть 0xbf27) по-прежнему 0xbf27 и т. д.), поэтому сервер все равно будет отклонять строку как недействительную. Однако см. ответ @ eggyal , чтобы узнать о другой уязвимости, которая может возникнуть при использовании этого режима SQL.

Безопасные Примеры

Следующие примеры безопасны:

mysql_query('SET NAMES utf8');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

Поскольку сервер ожидает utf8...

mysql_set_charset('gbk');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

Потому что мы правильно установили набор символов, чтобы клиент и сервер совпадали.

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

Потому что мы отключили эмулированные подготовленные заявления.

$pdo = new PDO('mysql:Host=localhost;dbname=testdb;charset=gbk', $user, $password);
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

Потому что мы установили правильный набор символов.

$mysqli->query('SET NAMES gbk');
$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "\xbf\x27 OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();

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

Завершение

Если ты:

  • Используйте современные версии MySQL (последняя версия 5.1, все версии 5.5, 5.6 и т.д.) Иmysql_set_charset()/$mysqli->set_charset()/параметр набора символов DSN PDO (в PHP ≥ 5.3.6)

ИЛИ

  • Не используйте уязвимый набор символов для кодирования соединения (вы используете только utf8/latin1/ascii/etc)

Вы на 100% в безопасности.

В противном случае вы уязвимы , даже если вы используете mysql_real_escape_string() ...

583
ircmaxell

TL; DR

mysql_real_escape_string() не обеспечит никакой защиты (и, кроме того, может испортить ваши данные), если:

  • MySQL NO_BACKSLASH_ESCAPES режим SQL включен (что может быть , если вы явно не выбрали другой режим SQL каждый раз при подключении ); а также

  • строковые литералы SQL заключаются в двойные кавычки ".

Это было подано как ошибка # 72458 и было исправлено в MySQL v5.7.6 (см. Раздел " The Saving Grace ", ниже).

Это еще один, (возможно, менее?) Непонятный Edge CASE !!!

В знак уважения к отличный ответ @ ircmaxell (действительно, это должно быть лестью, а не плагиатом!), Я приму его формат:

Атака

Начиная с демонстрации ...

mysql_query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"'); // could already be set
$var = mysql_real_escape_string('" OR 1=1 -- ');
mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');

Это вернет все записи из таблицы test. Расслоение:

  1. Выбор режима SQL

    mysql_query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"');
    

    Как указано в Строковые литералы :

    Есть несколько способов включить символы кавычки в строку:

    • "'" внутри строки, заключенной в "'", может быть записан как "''".

    • """ внутри строки, заключенной в """, может быть записан как """".

    • Перед символом кавычки должен стоять символ перехода ("\").

    • "'" внутри строки, заключенной в кавычки с """, не требует специальной обработки и не нуждается в удвоении или экранировании. Точно так же """ внутри строки, заключенной в "'", не требует специальной обработки.

    Если режим SQL сервера включает NO_BACKSLASH_ESCAPES , то третий из этих параметров - обычный подход, принятый mysql_real_escape_string() - недоступен: вместо него следует использовать один из первых двух параметров. Обратите внимание, что эффект четвертого маркера заключается в том, что необходимо обязательно знать символ, который будет использоваться для цитирования литерала, чтобы избежать манипулирования данными.

  2. Полезная нагрузка

    " OR 1=1 -- 
    

    Полезная нагрузка инициирует это внедрение буквально с символом ". Нет конкретной кодировки. Никаких специальных символов. Никаких странных байтов.

  3. mysql_real_escape_string ()

    $var = mysql_real_escape_string('" OR 1=1 -- ');
    

    К счастью, mysql_real_escape_string() проверяет режим SQL и соответствующим образом корректирует его поведение. Смотрите libmysql.c :

    ulong STDCALL
    mysql_real_escape_string(MYSQL *mysql, char *to,const char *from,
                 ulong length)
    {
      if (mysql->server_status & SERVER_STATUS_NO_BACKSLASH_ESCAPES)
        return escape_quotes_for_mysql(mysql->charset, to, 0, from, length);
      return escape_string_for_mysql(mysql->charset, to, 0, from, length);
    }
    

    Таким образом, другая базовая функция escape_quotes_for_mysql() вызывается, если используется режим SQL NO_BACKSLASH_ESCAPES. Как упомянуто выше, такая функция должна знать, какой символ будет использоваться для кавычек литерала, чтобы повторять его, не вызывая повторения буквально другого символа кавычки.

    Однако эта функция произвольно предполагает , что строка будет заключена в кавычки с использованием символа одинарной кавычки '. Смотрите charset.c :

    /*
      Escape apostrophes by doubling them up
    
    // [ deletia 839-845 ]
    
      DESCRIPTION
        This escapes the contents of a string by doubling up any apostrophes that
        it contains. This is used when the NO_BACKSLASH_ESCAPES SQL_MODE is in
        effect on the server.
    
    // [ deletia 852-858 ]
    */
    
    size_t escape_quotes_for_mysql(CHARSET_INFO *charset_info,
                                   char *to, size_t to_length,
                                   const char *from, size_t length)
    {
    // [ deletia 865-892 ]
    
        if (*from == '\'')
        {
          if (to + 2 > to_end)
          {
            overflow= TRUE;
            break;
          }
          *to++= '\'';
          *to++= '\'';
        }
    

    Таким образом, он оставляет нетронутыми " символов в двойных кавычках (и удваивает все символы ' в одинарных кавычках) независимо от фактического символа, который используется для кавычек литерала ! В нашем случае $var остается в точности тем же, что и аргумент, который был предоставлен mysql_real_escape_string() - как будто никакого экранирования не произошло вообще .

  4. Запрос

    mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');
    

    Нечто формальное, обработанный запрос:

    SELECT * FROM test WHERE name = "" OR 1=1 -- " LIMIT 1
    

Как сказал мой ученый друг: поздравляю, вы только что успешно атаковали программу, используя mysql_real_escape_string()...

Плохо

mysql_set_charset() не может помочь, так как это не имеет ничего общего с наборами символов; также не может mysqli::real_escape_string() , поскольку это просто другая оболочка для этой же функции.

Проблема, если она еще не очевидна, заключается в том, что вызов mysql_real_escape_string() не может знать , с каким символом будет заключен литерал в кавычки, так как это оставлено на усмотрение разработчика. решить позже. Таким образом, в режиме NO_BACKSLASH_ESCAPES буквально нет никакой возможности , чтобы эта функция могла безопасно экранировать каждый ввод для использования с произвольными кавычками (по крайней мере, не без удваивающих символов, которые не требуют удвоения и таким образом копая ваши данные).

Гадкий

Становится хуже. NO_BACKSLASH_ESCAPES может быть не таким уж редким явлением в силу необходимости его использования для совместимости со стандартным SQL (например, см. раздел 5.3 спецификация SQL-92 , а именно производство грамматики <quote symbol> ::= <quote><quote> и отсутствие какой-либо особое значение придается обратной косой черты). Кроме того, его использование было явно рекомендуется в качестве обходного пути к (давно исправленному) ошибка , которое описывает пост ircmaxell. Кто знает, некоторые администраторы БД могут даже настроить его на включение по умолчанию, чтобы не использовать неправильные методы экранирования, такие как addslashes() .

Кроме того, режим SQL для нового соединения устанавливается сервером в соответствии с его конфигурацией (которую пользователь SUPER может изменить в любое время); таким образом, чтобы быть уверенным в поведении сервера, вы должны всегда явно указывать желаемый режим после подключения.

Спасительная Грация

Пока вы всегда явно устанавливаете режим SQL, чтобы не включать NO_BACKSLASH_ESCAPES, или заключали в кавычки строковые литералы MySQL, используя символ одинарных кавычек, эта ошибка не может привести к появлению уродливой головы: соответственно escape_quotes_for_mysql() будет не будет использоваться, или его предположение о том, какие символы кавычек требуют повторения, будет правильным.

По этой причине я рекомендую всем, кто использует NO_BACKSLASH_ESCAPES, также включить режим ANSI_QUOTES , так как это приведет к обычному использованию строковых литералов в одинарных кавычках. Обратите внимание, что это не предотвращает внедрение SQL-кода в случае использования литералов в двойных кавычках - оно просто снижает вероятность этого (поскольку нормальные, не вредоносные запросы не будут работать).

В PDO его эквивалентная функция PDO::quote() и вызов подготовленного им эмулятора оператора mysql_handle_quoter() - это именно так: он гарантирует, что экранированный литерал заключается в одинарные кавычки, поэтому Вы можете быть уверены, что PDO всегда защищен от этой ошибки.

В MySQL v5.7.6 эта ошибка была исправлена. Смотрите журнал изменений :

Функциональность добавлена ​​или изменена

  • Несовместимое изменение: Новая функция C API, mysql_real_escape_string_quote() , была реализован в качестве замены для mysql_real_escape_string() , поскольку последняя функция не может правильно кодировать символы, когда включен режим SQL NO_BACKSLASH_ESCAPES . В этом случае mysql_real_escape_string() не может экранировать символы кавычек, кроме как путем их удвоения, и для правильного выполнения он должен знать больше информации о контексте цитирования, чем доступно. mysql_real_escape_string_quote() принимает дополнительный аргумент для указания контекста цитирования. Подробнее об использовании см. mysql_real_escape_string_quote () .

    Заметка

    Приложения должны быть изменены, чтобы использовать mysql_real_escape_string_quote() вместо mysql_real_escape_string() , которая теперь не работает и выдает ошибку CR_INSECURE_API_ERR , если NO_BACKSLASH_ESCAPES включен.

    Ссылки: См. Также ошибка # 19211994.

Безопасные Примеры

Взятые вместе с ошибкой, объясненной ircmaxell, следующие примеры полностью безопасны (при условии, что каждый использует MySQL более поздний, чем 4.1.20, 5.0.22, 5.1.11; или не использует кодировку соединения GBK/Big5) :

mysql_set_charset($charset);
mysql_query("SET SQL_MODE=''");
$var = mysql_real_escape_string('" OR 1=1 /*');
mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');

... потому что мы явно выбрали режим SQL, который не включает NO_BACKSLASH_ESCAPES.

mysql_set_charset($charset);
$var = mysql_real_escape_string("' OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

... потому что мы цитируем наш строковый литерал одинарными кавычками.

$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(["' OR 1=1 /*"]);

... потому что подготовленные операторы PDO защищены от этой уязвимости (и ircmaxell тоже, при условии, что вы используете PHP≥5.3.6 и набор символов был правильно задан в DSN; или эмуляция подготовленного оператора была отключена) ,.

$var  = $pdo->quote("' OR 1=1 /*");
$stmt = $pdo->query("SELECT * FROM test WHERE name = $var LIMIT 1");

... потому что функция PDO quote() не только экранирует литерал, но и заключает его в кавычки (в одинарных кавычках ' символов); обратите внимание, что во избежание ошибки ircmaxell в этом случае вы должны использовать PHP≥5.3.6 и правильно установить набор символов в ДСН.

$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "' OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();

... потому что подготовленные MySQLi заявления безопасны.

Завершение

Таким образом, если вы:

  • использовать нативно подготовленные высказывания

ИЛИ

  • использовать MySQL v5.7.6 или новее

ИЛИ

  • в добавление к использованию одного из решений в резюме ircmaxell, используйте по крайней мере одно из:

    • PDO;
    • строковые литералы в одинарных кавычках; или же
    • явно установленный режим SQL, который не включает NO_BACKSLASH_ESCAPES

... тогда вы должны быть полностью безопасными (уязвимости, выходящие за рамки выхода строки).

155
eggyal

Ну, на самом деле нет ничего, что может пройти через это, кроме символа %. Это может быть опасно, если вы используете оператор LIKE, поскольку злоумышленник может ввести в качестве логина только %, если вы не отфильтровываете его, и вам придется просто взломать пароль любого из ваших пользователей. Люди часто предлагают использовать подготовленные операторы, чтобы сделать их на 100% безопасными, так как данные не могут таким образом вмешиваться в сам запрос. Но для таких простых запросов, вероятно, было бы более эффективно сделать что-то вроде $login = preg_replace('/[^a-zA-Z0-9_]/', '', $login);

19
Slava

я сталкивался с этим, и я предлагаю вам поработать с PDO, но в некоторых случаях вы можете попробовать этот метод. Это работает и очень просто. Интересно, почему люди пренебрегали этим?.

Пример кода. // Используя мой фреймворк Moorexa

он поддерживает богатый ORM и многое другое. Но я должен был выполнить этот контрольный пример, чтобы быть уверенным в альтернативе людям, которые пишут необработанные SQL-выражения.

пример.

// checking from a user table
$check = DB::table('api_users')->get(['username' => "admin' or password='1'"])->run();

// expected output
SELECT * FROM api_users  WHERE username='admin' or password='1'

//sql generated output
SELECT * FROM api_users  WHERE username='admin\' or password=\'1\''

// lets try something heavy
$check = DB::table($table)->get(['username' => "admin' or 1=1 UNION SELECT password FROM api_users where id=1"])->run();

// expected output
SELECT * FROM api_users  WHERE username='admin' or 1=1 UNION SELECT password FROM api_users where id=1

// this would pass and fail
SELECT * FROM api_users  WHERE username='admin\' or 1=1 UNION SELECT password FROM api_users where id=1'

так в чем суть.

  1. Тип проверки.
  2. подтвердить ввод пользователя. <ДОВЕРЯЙТЕ НЕТ ОДНОМУ>
  3. кавычки для строк

я покажу вам пример кода, выполняющего запрос выбора.

// let's assume. would all work
$input = ['username' => "moorexa"]; //or $_POST or $_GET

$sql = 'SELECT * FROM '.$table.' ';

$safe = "";

// let's grab the user input from the array
foreach ($input as $key => $val)
{
    switch($val)
    {
        case is_string($val):
            $safe .= $key .'=\''.addslashes($val).'\' AND ';
        break;

        case is_int($val):
            $safe .= $key .'='.((int) $val).' AND ';
        break;

        case is_float($val):
        case is_double($val):
            $safe .= $key .'='.(double) $val.' AND ';
        break;

        default:
            // this failed
    }
}

$safe = rtrim($safe, "AND ");
$sql .= ' WHERE '. $safe .' ';  

// now sql contains a valid statement. and would only fail when terms are not met.
// Hope you can apply this and also use more test cases.
0
Ifeanyi Amadi