Короткий ответ: да, да, есть способ обойти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
. Как выясняется, есть 5 таких кодировок , поддерживаемых в MySQL 5.6 по умолчанию: big5
, cp932
, gb2312
, gbk
и sjis
. Мы выберем gbk
здесь.
Теперь очень важно отметить использование SET NAMES
здесь. Это устанавливает набор символов на сервере . Если бы мы использовали вызов функции C API mysql_set_charset()
, у нас все было бы в порядке (в версиях MySQL с 2006 года). Но подробнее о том, почему через минуту ...
Полезная нагрузка
Полезная нагрузка, которую мы собираемся использовать для этой инъекции, начинается с последовательности байтов 0xbf27
. Во- gbk
первых, это недопустимый многобайтовый символ; в latin1
это строка ¿'
. Обратите внимание, что в latin1
и gbk
, 0x27
само по себе является буквальным '
символом.
Мы выбрали эту полезную нагрузку, потому что, если бы мы ее вызвали addslashes()
, мы вставили бы ASCII, \
т.е. 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
наборе символов, мы видим:
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 для PDO (в PHP ≥ 5.3.6)
ИЛИ
- Не используйте уязвимый набор символов для кодирования соединения (вы используете только
utf8
/ latin1
/ ascii
/ etc)
Вы на 100% в безопасности.
В противном случае вы уязвимы, даже если вы используетеmysql_real_escape_string()
...