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

Apr 21 2011

Есть ли возможность 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?

Ответы

393 WesleyvanOpdorp Apr 21 2011 at 15:05

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

$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";
652 ircmaxell Aug 25 2012 at 09:08

Короткий ответ - да, есть способ обойти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. Как выясняется, есть 5 таких кодировок , поддерживаемых в MySQL 5.6 по умолчанию: big5, cp932, gb2312, gbkи sjis. Выберем gbkздесь.

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

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

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

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

  3. mysql_real_escape_string ()

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

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

    縗 'ИЛИ 1 = 1 / *

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

  4. Запрос

    Это всего лишь формальность, но вот обработанный запрос:

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

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

Плохо

Становится хуже. PDOпо умолчанию эмулирует подготовленные операторы с MySQL. Это означает, что на стороне клиента он в основном выполняет sprintf through 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.20 , 5.0.22 и 5.1.11 .

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

Спасительная благодать

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

Кроме того, вы можете включить NO_BACKSLASH_ESCAPESрежим SQL, который (помимо прочего) изменяет работу 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() ...

190 eggyal Apr 25 2014 at 02:15

TL; DR

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

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

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

Это было зарегистрировано как ошибка № 72458 и была исправлена ​​в MySQL v5.7.6 (см. Раздел под заголовком « Благодать экономии » ниже).

Это еще один (а может, и менее?) Непонятный 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"');
    

    Как описано в разделе Строковые литералы :

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

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

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

    • Перед кавычкой ставьте escape-символ (« \»).

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

    Если режим 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(), если используется NO_BACKSLASH_ESCAPESрежим SQL , вызывается другая базовая функция . Как упоминалось выше, такая функция должна знать, какой символ будет использоваться для цитирования литерала, чтобы повторить его, не вызывая буквального повторения другого символа кавычки.

    Однако эта функция произвольно предполагает, что строка будет заключена в кавычки с использованием 'символа одинарной кавычки . Смотрите 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()-it как будто не миновать не произошли вообще .

  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(), поскольку это просто другая оболочка для этой же функции.

Проблема, если она еще не очевидна, заключается в том, что вызов to 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 эта ошибка исправлена. См. Журнал изменений :

Functionality Added or Changed

  • Incompatible Change: A new C API function, mysql_real_escape_string_quote(), has been implemented as a replacement for mysql_real_escape_string() because the latter function can fail to properly encode characters when the NO_BACKSLASH_ESCAPES SQL mode is enabled. In this case, mysql_real_escape_string() cannot escape quote characters except by doubling them, and to do this properly, it must know more information about the quoting context than is available. mysql_real_escape_string_quote() takes an extra argument for specifying the quoting context. For usage details, see mysql_real_escape_string_quote().

     Note

    Applications should be modified to use mysql_real_escape_string_quote(), instead of mysql_real_escape_string(), which now fails and produces an CR_INSECURE_API_ERR error if NO_BACKSLASH_ESCAPES is enabled.

    References: See also Bug #19211994.

Safe Examples

Taken together with the bug explained by ircmaxell, the following examples are entirely safe (assuming that one is either using MySQL later than 4.1.20, 5.0.22, 5.1.11; or that one is not using a GBK/Big5 connection encoding):

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');

...because we've explicitly selected an SQL mode that doesn't include 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");

...because we're quoting our string literal with single-quotes.

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

...because PDO prepared statements are immune from this vulnerability (and ircmaxell's too, provided either that you're using PHP≥5.3.6 and the character set has been correctly set in the DSN; or that prepared statement emulation has been disabled).

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

...because PDO's quote() function not only escapes the literal, but also quotes it (in single-quote ' characters); note that to avoid ircmaxell's bug in this case, you must be using PHP≥5.3.6 and have correctly set the character set in the DSN.

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

...because MySQLi prepared statements are safe.

Wrapping Up

Thus, if you:

  • use native prepared statements

OR

  • use MySQL v5.7.6 or later

OR

  • in addition to employing one of the solutions in ircmaxell's summary, use at least one of:

    • PDO;
    • single-quoted string literals; or
    • an explicitly set SQL mode that does not include NO_BACKSLASH_ESCAPES

...then you should be completely safe (vulnerabilities outside the scope of string escaping aside).

19 Slava Apr 21 2011 at 15:01

Well, there's nothing really that can pass through that, other than % wildcard. It could be dangerous if you were using LIKE statement as attacker could put just % as login if you don't filter that out, and would have to just bruteforce a password of any of your users. People often suggest using prepared statements to make it 100% safe, as data can't interfere with the query itself that way. But for such simple queries it probably would be more efficient to do something like $login = preg_replace('/[^a-zA-Z0-9_]/', '', $login);