SQL-инъекция, которая распространяется вокруг mysql_real_escape_string()
Есть ли возможность внедрения 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()
. Но я не могу думать ни о каком возможном подвиге?
Классические инъекции, как это:
aaa' OR 1=1 --
не работай.
Знаете ли вы о возможных инъекциях, которые могли бы пройти через код PHP выше?
Ответы
Ответ 1
Рассмотрим следующий запрос:
$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";
Ответ 2
Короткий ответ да, да есть способ обойти 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()
, мы должны добавить ASCII \
i.e. 0x5c
перед символом '
. Итак, мы закончили с 0xbf5c27
, который в gbk
представляет собой последовательность из двух символов: 0xbf5c
, за которой следует 0x27
. Или, другими словами, действительный символ, за которым следует неизолированный '
. Но мы не используем addslashes()
. Итак, на следующий шаг...
-
mysql_real_escape_string()
Запрос API C на mysql_real_escape_string()
отличается от addslashes()
тем, что он знает набор символов соединения. Таким образом, он может правильно выполнить экранирование набора символов, ожидаемого сервером. Однако до этого момента клиент думает, что мы все еще используем latin1
для соединения, потому что мы никогда не говорили об этом иначе. Мы сказали сервер, на котором мы используем gbk
, но клиент все еще считает его latin1
.
Поэтому вызов mysql_real_escape_string()
вставляет обратную косую черту, и у нас есть свободный висячий символ '
в нашем "экранированном" контенте! Фактически, если бы мы посмотрели на $var
в наборе символов gbk
, мы увидели бы:
縗' OR 1=1 /*
Что означает то, что требует атака.
-
Запрос
Эта часть является просто формальностью, но здесь представленный запрос:
SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
Поздравляем, вы просто успешно атаковали программу, используя mysql_real_escape_string()
...
Плохой
Ухудшается. PDO
defaults для эмуляции подготовленных операторов с 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 будет молча backback для эмуляции утверждений, которые MySQL не может подготовить изначально: те, которые могут быть в руководстве, но будьте осторожны, чтобы выбрать соответствующую версию сервера).
Уродливый
Я сказал в самом начале, что мы могли бы предотвратить все это, если бы мы использовали mysql_set_charset('gbk')
вместо SET NAMES gbk
. И это правда, если вы используете выпуск MySQL с 2006 года.
Если вы используете более раннюю версию MySQL, то bug в mysql_real_escape_string()
означает, что недопустимые многобайтовые символы, например, в наша полезная нагрузка была обработана как одиночные байты для экранирования, даже если клиент был правильно проинформирован о кодировке соединения, и поэтому эта атака все равно будет успешной. Ошибка была исправлена в MySQL 4.1.20, 5.0.22 и 5.1.11.
Но худшая часть заключается в том, что PDO
не показывал C API для mysql_set_charset()
до 5.3.6, поэтому в предыдущих версиях он не могпредотвратите эту атаку для каждой возможной команды!
Теперь он отображается как параметр utf8mb4
не является уязвимым и все же может поддерживать каждый символ Юникода: так что вы можете выбрать использовать это вместо — но он доступен только после того, как MySQL 5.5.3. Альтернативой является utf8
, который также не уязвим и может поддерживать весь Unicode Основная многоязычная плоскость.
В качестве альтернативы вы можете включить режим NO_BACKSLASH_ESCAPES
SQL, который (среди прочего) изменяет работу mysql_real_escape_string()
. Если этот режим включен, 0x27
будет заменен на 0x2727
, а не 0x5c27
, и поэтому процесс экранирования не сможет создать допустимые символы в любом из уязвимых кодировок, где они не существовали ранее (т.е. 0xbf27
по-прежнему 0xbf27
и т.д.) — поэтому сервер все равно будет отклонять строку как недопустимую. Однако см. @eggyal 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()
/PDO DSN параметр charset (в PHP & ge; 5.3.6)
ИЛИ
- Не используйте уязвимый набор символов для кодирования соединения (вы используете только
utf8
/latin1
/ascii
/etc)
Вы на 100% безопасны.
В противном случае вы уязвимы , хотя используете mysql_real_escape_string()
...
Ответ 3
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"');
Как описано в Строковые литералы:
Существует несколько способов включить символы кавычек в строку:
-
A "'
" внутри строки, указанной в "'
", может быть записана как "''
".
-
A ""
" внутри строки, указанной в ""
", может быть записана как """
".
-
Представьте символ кавычки символом-побегом ( "\
" ).
-
A "'
" внутри строки, указанной в ""
", не нуждается в особом обращении и не нуждается в удвоении или исключении. Точно так же ""
" внутри строки, указанной в "'
", не нуждается в особой обработке.
Если режим 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()
&mdash, как будто никакого экранирования не произошло вообще.
-
Запрос
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>
и отсутствие какого-либо особого значения для обратной косой черты). Кроме того, его использование было явно рекомендовано в качестве обходного пути к (уже давно фиксированному) bug, о котором сообщает почта 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 эта ошибка исправлена. См. журнал изменений:
Функциональность добавлена или изменена
Безопасные примеры
В сочетании с ошибкой, объясненной 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 или новее
ИЛИ
... тогда вы должны быть полностью в безопасности (уязвимости, выходящие за пределы строки, выходящей в сторону).
Ответ 4
Ну, нет ничего, что могло бы пройти через это, кроме %
wildcard. Это может быть опасно, если вы используете инструкцию LIKE
, поскольку злоумышленник может поместить только %
в качестве логина, если вы не отфильтровываете это, и вам придется просто набросать пароль любого из ваших пользователей.
Люди часто предлагают использовать подготовленные заявления, чтобы сделать его на 100% безопасным, поскольку данные не могут помешать самому запросу.
Но для таких простых запросов, вероятно, было бы более эффективно делать что-то вроде $login = preg_replace('/[^a-zA-Z0-9_]/', '', $login);
Ответ 5
Я сталкивался с этим, и я предлагаю вам поработать с PDO, но в некоторых случаях вы можете попробовать этот метод. Работает и очень просто. Интересно, почему люди пренебрегали этим?
Пример кода. // Используя мой фреймворк Moorexa
он поддерживает богатый ORM и многое другое. Но я должен был выполнить этот контрольный пример, чтобы быть уверенным в альтернативе людям, которые пишут необработанные SQL-выражения.
пример.
// checking from a user table
$check = DB::table('api_users')->get(['username' => "admin' or password='1'"])->run();
// expected output
SELECT * FROM api_users WHERE username='admin' or password='1'
//sql generated output
SELECT * FROM api_users WHERE username='admin\' or password=\'1\''
// lets try something heavy
$check = DB::table($table)->get(['username' => "admin' or 1=1 UNION SELECT password FROM api_users where id=1"])->run();
// expected output
SELECT * FROM api_users WHERE username='admin' or 1=1 UNION SELECT password FROM api_users where id=1
// this would pass and fail
SELECT * FROM api_users WHERE username='admin\' or 1=1 UNION SELECT password FROM api_users where id=1'
так в чем суть.
- Тип проверки.
- подтвердить ввод пользователя. <ДОВЕРЯЙТЕ НЕТ ОДНОМУ>
- кавычки для строк
Я покажу вам пример кода, выполняющего запрос выбора.
// let assume. would all work
$input = ['username' => "moorexa"]; //or $_POST or $_GET
$sql = 'SELECT * FROM '.$table.' ';
$safe = "";
// let grab the user input from the array
foreach ($input as $key => $val)
{
switch($val)
{
case is_string($val):
$safe .= $key .'=\''.addslashes($val).'\' AND ';
break;
case is_int($val):
$safe .= $key .'='.((int) $val).' AND ';
break;
case is_float($val):
case is_double($val):
$safe .= $key .'='.(double) $val.' AND ';
break;
default:
// this failed
}
}
$safe = rtrim($safe, "AND ");
$sql .= ' WHERE '. $safe .' ';
// now sql contains a valid statement. and would only fail when terms are not met.
// Hope you can apply this and also use more test cases.
Ответ 6
Расширение mysql устарело с PHP 5.5 и полностью удалено в PHP7. Это означает, что вы не можете использовать mysql_ * в PHP7
Используйте PDO или Mysqli вместо mysql. С точки зрения предотвращения инъекций sql используйте параметризованный запрос или подготовленный отчет.