SQL injection, który krąży wokół mysql real escape string()
Czy istnieje możliwość SQL injection nawet przy użyciu funkcji mysql_real_escape_string()
?
Rozważ tę przykładową sytuację. SQL jest skonstruowany w PHP Tak:
$login = mysql_real_escape_string(GetFromPost('login'));
$password = mysql_real_escape_string(GetFromPost('password'));
$sql = "SELECT * FROM table WHERE login='$login' AND password='$password'";
Słyszałem, jak wiele osób mówiło mi, że taki kod jest nadal niebezpieczny i możliwy do zhakowania nawet z mysql_real_escape_string()
użytą funkcją. Ale nie mogę wymyślić żadnego możliwego wyzysku?
Klasyczne zastrzyki jak to:
aaa' OR 1=1 --
Nie działa.
Czy wiesz o jakimkolwiek możliwym zastrzyku, który mógłby przedostać się przez powyższy kod PHP?
4 answers
Rozważ następujące zapytanie:
$iId = mysql_real_escape_string("1 OR 1=1");
$sSql = "SELECT * FROM table WHERE id = $iId";
mysql_real_escape_string()
nie ochroni Cię przed tym.
to, że używasz pojedynczych cudzysłowów (' '
) wokół zmiennych wewnątrz zapytania, chroni cię przed tym. opcja jest również następująca:
$iId = (int)"1 OR 1=1";
$sSql = "SELECT * FROM table WHERE id = $iId";
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-12-16 12:45:41
Krótka odpowiedź brzmi tak, tak jest sposób na obejście mysql_real_escape_string()
.
Dla bardzo niejasnych przypadków krawędzi!!!
Długa odpowiedź nie jest taka łatwa. Opiera się na ataku pokazanym tutaj .Atak
Zacznijmy 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. Zastanówmy się, co tu się dzieje:
-
Wybór postaci Zestaw
mysql_query('SET NAMES gbk');
Aby ten atak zadziałał, potrzebujemy kodowania, którego serwer oczekuje na połączeniu, aby zakodować
'
jak w ASCII tzn.0x27
i mieć jakiś znak, którego końcowy bajt to ASCII\
tzn.0x5c
. Jak się okazuje, w MySQL 5.6 domyślnie wspieranych jest 5 takich kodowań:big5
,cp932
,gb2312
,gbk
isjis
. Wybierzemygbk
tutaj.Teraz, to bardzo ważne, aby zwrócić uwagę na użycie
SET NAMES
tutaj. To ustawia zestaw znaków na serwerze . Gdybyśmy użyli wywołania funkcji C APImysql_set_charset()
, byłoby dobrze (w wydaniach MySQL od 2006 roku). Ale więcej o tym, dlaczego za chwilę... -
Ładowność
Ładunek, którego użyjemy do tego zastrzyku, zaczyna się od sekwencji bajtów
0xbf27
. Wgbk
jest to nieprawidłowy znak wielobajtowy; wlatin1
jest to ciąg znaków¿'
. Zauważ, że wlatin1
orazgbk
,0x27
sam w sobie jest literalnym znakiem'
.Wybraliśmy ten ładunek, ponieważ gdybyśmy wywołali {[32] } na nim, wstawilibyśmy znak ASCII
\
, tj.0x5c
, przed znakiem'
. Tak więc kończymy0xbf5c27
, który wgbk
jest ciągiem dwuznakowym:0xbf5c
, po którym następuje0x27
. Lub innymi słowy, znak valid , po którym następuje unescaped'
. Ale nie używamyaddslashes()
. Więc przejdź do następnego kroku... -
Mysql_real_escape_string()
Wywołanie C API do
mysql_real_escape_string()
różni się zaddslashes()
w tym, że zna zestaw znaków połączenia. W ten sposób może poprawnie wykonać ucieczkę dla zestawu znaków, którego oczekuje serwer. Jednak do tej pory klient myśli, że nadal Używamylatin1
do połączenia, ponieważ nigdy nie powiedzieliśmy inaczej. Powiedzieliśmy serwerowi , że używamygbk
, ale klient nadal myśli, że tolatin1
.Dlatego wywołanie
mysql_real_escape_string()
wstawia ukośnik wsteczny i mamy znak free hanging'
w naszej" ucieczce " treści! W rzeczywistości, gdybyśmy spojrzeli na$var
W zestawie znakówgbk
, zobaczylibyśmy:縗' OR 1=1 /*
-
Zapytanie
Ta część to tylko formalność, ale oto renderowane zapytanie:]}SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
Gratulacje, właśnie z powodzeniem zaatakowałeś program używając mysql_real_escape_string()
...
The Bad
Jest jeszcze gorzej.PDO
domyślnie emulowanie przygotowanych instrukcji za pomocą MySQL. Oznacza to, że po stronie klienta, w zasadzie wykonuje sprintf przez mysql_real_escape_string()
(w bibliotece C), co oznacza, że następujące spowoduje 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 /*"));
Warto zauważyć, że można temu zapobiec, wyłączając emulowane, przygotowane instrukcje:]}
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
To spowoduje Zwykle wynik prawdziwego, przygotowanego Oświadczenia (tzn. dane są przesyłane w oddzielnym pakiecie od zapytania). Jednak być świadomość, że PDO będzie po cichu fallback do emulacji instrukcji, których MySQL nie może przygotować natywnie: te, które może być wymienione w podręczniku, ale uważaj, aby wybrać odpowiednią wersję serwera).
Brzydka
Na samym początku powiedziałem, że moglibyśmy temu zapobiec, gdybyśmy użylimysql_set_charset('gbk')
zamiast SET NAMES gbk
. I to prawda, pod warunkiem, że używasz Wydania MySQL od 2006 roku.
Jeśli używasz wcześniejszego wydania MySQL, to błąd w mysql_real_escape_string()
oznaczał, że niepoprawne 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, a więc atak nadal się powiedzie. Błąd został naprawiony w MySQL 4.1.20, 5.0.22 oraz 5.1.11.
Ale najgorsze jest to, że PDO
nie ujawnił API C dla mysql_set_charset()
aż do 5.3.6, więc w poprzednich wersjach nie można zapobiec temu atakowi dla każdego możliwego rozkazu!
Jest teraz wyświetlany jako parametr DSN .
The Saving Grace
Jak powiedzieliśmy na początku, aby ten atak zadziałał, połączenie z bazą danych musi być zakodowane przy użyciu zestawu znaków podatnych na ataki. utf8mb4
jest nie podatny na ataki , a mimo to może obsługiwać każdy znak Unicode: więc możesz go użyć zamiast tego-ale jest on dostępny dopiero od MySQL 5.5.3. Alternatywa na utf8
, który jest również nie podatny i może obsługiwać cały Unicode Podstawowa płaszczyzna Wielojęzyczna.
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 0x2727
zamiast 0x5c27
, a zatem proces wycinający nie może utworzyć poprawnych znaków w żadnym z podatnych kodowań, gdzie nie istniały poprzednio (tzn. 0xbf27
jest nadal 0xbf27
itd.)- więc serwer nadal odrzuci łańcuch jako nieprawidłowy. Jednak zobacz @eggyal ' s answer dla innej luki, która może wynikać z 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ż odpowiednio ustawiliśmy zestaw znaków tak, aby klient i serwer pasowały.
$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 wypowiedzi.
$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ż ustawiliśmy odpowiednio 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 robi prawdziwe przygotowane wypowiedzi.
Owijanie
Jeśli:
- użyj nowoczesnych wersji MySQL (późne 5.1, wszystkie 5.5, 5.6, itp.) I
mysql_set_charset()
/$mysqli->set_charset()
/ PDO ' s DSN charset parameter (w PHP ≥ 5.3.6)
Lub
- nie używaj podatnego zestawu znaków do kodowania połączenia (używasz tylko
utf8
/latin1
/ascii
/ etc)
W Przeciwnym Razie, jesteś bezbronny nawet jeśli używasz mysql_real_escape_string()
...
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:55:01
TL;DR
mysql_real_escape_string()
czy nie zapewnia żadnej ochrony (a ponadto może munge Twoje dane), jeśli:
MySQL ' s
NO_BACKSLASH_ESCAPES
tryb SQL jest włączony (który może być, chyba że jawnie wybierzesz inny tryb SQL za każdym razem, gdy połączysz się ); iTwoje literały ciągu SQL są cytowane za pomocą znaków podwójnego cudzysłowu
"
.Ten został złożony jako błąd # 72458 i został naprawiony w MySQL v5.7.6 (zobacz sekcję "the Saving Grace", poniżej).
To jest inny, (może mniej?) obscure EDGE CASE!!!
W hołdzie@ircmaxell ' s excellent answer (naprawdę, to ma być pochlebstwo, a nie plagiat!), Przyjmę jego format:
Atak
Zaczynając od demonstracji...
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');
To zwróci wszystkie rekordy z test
stolik. Rozwarstwienie:
-
Wybór trybu SQL
mysql_query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"');
Udokumentowane pod literały łańcuchowe :
Istnieje kilka sposobów na umieszczenie znaków cudzysłowu w łańcuchu znaków:
"
'
"wewnątrz ciągu cytowanego z"'
"można zapisać jako"''
"."
"
"wewnątrz ciągu cytowanego z""
"można zapisać jako"""
".Poprzedzaj cytat postać po znaku ucieczki ("
\
")."
'
"wewnątrz ciągu cytowanego z""
" nie wymaga specjalnego traktowania i nie musi być PODWAJANE lub unikane. W ten sam sposób ""
" wewnątrz ciągu cytowanego z "'
" nie wymaga specjalnego traktowania.
Jeśli tryb SQL serwera zawiera
NO_BACKSLASH_ESCAPES
, wtedy trzecia z tych opcji - która jest zwyczajowym podejściem przyjętym przezmysql_real_escape_string()
- nie jest dostępna: jedna z dwóch pierwszych opcji musi być używany zamiast. Zauważ, że efekt czwartego pocisku polega na tym, że trzeba koniecznie znać znak, który będzie używany do cytowania dosłownego, aby uniknąć muntowania swoich danych. -
Ładowność
" OR 1=1 --
Ładunek inicjuje to wstrzyknięcie dosłownie znakiem
"
. Brak konkretnego kodowania. Brak znaków specjalnych. Żadnych dziwnych bajtów. -
Mysql_real_escape_string()
$var = mysql_real_escape_string('" OR 1=1 -- ');
Na szczęście,
mysql_real_escape_string()
sprawdza tryb SQL i odpowiednio dostosowuje jego zachowanie. Zobacz też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); }
Tak więc inna podstawowa funkcja,
escape_quotes_for_mysql()
, jest wywoływana, jeśli używany jest trybNO_BACKSLASH_ESCAPES
SQL. Jak wspomniano powyżej, taka funkcja musi wiedzieć, który znak będzie używany do cytowania dosłownego, aby powtórzyć go bez powodowania, że drugi znak cytowania jest powtarzany dosłownie.Jednak funkcja ta arbitralnie zakłada , że łańcuch będzie cytowanie za pomocą pojedynczego cudzysłowu
'
. Zobacz też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++= '\''; }
Tak więc, pozostawia znaki podwójnego cudzysłowu
"
nietknięte (i podwaja wszystkie znaki pojedynczego cudzysłowu'
) niezależnie od rzeczywistego znaku, który jest używany do cytowania dosłownego! W naszym przypadku$var
pozostaje dokładnie taki sam jak argument, który został dostarczonymysql_real_escape_string()
-to tak, jakby nie miało miejsca żadne wyjście } w ogóle. -
Na Zapytanie
mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');
Coś z formalności, renderowane zapytanie to:
SELECT * FROM test WHERE name = "" OR 1=1 -- " LIMIT 1
Jak to ujął mój uczony przyjaciel: gratulacje, właśnie z powodzeniem zaatakowałeś program używając mysql_real_escape_string()
...
The Bad
mysql_set_charset()
nie może pomóc, ponieważ nie ma to nic wspólnego z zestawami znaków; ani nie może mysqli::real_escape_string()
, ponieważ to tylko inna owijka wokół tej samej funkcji.
Problem, jeśli nie już oczywisty, polega na tym, że wezwanie do mysql_real_escape_string()
nie można wiedzieć , z którym znakiem literał będzie cytowany, ponieważ decyzja o tym zostanie pozostawiona deweloperowi w późniejszym czasie. Tak więc, w trybie NO_BACKSLASH_ESCAPES
, nie ma dosłownie , nie ma możliwości, aby ta funkcja mogła bezpiecznie uciec od każdego wejścia do użycia z dowolnym cytowaniem(przynajmniej, nie bez podwojenia znaków, które nie wymagają podwojenia i tym samym munging danych).
Brzydka
Jest jeszcze gorzej.NO_BACKSLASH_ESCAPES
może nie być to wszystko, co rzadkie na wolności ze względu na potrzeba użycia go w celu zapewnienia zgodności ze standardowym SQL (na przykład patrz punkt 5.3 specyfikacji SQL-92, a mianowicie produkcja gramatyki <quote symbol> ::= <quote><quote>
i brak specjalnego znaczenia przypisywanego ukośnikowi odwrotnemu). Co więcej, jego użycie zostało wyraźnie zalecane jako obejście do (dawno naprawionego) błędu opisanego w poście ircmaxell. Kto wie, niektóre bazy danych mogą nawet skonfigurować je tak, aby były domyślnie włączone, aby zniechęcić do stosowania niewłaściwych metod ucieczki, takich jak addslashes()
.
Ponadto, tryb SQL nowego połączenia jest ustawiany przez serwer zgodnie z jego konfiguracją (którą użytkownik SUPER
może zmienić w dowolnym momencie); dlatego, aby być pewnym zachowania serwera, musisz zawsze jawnie określić żądany tryb po połączeniu.
The Saving Grace
Tak długo, jak zawsze jawnie ustaw tryb SQL, aby nie zawierał NO_BACKSLASH_ESCAPES
, lub cytowania literałów ciągu MySQL za pomocą pojedynczego cudzysłowu znak, ten bug nie może cofnąć swojej brzydkiej głowy: odpowiednio escape_quotes_for_mysql()
nie zostanie użyty, lub jego założenie, które znaki cytowania wymagają powtórzenia, będzie poprawne.
Z tego powodu polecam każdemu, kto używa NO_BACKSLASH_ESCAPES
również umożliwia ANSI_QUOTES
mode, gdyż wymusi nawykowe użycie pojedynczych cytowanych liter. Zauważ, że nie uniemożliwia to SQL injection w przypadku użycia podwójnych cytowanych liter-zmniejsza to jedynie prawdopodobieństwo, że tak się stanie (ponieważ zwykłe, Nie złośliwe zapytania zawiedzie).
W PDO, zarówno jego równoważna funkcja PDO::quote()
i jego przygotowanego emulatora instrukcji wywołania mysql_handle_quoter()
-co robi dokładnie to: zapewnia, że literał ucieczki jest cytowany w pojedynczych cudzysłowach, więc możesz być pewien, że PDO jest zawsze odporny na ten błąd.
Od wersji MySQL V5. 7. 6 ten błąd został naprawiony. Zobacz change log :
Dodano lub zmieniono funkcjonalność
niezgodna Zmiana: nowa funkcja C API,
mysql_real_escape_string_quote()
, został wdrożony jako zamiennik dlamysql_real_escape_string()
ponieważ ta ostatnia funkcja może nie poprawnie kodować znaków, gdyNO_BACKSLASH_ESCAPES
tryb SQL jest włączony. W tym przypadku,mysql_real_escape_string()
nie może uniknąć znaków cytowania, z wyjątkiem podwojenia ich i aby to zrobić poprawnie, musi znać więcej informacji o kontekście cytowania niż jest dostępna.mysql_real_escape_string_quote()
pobiera dodatkowy argument do określenia kontekstu cytowania. Aby dowiedzieć się więcej, Zobacz mysql_real_escape_string_quote().Uwaga
Aplikacje powinny być modyfikowane w celu wykorzystania
mysql_real_escape_string_quote()
, zamiastmysql_real_escape_string()
, który teraz zawodzi i produkujeCR_INSECURE_API_ERR
błąd jeśliNO_BACKSLASH_ESCAPES
jest włączone.Bibliografia: Zobacz też Bug #19211994.
Bezpieczny Przykłady
Wzięte razem z błędem wyjaśnionym przez ircmaxell, poniższe przykłady są całkowicie bezpieczne (zakładając, że ktoś używa MySQL później niż 4.1.20, 5.0.22, 5.1.11; lub że ktoś nie używa kodowania połączenia 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');
...ponieważ wyraźnie wybraliśmy tryb SQL, który nie zawiera 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");
...ponieważ cytujemy nasz ciąg literalny z pojedynczymi cudzysłowami.
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(["' OR 1=1 /*"]);
...ponieważ PDO przygotowane oświadczenia są odporne z tej luki (i ircmaxell też, pod warunkiem, że używasz PHP≥5.3.6 i zestaw znaków został poprawnie ustawiony w DSN; lub że emulacja przygotowanej instrukcji została wyłączona).
$var = $pdo->quote("' OR 1=1 /*");
$stmt = $pdo->query("SELECT * FROM test WHERE name = $var LIMIT 1");
...ponieważ funkcja quote()
PDO nie tylko ucieka literałowi, ale także cytuje go (w postaci pojedynczego cudzysłowu '
); zauważ, że aby uniknąć błędu ircmaxell w tym przypadku, musisz używać PHP≥5.3.6 i poprawnie ustawić zestaw znaków w DSN.
$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "' OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();
...ponieważ instrukcje przygotowane przez MySQLi są bezpieczne.
Owijanie W Górę
Tak więc, jeśli:
- użyj natywnych, przygotowanych instrukcji
Lub
- użyj MySQL v5.7. 6 lub nowszego
Lub
-
W dodaniu do jednego z rozwiązań w podsumowaniu ircmaxell, Użyj co najmniej jednego z:
- PDO;
- pojedyncze cytowane literały łańcuchowe; lub
- jawnie ustawiony tryb SQL, który nie zawiera
NO_BACKSLASH_ESCAPES
...następnie powinieneś być całkowicie bezpieczny(luki poza zakresem ucieczki łańcucha znaków na bok).
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:10:45
Nie ma nic, co mogłoby przez to przejść, poza %
wildcard. To może być niebezpieczne, jeśli używasz LIKE
oświadczenie jako atakujący może umieścić po prostu %
jako login, jeśli nie filtruj tego, i będzie musiał po prostu bruteforce hasło dowolnego z użytkowników.
Ludzie często sugerują użycie gotowych oświadczeń, aby było w 100% bezpieczne, ponieważ dane nie mogą w ten sposób zakłócać samego zapytania.
Ale dla takich prostych zapytań prawdopodobnie bardziej efektywne byłoby zrobienie czegoś takiego jak $login = preg_replace('/[^a-zA-Z0-9_]/', '', $login);
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-04-21 08:15:22