Krótka odpowiedź to tak, tak, jest sposób na obejście mysql_real_escape_string()
.#Dla bardzo NIEZNANYCH PRZYPADKÓW!!!
Długa odpowiedź nie jest taka prosta. Opiera się na ataku pokazany tutaj .
Atak
Zacznijmy więc od pokazania ataku...
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");
W pewnych okolicznościach zwróci więcej niż 1 wiersz. Przeanalizujmy, co się tutaj dzieje:
-
Wybieranie zestawu znaków
mysql_query('SET NAMES gbk');
Aby ten atak zadziałał, potrzebujemy kodowania, którego oczekuje serwer w połączeniu, aby zakodować
'
jak w ASCII tj.0x27
i mieć jakiś znak, którego końcowy bajt to ASCII\
tj.0x5c
. Jak się okazuje, domyślnie w MySQL 5.6 obsługiwanych jest 5 takich kodowań:big5
,cp932
,gb2312
,gbk
isjis
. Wybierzemygbk
tutaj.Teraz bardzo ważne jest, aby zwrócić uwagę na użycie
SET NAMES
tutaj. To ustawia zestaw znaków NA SERWERZE . Jeśli użyliśmy wywołania funkcji C APImysql_set_charset()
, wszystko byłoby dobrze (w wydaniach MySQL od 2006). Ale więcej o tym za chwilę... -
Ładunek
Ładunek, którego zamierzamy użyć do tego wstrzyknięcia, zaczyna się od sekwencji bajtów
0xbf27
. Wgbk
, to jest nieprawidłowy znak wielobajtowy; w językulatin1
, jest to ciąg¿'
. Zauważ, że wlatin1
igbk
,0x27
sam w sobie jest dosłownym'
znak.Wybraliśmy ten ładunek, ponieważ jeśli wywołaliśmy
addslashes()
na nim wstawilibyśmy ASCII\
tj.0x5c
, przed'
postać. Więc skończylibyśmy z0xbf5c27
, który wgbk
jest sekwencją dwóch znaków:0xbf5c
po którym następuje0x27
. Innymi słowy, ważny znak, po którym następuje nieunikniony'
. Ale nie używamyaddslashes()
. Przejdźmy do następnego kroku... -
mysql_real_escape_string()
Wywołanie C API do
mysql_real_escape_string()
różni się odaddslashes()
w tym, że zna zestaw znaków połączenia. Dzięki temu może poprawnie wykonać ucieczkę dla zestawu znaków, którego oczekuje serwer. Jednak do tego momentu klient myśli, że nadal używamylatin1
za połączenie, ponieważ nigdy nie powiedzieliśmy inaczej. Poinformowaliśmy serwer używamygbk
, ale klient nadal myśli, że tolatin1
.Dlatego wywołanie
mysql_real_escape_string()
wstawia odwrotny ukośnik i mamy swobodny wiszący'
postaci w naszej "uciekłej" zawartości! W rzeczywistości, gdybyśmy spojrzeli na$var
wgbk
zestaw znaków, zobaczymy:縗' OR 1=1 /*
Czyli dokładnie co atak wymaga.
-
Zapytanie
Ta część to tylko formalność, ale oto renderowane zapytanie:
SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
Gratulacje, właśnie pomyślnie zaatakowałeś program przy użyciu mysql_real_escape_string()
...
Zły
Pogarsza się. PDO
domyślnie emulacja przygotowane zestawienia z MySQL. Oznacza to, że po stronie klienta zasadniczo wykonuje sprintf przez mysql_real_escape_string()
(w bibliotece C), co oznacza pomyślne wstrzyknięcie:
$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));
Teraz warto zauważyć, że możesz temu zapobiec, wyłączając emulowane przygotowane instrukcje:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
To zazwyczaj skutkuje prawdziwym przygotowanym oświadczeniem (tj. danymi przesyłanymi w oddzielnym pakiecie od zapytania). Należy jednak pamiętać, że PDO po cichu powrót do emulowania instrukcji, których MySQL nie może przygotować natywnie:te, których potrafi to wymienione w instrukcji, ale uważaj, aby wybrać odpowiednią wersję serwera).
Brzydkie
Powiedziałem na samym początku, że moglibyśmy temu zapobiec, gdybyśmy użyli mysql_set_charset('gbk')
zamiast SET NAMES gbk
. I to prawda, pod warunkiem, że używasz wersji MySQL od 2006 roku.
Jeśli używasz wcześniejszej wersji MySQL, to błąd
w mysql_real_escape_string()
oznaczało, że nieprawidłowe znaki wielobajtowe, takie jak te w naszym ładunku, były traktowane jako pojedyncze bajty dla celów ucieczki nawet jeśli klient został prawidłowo poinformowany o kodowaniu połączenia i tak ten atak się powiódł. Błąd został naprawiony w MySQL 4.1.20
, 5.0.22 i 5.1.11 .
Ale najgorsze jest to, że PDO
nie ujawnił C API dla mysql_set_charset()
do 5.3.6, więc w poprzednich wersjach nie zapobiegaj temu atakowi dla każdego możliwego polecenia! Jest teraz ujawniony jako Parametr DSN
.
Łaska zbawienia
Jak powiedzieliśmy na początku, aby ten atak zadziałał, połączenie z bazą danych musi być zaszyfrowane przy użyciu zestawu znaków, który jest podatny na ataki. utf8mb4
nie jest zagrożony a mimo to może obsługiwać każdą Znak Unicode:więc możesz użyć go zamiast tego — ale jest dostępny dopiero od MySQL 5.5.3. Alternatywą jest utf8
, który również nie jest zagrożony i może obsługiwać cały Podstawową płaszczyznę wielojęzyczną
.
Alternatywnie możesz włączyć NO_BACKSLASH_ESCAPES
Tryb SQL, który (między innymi) zmienia działanie mysql_real_escape_string()
. Gdy ten tryb jest włączony, 0x27
zostanie zastąpiony przez 0x2727
zamiast 0x5c27
i dlatego proces ucieczki nie może tworzyć prawidłowe znaki w dowolnym z wrażliwych kodowań, w których wcześniej nie istniały (np. 0xbf27
to nadal 0xbf27
itp.) — więc serwer nadal odrzuci ciąg jako nieprawidłowy. Zobacz jednak odpowiedź @eggyal
dla innej luki, która może powstać w wyniku używania tego trybu SQL.
Bezpieczne przykłady
Poniższe przykłady są bezpieczne:
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");
Ponieważ serwer oczekuje 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");
Ponieważ właściwie ustawiliśmy zestaw znaków, aby klient i serwer pasowały do siebie.
$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 /*"));
Ponieważ wyłączyliśmy emulowane przygotowane wyciągi.
$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 /*"));
Ponieważ poprawnie ustawiliśmy zestaw znaków.
$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();
Ponieważ MySQLi cały czas wykonuje prawdziwie przygotowane instrukcje.
Zawijanie
Jeśli:
- Używaj nowoczesnych wersji MySQL (późne 5.1, wszystkie 5.5, 5.6 itd.) ORAZ
mysql_set_charset()
/$mysqli->set_charset()
/ Parametr zestawu znaków DSN PDO (w PHP ≥ 5.3.6)
LUB
- Nie używaj zagrożonego zestawu znaków do kodowania połączenia (używasz tylko
utf8
/latin1
/ascii
/ itp)
Jesteś w 100% bezpieczny.
W przeciwnym razie jesteś narażony nawet jeśli używasz mysql_real_escape_string()
...