Czy istnieje możliwość wstrzyknięcia SQL, nawet przy użyciu mysql_real_escape_string()
funkcji?
Rozważ tę przykładową sytuację. SQL jest zbudowany w PHP w następujący sposób:
$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, że wiele osób mówi mi, że taki kod jest nadal niebezpieczny i można go zhakować nawet przy użyciu mysql_real_escape_string()
funkcji. Ale nie mogę wymyślić żadnego możliwego exploita?
Klasyczne zastrzyki takie jak to:
aaa' OR 1=1 --
nie działa.
Czy znasz jakieś możliwe zastrzyki, które mogłyby przejść przez powyższy kod PHP?
mysql_*
funkcji w nowym kodzie . Nie są już utrzymywane irozpoczął się proces amortyzacji. Widzisz czerwone pole ? Dowiedz sięzamiast tegoo przygotowanych instrukcjach i użyj PDO lub MySQLi - ten artykuł pomoże ci zdecydować, które. Jeśli wybierzesz PDO, oto dobry tutorial .mysql_*
funkcje już generująE_DEPRECATED
ostrzeżenie.ext/mysql
Rozszerzenie nie zostało utrzymane przez okres dłuższy niż 10 lat. Czy naprawdę jesteś tak złudny?Odpowiedzi:
Rozważ następujące zapytanie:
mysql_real_escape_string()
nie ochroni cię przed tym. Chroni cię przed tym fakt, że używasz pojedynczych cudzysłowów (' '
) wokół zmiennych w zapytaniu. Następująca jest również opcja:źródło
mysql_query()
nie wykonuje wielu instrukcji, nie?DROP TABLE
, w praktyce atakujący jest bardziej prawdopodobneSELECT passwd FROM users
. W tym drugim przypadku drugie zapytanie jest zwykle wykonywane za pomocąUNION
klauzuli.(int)mysql_real_escape_string
- to nie ma sensu. Nie różni(int)
się wcale. I przyniosą ten sam wynik dla każdego wkładumysql_real_escape_string
, a niemysql_real_escape_integer
. Nie należy go stosować z polami liczb całkowitych.Krótka odpowiedź brzmi: tak, tak jest sposób na obejście
mysql_real_escape_string()
.Do bardzo OBECURE CASE CASE !!!
Długa odpowiedź nie jest taka łatwa. Opiera się na pokazanym tutaj ataku .
Atak
Zacznijmy więc od pokazania ataku ...
W pewnych okolicznościach zwróci więcej niż 1 wiersz. Przeanalizujmy, co się tutaj dzieje:
Wybieranie zestawu znaków
Aby ten atak zadziałał, potrzebujemy kodowania, którego serwer oczekuje w połączeniu zarówno do kodowania
'
jak w ASCII tj.0x27
I do posiadania znaku, którego ostatnim bajtem jest ASCII\
tj0x5c
. Jak się okazuje, istnieje 5 takich kodowania obsługiwane w MySQL 5.6 domyślnie:big5
,cp932
,gb2312
,gbk
isjis
.gbk
Tutaj wybierzemy .Teraz bardzo ważne jest, aby zauważyć użycie
SET NAMES
tutaj. To ustawia zestaw znaków NA SERWERZE . Gdybyśmy użyli wywołania funkcji C APImysql_set_charset()
, wszystko byłoby w porządku (w wersjach MySQL od 2006 roku). Ale więcej o tym, dlaczego za minutę ...Ładunek
Ładunek, którego użyjemy do tego wstrzyknięcia, zaczyna się od sekwencji bajtów
0xbf27
. Wgbk
to niepoprawny znak wielobajtowy; wlatin1
to jest ciąg¿'
. Zauważ, że wlatin1
igbk
,0x27
na własną rękę jest dosłowny'
charakter.Wybraliśmy ten ładunek, ponieważ jeśli
addslashes()
go wywołamy, wstawimy ASCII,\
tj.0x5c
Przed'
znakiem. Więc chcemy skończyć z0xbf5c27
, któragbk
jest sekwencją dwóch znaków:0xbf5c
następuje0x27
. Lub innymi słowy, prawidłowy znak, po którym następuje nieskalowany'
. Ale my nie używamyaddslashes()
. Przejdźmy do następnego kroku ...mysql_real_escape_string ()
Wywołanie interfejsu API C
mysql_real_escape_string()
różni sięaddslashes()
tym, że zna zestaw znaków połączenia. Dzięki temu może poprawnie wykonać zmianę znaczenia dla zestawu znaków, którego oczekuje serwer. Jednak do tego momentu klient uważa, że nadal używamylatin1
do połączenia, ponieważ nigdy nie powiedzieliśmy tego inaczej. Poinformowaliśmy serwer , którego używamygbk
, ale klient nadal uważa, że taklatin1
.Dlatego wezwanie do
mysql_real_escape_string()
wstawienia odwrotnego ukośnika, a my mamy wolną'
postać w naszej „ucieczce” treści! W rzeczywistości, jeśli byliśmy patrzeć$var
wgbk
zestawie znaków, to widzimy:Właśnie tego wymaga atak.
Zapytanie
Ta część jest tylko formalnością, ale oto renderowane zapytanie:
Gratulacje, właśnie zaatakowałeś program za pomocą
mysql_real_escape_string()
...Źli
Pogarsza się.
PDO
domyślnie emuluje przygotowane instrukcje za pomocą MySQL. Oznacza to, że po stronie klienta wykonuje on sprintfmysql_real_escape_string()
(w bibliotece C), co oznacza, że następujące działania zakończą się powodzeniem:Teraz warto zauważyć, że można temu zapobiec, wyłączając emulowane przygotowane instrukcje:
Będzie to zwykle prowadzi do prawdziwej przygotowanego zestawienia (czyli danych wysyłanych przez w oddzielnym pakiecie z zapytania). Należy jednak pamiętać, że PDO będzie cicho awaryjna do emulacji oświadczenia, że MySQL nie może przygotować natywnie: ci, że może są wymienione w instrukcji, ale uważaj, aby wybrać odpowiednią wersję serwera).
Brzydki
Na samym początku powiedziałem, że moglibyśmy temu zapobiec, gdybyśmy tego użyli
mysql_set_charset('gbk')
zamiastSET NAMES gbk
. I to prawda, pod warunkiem, że używasz wersji MySQL od 2006 roku.Jeśli używasz wcześniejszej wersji MySQL, potem błąd w
mysql_real_escape_string()
oznaczało, że nieprawidłowe znaki wielobajtowe, takie jak te w naszym ładowności były traktowane jako pojedyncze bajty dla celów ucieczce nawet jeśli klient został prawidłowo poinformowany o kodowaniu połączeń i tak ten atak wciąż się udaje. Błąd został naprawiony w MySQL 4.1.20 , 5.0.22 i 5.1.11 .Ale najgorsze jest to, że
PDO
nie ujawniało C APImysql_set_charset()
do 5.3.6, więc we wcześniejszych wersjach nie mogło zapobiec temu atakowi dla każdego możliwego polecenia! Jest teraz dostępny jako parametr DSN .The Saving Grace
Jak powiedzieliśmy na wstępie, aby ten atak zadziałał, połączenie z bazą danych musi zostać zakodowane przy użyciu podatnego zestawu znaków. nie
utf8mb4
jest podatny na ataki, a jednak może obsługiwać każdą postać Unicode: możesz więc zamiast tego użyć tej opcji - ale jest ona dostępna tylko od MySQL 5.5.3. Alternatywą jestutf8
, która również nie jest wrażliwa i może obsługiwać cały podstawowy wielojęzyczny samolot Unicode .Alternatywnie możesz włączyć
NO_BACKSLASH_ESCAPES
tryb SQL, który (między innymi) zmienia działaniemysql_real_escape_string()
. Po włączeniu tego trybu0x27
zostanie zastąpione0x2727
zamiast zamiast,0x5c27
dlatego proces zmiany znaczenia nie może utworzyć prawidłowych znaków w żadnym z wrażliwych kodowań, w których wcześniej nie istniały (tj.0xbf27
Nadal jest0xbf27
itp.) - więc serwer nadal odrzuca ciąg jako nieprawidłowy . Jednak zobacz odpowiedź @ eggyal na inną podatność, która może wynikać z używania tego trybu SQL.Bezpieczne przykłady
Następujące przykłady są bezpieczne:
Ponieważ serwer oczekuje
utf8
...Ponieważ właściwie ustawiliśmy zestaw znaków, aby klient i serwer pasowały do siebie.
Ponieważ wyłączyliśmy emulowane przygotowane oświadczenia.
Ponieważ poprawnie ustawiliśmy zestaw znaków.
Ponieważ MySQLi cały czas wykonuje prawdziwe przygotowane instrukcje.
Podsumowanie
Jeśli ty:
mysql_set_charset()
/$mysqli->set_charset()
/ PDO parametr zestawu znaków DSN (w PHP ≥ 5.3.6)LUB
utf8
/latin1
/ascii
/ etc)Jesteś w 100% bezpieczny.
W przeciwnym razie jesteś podatny na zagrożenia, nawet jeśli używasz
mysql_real_escape_string()
...źródło
addslashes
. I na podstawie tej podatności na jednym. Spróbuj sam. Pobierz MySQL 5.0, uruchom ten exploit i przekonaj się sam. Jeśli chodzi o to, jak umieścić to w PUT / GET / POST, to jest TRIVIAL. Dane wejściowe to tylko bajty strumieniowe.char(0xBF)
to tylko czytelny sposób generowania bajtu. Pokazałem tę lukę na żywo przed wieloma konferencjami. Zaufaj mi w tym ... Ale jeśli nie, spróbuj sam. Działa ...?var=%BF%27+OR+1=1+%2F%2A
w adresie URL,$var = $_GET['var'];
w kodzie, a Bob jest twoim wujem.To kolejna (może mniej?) Niejasna KRAWĘDZIA EDGE !!!
W hołdzie doskonałej odpowiedzi @ ircmaxell (naprawdę, to ma być pochlebstwo, a nie plagiat!), Przyjmuję jego format:
Atak
Zaczynam od demonstracji ...
Spowoduje to zwrócenie wszystkich rekordów z
test
tabeli. Rozbiór:Wybór trybu SQL
Jak udokumentowano pod Smyczkowych literale :
Jeśli zawiera tryb SQL serwera
NO_BACKSLASH_ESCAPES
, wówczas trzecia z tych opcji - co jest typowym podejściem przyjętym przezmysql_real_escape_string()
- nie jest dostępna: zamiast niej należy użyć jednej z dwóch pierwszych opcji. Zauważ, że efekt czwartego pocisku polega na tym, że trzeba koniecznie znać znak, który zostanie użyty do zacytowania literału, aby uniknąć mungowania danych.Ładunek
Ładunek inicjuje ten zastrzyk dosłownie z
"
postacią. Bez szczególnego kodowania. Brak znaków specjalnych. Żadnych dziwnych bajtów.mysql_real_escape_string ()
Na szczęście
mysql_real_escape_string()
sprawdza tryb SQL i odpowiednio dostosowuje jego zachowanie. Zobaczlibmysql.c
:W ten sposób
escape_quotes_for_mysql()
wywoływana jest inna funkcja bazowa, jeśliNO_BACKSLASH_ESCAPES
używany jest tryb SQL. Jak wspomniano powyżej, taka funkcja musi wiedzieć, który znak zostanie użyty do zacytowania literału, aby powtórzyć go bez powodowania dosłownego powtórzenia drugiego znaku cytowania.Jednak ta funkcja arbitralnie zakłada, że ciąg będzie cytowany przy użyciu
'
znaku pojedynczego cudzysłowu . Zobaczcharset.c
:Tak więc pozostawia
"
nietknięte znaki podwójnego cudzysłowu (i podwaja wszystkie'
znaki pojedynczego cudzysłowu ), niezależnie od rzeczywistego znaku używanego do cytowania literału ! W naszym przypadku$var
szczątków dokładnie taka sama jak argument, który został przewidziany domysql_real_escape_string()
-To jakby ma ucieczki doszło w ogóle .Zapytanie
Renderowane zapytanie jest formalnością:
Jak ujął to mój wyuczony przyjaciel: gratulacje, właśnie zaatakowałeś program za pomocą
mysql_real_escape_string()
...Źli
mysql_set_charset()
nie może pomóc, ponieważ nie ma to nic wspólnego z zestawami znaków; też nie możemysqli::real_escape_string()
, ponieważ jest to tylko inne opakowanie wokół tej samej funkcji.Problem, o ile jeszcze nie jest oczywisty, polega na tym, że wezwanie do
mysql_real_escape_string()
nie może wiedzieć, z jaką literą będzie cytowany literał, ponieważ programista musi zdecydować później. Tak więc wNO_BACKSLASH_ESCAPES
trybie dosłownie nie ma sposobu, aby ta funkcja mogła bezpiecznie uciec przed każdym wejściem w celu użycia z arbitralnym cytowaniem (przynajmniej bez podwojenia znaków, które nie wymagają podwajania, a tym samym mungowania danych).Brzydki
Pogarsza się.
NO_BACKSLASH_ESCAPES
może nie być wcale tak rzadkie na wolności ze względu na konieczność użycia go do kompatybilności ze standardowym SQL (np. patrz sekcja 5.3 specyfikacji SQL-92 , a mianowicie<quote symbol> ::= <quote><quote>
produkcja gramatyki i brak jakiegokolwiek specjalnego znaczenia nadanego odwrotnemu ukośnikowi). Co więcej, jego użycie zostało wyraźnie zalecane jako obejście (dawno naprawionego) błędu opisanego w poście ircmaxell. Kto wie, niektóre DBA mogą nawet skonfigurować go tak, aby był domyślnie włączony, aby zniechęcić do używania niewłaściwych metod zmiany znaczeniaaddslashes()
.Ponadto tryb SQL nowego połączenia jest ustawiany przez serwer zgodnie z jego konfiguracją (którą
SUPER
użytkownik może zmienić w dowolnym momencie); dlatego, aby być pewnym zachowania serwera, należy zawsze wyraźnie określić pożądany tryb po połączeniu.The Saving Grace
Tak długo, jak zawsze jawnie ustawiasz tryb SQL, aby nie dołączać
NO_BACKSLASH_ESCAPES
lub cytować literałów łańcuchowych MySQL za pomocą znaku pojedynczego cudzysłowu, ten błąd nie może odwrócić swojej brzydkiej głowy: odpowiednioescape_quotes_for_mysql()
nie zostanie użyty, lub jego założenie o tym, które znaki cytatu wymagają powtarzania, będzie być poprawnym.Z tego powodu zalecam, aby każdy, kto używa,
NO_BACKSLASH_ESCAPES
również włączaANSI_QUOTES
tryb, ponieważ wymusi zwykłe używanie literałów ciągowych w cudzysłowie. Zauważ, że nie zapobiega to iniekcji SQL w przypadku użycia literałów podwójnie cytowanych - ogranicza jedynie prawdopodobieństwo takiego zdarzenia (ponieważ nie powiodłyby się normalne, nie złośliwe zapytania).W PDO
PDO::quote()
wywoływana jest zarówno jego równoważna funkcja, jak i przygotowany emulator instrukcjimysql_handle_quoter()
- co robi dokładnie to: zapewnia, że literał, który uciekł, jest cytowany w pojedynczych cudzysłowach, dzięki czemu można mieć pewność, że PDO jest zawsze odporny na ten błąd.Od wersji MySQL v5.7.6 ten błąd został naprawiony. Zobacz dziennik zmian :
Bezpieczne przykłady
W połączeniu z błędem wyjaśnionym przez ircmaxell, poniższe przykłady są całkowicie bezpieczne (zakładając, że albo używa MySQL później niż 4.1.20, 5.0.22, 5.1.11; albo że nie używa kodowania połączenia GBK / Big5) :
... ponieważ jawnie wybraliśmy tryb SQL, który nie obejmuje
NO_BACKSLASH_ESCAPES
.... ponieważ cytujemy nasz literał łańcuchowy pojedynczymi cudzysłowami.
... ponieważ instrukcje przygotowane przez PDO są odporne na tę lukę (i ircmaxell również, 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) .
... ponieważ
quote()
funkcja PDO nie tylko wymyka się dosłowności, ale także ją cytuje (w postaci pojedynczych cudzysłowów'
); Pamiętaj, że aby uniknąć ircmaxell za błąd w tym przypadku musi być za pomocą PHP≥5.3.6 i poprawnie ustawić zestaw znaków w DSN.... ponieważ instrukcje przygotowane przez MySQLi są bezpieczne.
Podsumowanie
Zatem jeśli:
LUB
LUB
w uzupełnieniu do zatrudniania jednym z rozwiązań w ircmaxell za Podsumowując, należy użyć co najmniej jednego z:
NO_BACKSLASH_ESCAPES
... powinieneś być całkowicie bezpieczny (luki poza zakresem uciekania łańcucha).
źródło
"
przede wszystkim używać ciągów znaków. SQL mówi, że to dla identyfikatorów. Ale eh ... tylko kolejny przykład MySQL-a mówiącego: „śrubuj standardy, zrobię, co chcę”. (Na szczęście możesz włączyćANSI_QUOTES
tryb poprawiania cytowania. Otwarte lekceważenie standardów jest jednak większym problemem, który może wymagać surowszych środków.)quote()
funkcji PDO - ale przygotowane oświadczenia są znacznie bezpieczniejszym i bardziej odpowiednim sposobem na uniknięcie zastrzyku. Oczywiście, jeśli bezpośrednio połączyłeś zmienne nieskalowane z SQL, to z pewnością jesteś podatny na wstrzykiwanie, bez względu na to, jakich metod użyjesz później.Cóż, tak naprawdę nic nie może przez to przejść, oprócz
%
symboli wieloznacznych. Może to być niebezpieczne, jeśli użyjeszLIKE
instrukcji, ponieważ atakujący może podać dane%
logowania, jeśli nie odfiltrujesz tego i będziesz musiał po prostu brutalnie wymusić hasło któregokolwiek z twoich użytkowników. Ludzie często sugerują użycie przygotowanych instrukcji, aby były w 100% bezpieczne, ponieważ dane nie mogą w ten sposób zakłócać samego zapytania. Ale w przypadku takich prostych zapytań prawdopodobnie bardziej efektywne byłoby zrobienie czegoś takiego$login = preg_replace('/[^a-zA-Z0-9_]/', '', $login);
źródło
more efficient
niż używanie przygotowanych wyciągów? (Przygotowane instrukcje zawsze działają, biblioteka może być szybko poprawiona w przypadku ataków, nie ujawnia błędu ludzkiego [takiego jak błędne wpisanie całego ciągu zastępującego] i ma znaczące korzyści w zakresie wydajności, jeśli instrukcja zostanie ponownie użyta.)