SQL-инъекция, которая обходит mysql real escape string()

есть ли возможность SQL-инъекции даже при использовании

4 ответов


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

$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";

короткий ответ:да, да, есть способ обойти 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. Или в другом слова, а действительный символ, за которым следует unescaped '. Но мы не используем 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 is не уязвимых и все же может поддерживать символ Unicode: поэтому вы можете использовать это вместо этого-но это было только доступно начиная с MySQL 5.5.3. Альтернативой является utf8, который также не уязвимых и может поддерживать весь Unicode Основной Многоязычный Самолет.

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


TL; DR

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

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

это было подано как ошибка #72458 и был исправлен в MySQL v5.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"');
    

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

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

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

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

    • перед символом кавычки escape-символом ("\").

    • A"'" внутри строки, заключенной в кавычки с "" " не нуждается в специальном лечении и не должен быть удвоен или убежал. В точно так же!"--15-->" внутри строки, заключенной в кавычки с "'" не нуждается в особом лечении.

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

  2. Груз

    " OR 1=1 -- 
    

    полезная нагрузка инициирует эту инъекцию буквально с помощью .

  3. Запрос

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

Некрасиво

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

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

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

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

по этой причине я рекомендую всем, кто использует NO_BACKSLASH_ESCAPES также позволяет ANSI_QUOTES mode, так как это заставит привычное использование одинарных строковых литералов. Обратите внимание, что это не предотвращает 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 подготовленные операторы являются безопасными.

Подводя Итоги

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

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

или

  • используйте MySQL v5.7.6 или позже!--88-->

или

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

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

...тогда ты!--81-->должны быть в полной безопасности (уязвимости снаружи область экранирования строки в сторону).


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