Инъекция SQL, которая обходит реальную escape-строку mysql()


Существует ли возможность 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 выше?

Author: Brad Larson, 2011-04-21

4 answers

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

$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";
 318
Author: Wesley van Opdorp, 2017-12-16 12:45:41

Короткий ответ: да, да, есть способ обойти 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 здесь. Это устанавливает набор символов НА СЕРВЕРЕ. Если бы мы использовали вызов функции C API 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.20, 5.0.22 и 5.1.11.

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

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

Как мы уже говорили в самом начале, для того, чтобы эта атака сработала, соединение с базой данных должно быть закодировано с использованием уязвимого набора символов. utf8mb4 является не уязвимым и все же может поддерживать каждый символ Юникода: поэтому вы могли бы использовать его вместо этого - но он доступен только с MySQL 5.5.3.An альтернативой является 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 / и т.д.)

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

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

 548
Author: ircmaxell, 2017-05-23 11:55:01

TL;ДР

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

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

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

Это было подано как ошибка #72458 и была исправлена в MySQL версии 5.7.6 (см. раздел, озаглавленный " Спасительная благодать", ниже).

Это еще один (возможно, менее?) неясный КРАЙНИЙ СЛУЧАЙ!!!

В знак уважения к Превосходному ответу @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(), вызывается, если используется режим 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 версии 5.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 безопасны.

Завершение

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

  • используйте собственные подготовленные инструкции

ИЛИ

  • используйте MySQL версии 5.7.6 или более поздней версии

ИЛИ

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

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

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

 142
Author: eggyal, 2017-05-23 12:10:45

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

 19
Author: Slava, 2011-04-21 08:15:22