Краткият отговор е да, да, има начин да заобиколите 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 APImysql_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
/ и т.н.)
Вие сте 100% в безопасност.
В противен случай сте уязвими въпреки че използвате mysql_real_escape_string()
...