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()
функция используется. Но я не могу думать ни о каком возможном подвиге?
Классические инъекции, как это:
aaa' OR 1=1 --
не работай.
Знаете ли вы о возможных инъекциях, которые могли бы пройти через код PHP выше?
5 ответов
Рассмотрим следующий запрос:
$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 APImysql_set_charset()
все будет в порядке (в версиях MySQL с 2006 года). Но подробнее о том, почему через минуту...Полезная нагрузка
Полезная нагрузка, которую мы собираемся использовать для этой инъекции, начинается с последовательности байтов
0xbf27
, Вgbk
это недопустимый многобайтовый символ; вlatin1
Строка¿'
, Обратите внимание, что вlatin1
а такжеgbk
,0x27
сам по себе является буквальным'
персонаж.Мы выбрали эту полезную нагрузку, потому что, если мы позвонили
addslashes()
на нем мы вставим ASCII\
т.е.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
по умолчанию эмулирует подготовленные операторы с 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.
Спасительная Грация
Как мы уже говорили, чтобы эта атака работала, соединение с базой данных должно быть закодировано с использованием уязвимого набора символов. utf8mb4
не уязвим и все же может поддерживать каждый символ Unicode: так что вы можете использовать его вместо этого, но он был доступен только с 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 (последняя версия 5.1, все версии 5.5, 5.6 и т. Д.) И
mysql_set_charset()
/$mysqli->set_charset()
/ Параметр DSN charset (в PHP ≥ 5.3.6)
ИЛИ ЖЕ
- Не используйте уязвимый набор символов для кодирования соединения (вы используете только
utf8
/latin1
/ascii
/ так далее)
Вы на 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"');
Как описано в строковых литералах:
Есть несколько способов включить символы кавычки в строку:
А
'
Внутри строки, заключенной в кавычки'
"Может быть написано как"''
".А
"
Внутри строки, заключенной в кавычки"
"Может быть написано как"""
".Перед символом кавычки должен быть символ перехода ("
\
").А
'
Внутри строки, заключенной в кавычки"
”Не требует особого отношения и не нуждается в удвоении или спасении. Точно так же:"
Внутри строки, заключенной в кавычки'
"Не требует особого отношения.
Если режим 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()
не будет использоваться, или его предположение о том, какие символы кавычек требуют повторения, будет правильным.
По этой причине я рекомендую всем, кто использует 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 заявления безопасны.
Завершение
Таким образом, если вы:
- использовать нативно подготовленные заявления
ИЛИ ЖЕ
- использовать MySQL v5.7.6 или новее
ИЛИ ЖЕ
в дополнение к использованию одного из решений в резюме ircmaxell, используйте по крайней мере одно из:
- PDO;
- строковые литералы в одинарных кавычках; или же
- явно установленный режим SQL, который не включает
NO_BACKSLASH_ESCAPES
... тогда вы должны быть полностью в безопасности (уязвимости выходят за рамки выхода строки).
Ну, на самом деле нет ничего, что может пройти через это, кроме %
подстановочные. Это может быть опасно, если вы используете LIKE
Заявление, как злоумышленник может поставить просто %
в качестве входа, если вы не отфильтровываете это, и вам придется просто взломать пароль любого из ваших пользователей. Люди часто предлагают использовать подготовленные операторы, чтобы сделать их на 100% безопасными, так как данные не могут таким образом вмешиваться в сам запрос. Но для таких простых запросов, вероятно, было бы более эффективно сделать что-то вроде $login = preg_replace('/[^a-zA-Z0-9_]/', '', $login);
Я сталкивался с этим, и я предлагаю вам поработать с 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's assume. would all work
$input = ['username' => "moorexa"]; //or $_POST or $_GET
$sql = 'SELECT * FROM '.$table.' ';
$safe = "";
// let's 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.