SQL-инъекция, которая обходит mysql_real_escape_string () - PullRequest
571 голосов
/ 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 выше?

Ответы [ 4 ]

591 голосов
/ 25 августа 2012

Короткий ответ: да, да, есть способ обойти 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 здесь.Это устанавливает набор символов ON SERVER .Если бы мы использовали вызов функции C API mysql_set_charset(), все было бы хорошо (в версиях MySQL с 2006 года).Но подробнее о том, почему через минуту ...

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

    Полезная нагрузка, которую мы собираемся использовать для этой инъекции, начинается споследовательность байтов 0xbf27gbk это недопустимый многобайтовый символ;в 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.20 , 5.0.22 и 5.1.11 .

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

The Grace Saving

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

В качестве альтернативы,Вы можете включить режим SQL NO_BACKSLASH_ESCAPES, который (среди прочего) изменяет работу mysql_real_escape_string().Если этот режим включен, 0x27 будет заменен на 0x2727 вместо 0x5c27, и, таким образом, процесс выхода не может создавать допустимые символы в любой из уязвимых кодировок, где они ранее не существовали (т. Е. 0xbf27 по-прежнему 0xbf27 и т. Д.), Поэтому сервер все равно отклонит строку как недействительную.Однако см. @ eggyal's answer о другой уязвимости, которая может возникнуть при использовании этого режима 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% в безопасности.

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

343 голосов
/ 21 апреля 2011

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

$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";
158 голосов
/ 24 апреля 2014

TL; DR

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

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

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

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

Это еще один, (возможно, менее?) Неясный случай КРАЯ !!!

В знак уважения к превосходному ответу @ 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, то третий из этих параметров & ndash; это обычный подход, принятый mysql_real_escape_string() & ndash; недоступен: один из первых двух вариантов должен быть используется вместо Обратите внимание, что эффект четвертого маркера заключается в том, что нужно обязательно знать символ, который будет использоваться для кавычек литерала, чтобы избежать манипулирования данными.

  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() & mdash; как будто никакого побега не произошло вообще .

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

Проблема, если она еще не очевидна,s * то, что вызов 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(), поскольку последняя функция не может правильно кодировать символы, когда NO_BACKSLASH_ESCAPES Режим SQL включен.В этом случае 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 и правильно установили набор символов в DSN.

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

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

Завершение

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

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

OR

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

OR

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

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

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

19 голосов
/ 21 апреля 2011

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...