Есть ли возможность 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()
даже при mysql_real_escape_string()
функции mysql_real_escape_string()
. Но я не могу придумать никакого возможного эксплойта?
Классические инъекции:
aaa' OR 1=1 --
не работает.
Знаете ли вы о какой-либо возможной инъекции, которая будет проходить через код PHP выше?
Рассмотрим следующий запрос:
$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 строки. Давайте проанализируем, что здесь происходит:
Выбор набора символов
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 года). Но больше о том, почему через минуту …
Полезная нагрузка
Полезная нагрузка, которую мы будем использовать для этой инъекции, начинается с байтовой последовательности 0xbf27
. В gbk
это недопустимый многобайтовый символ; в latin1
это строка ¿'
. Обратите внимание, что в latin1
и gbk
0x27
по себе является буквенным символом.
Мы выбрали эту полезную нагрузку, потому что, если мы назовем ее addslashes()
, мы 0x5c
ASCII \
ie 0x5c
перед символом '
. Итак, мы закончили с 0xbf5c27
, который в gbk
представляет собой последовательность из двух символов: 0xbf5c
за которой следует 0x27
. Или, другими словами, действительный символ, за которым следует неизолированный '
. Но мы не используем addslashes()
. Итак, на следующий шаг …
mysql_real_escape_string ()
Вызов C API для mysql_real_escape_string()
отличается от addslashes()
тем, что он знает набор символов соединения. Таким образом, он может правильно выполнить экранирование набора символов, ожидаемого сервером. Однако до этого момента клиент думает, что мы все еще используем latin1
для соединения, потому что мы никогда не говорили об этом иначе. Мы сказали серверу, что мы используем gbk
, но клиент все еще думает, что это latin1
.
Поэтому вызов mysql_real_escape_string()
вставляет обратную косую черту, и у нас есть свободный висячий символ в нашем «экранированном» контенте! На самом деле, если бы мы посмотрели на $var
в gbk
символов gbk
, мы увидели бы:
縗 'OR 1 = 1 / *
Это именно то, что требует атака.
Запрос
Эта часть является просто формальностью, но вот вынесенный запрос:
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
не mysql_set_charset()
API C для mysql_set_charset()
до 5.3.6, поэтому в предыдущих версиях он не может предотвратить эту атаку для каждой возможной команды! Теперь он отображается как параметр DSN .
Как мы уже говорили, для этой атаки для работы соединение с базой данных должно быть закодировано с использованием уязвимого набора символов. utf8mb4
не уязвим и все же может поддерживать каждый символ Юникода: поэтому вы можете использовать его вместо этого, но он доступен только с 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_set_charset()
/ $mysqli->set_charset()
/ PDO для DSN (в PHP ≥ 5.3.6) ИЛИ
utf8
/ latin1
/ ascii
/ etc) Вы на 100% безопасны.
В противном случае вы уязвимы, хотя вы используете mysql_real_escape_string()
…
TL; DR
mysql_real_escape_string()
будет обеспечивать никакой защиты (и, кроме того, может делить ваши данные), если:
Режим MySQL
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
таблицы. Рассеяние:
Выбор режима SQL
mysql_query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"');
Как описано в String Literals :
Существует несколько способов включения символов кавычек в строку:
A "
'
внутри строки, указанной в'
", может быть записано как''
.
"
" Внутри строки, указанной в ""
", может быть записано как"""
".Представьте символ кавычки символом escape ("
\
").«Внутри строки, указанной в«
"
« нет необходимости в специальном обращении, и ее не нужно удваивать или избегать. Точно так же «» внутри строки, указанной с помощью «», не нуждается в особой обработке.
Если SQL-режим сервера включает NO_BACKSLASH_ESCAPES
, то третий из этих параметров, который является обычным подходом, принятым mysql_real_escape_string()
, недоступен: вместо этого следует использовать один из первых двух параметров. Обратите внимание, что эффект четвертой пули заключается в том, что необходимо обязательно знать символ, который будет использоваться для цитирования литерала, чтобы избежать перебора своих данных.
Полезная нагрузка
" OR 1=1 --
Полезная нагрузка инициирует эту инъекцию буквально с "
символом». Нет конкретной кодировки. Никаких специальных символов. Нет странных байтов.
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()
– как будто никакого побега не произошло вообще .
Запрос
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()
свою уродливую голову: соответственно escape_quotes_for_mysql()
не будет использоваться или его предположение о том, какая цитата символы, требующие повторения, будут правильными.
По этой причине я рекомендую, чтобы кто-либо, использующий NO_BACKSLASH_ESCAPES
также ANSI_QUOTES
режим ANSI_QUOTES
, так как он будет принудительно использовать 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
режим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()
, который теперь терпит неудачу и создает ошибкуNO_BACKSLASH_ESCAPES
если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");
… потому что функция quote()
PDO не только ускользает от литерала, но и цитирует его (в символах с одной кавычкой); обратите внимание, что во избежание ошибки 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 безопасны.
Таким образом, если вы:
ИЛИ
ИЛИ
в дополнение к использованию одного из решений в резюме ircmaxell, используйте по крайней мере один из:
NO_BACKSLASH_ESCAPES
… тогда вы должны быть полностью в безопасности (уязвимости, выходящие за пределы строки, отступающей в сторону).
Что ж, в этом нет ничего, что могло бы пройти через это, кроме группового символа %
. Это может быть опасно, если вы используете инструкцию LIKE
поскольку злоумышленник может поставить только %
качестве логина, если вы не отфильтровываете это, и вам придется просто наброситься на пароль любого из ваших пользователей. Люди часто предлагают использовать подготовленные заявления, чтобы сделать его на 100% безопасным, поскольку данные не могут помешать самому запросу. Но для таких простых запросов, вероятно, было бы более эффективно делать что-то вроде $login = preg_replace('/[^a-zA-Z0-9_]/', '', $login);