Dobrze spreparowany kod SQL potrafi sparaliżować pracę całego serwisu WWW. Artykuł ten zapoznaje z podstawowymi metodami doklejania złośliwego kodu SQL oraz ze sposobami eliminacji tych zagrożeń.
Czym jest SQL Injection?
SQL Injection to metoda doklejania niebezpiecznego
kodu SQL do istniejącego zapytania. Najczęściej
stosuje się ją poprzez przygotowanie odpowiednio
zmodyfikowanego kodu i wklejenie go do formularza
obsługiwanego przez skrypt, którego zawartość
korzysta z bazy danych.
Włamania polegające na doklejaniu kodu SQL
bazują przede wszystkim na niezabezpieczonych
formularzach internetowych, to znaczy takich, których
zawartość nie jest w żaden sposób walidowana.
Jak wiadomo, język SQL w swoich zapytaniach
wykorzystuje znaki cudzysłowów. Jeżeli formularz
udostępniony użytkownikowi generuje kod
SQL na podstawie danych w nim zawartych, to istnieje
prawdopodobieństwo, że przesłane zapytanie
nie będzie poprawne. Co gorsza może być również
niebezpieczne.
Najczęściej do ataków SQL Injection stosuje
się odpowiednie modyfikacje tekstu wykorzystujące
znaki cudzysłowów i myślników. Wynika to
z faktu, że wszystkie łańcuchy znaków muszą być
zamknięte cudzysłowem.
Przygotowanie
środowiska testowego
Poznanie metod włamań związanych z doklejaniem
kodu SQL wymaga utworzenia skryptu, który
jest podatny na opisane tutaj techniki. Jednak
należy zwrócić uwagę na to, że celowe włamania
do serwisów WWW i niszczenie lub kopiowanie
danych jest według polskiego prawa nielegalne.
W związku z tym nie pozostaje nam nic innego,
jak stworzenie autorskiego skryptu pozwalającego
na stosowanie różnych metod doklejania danych.
Najlepszym przykładem, strategicznym z punktu widzenia
zabezpieczeń, jest z pewnością system autoryzacji
i logowania. Jest on na tyle prosty w implementacji,
że większość osób nie przywiązuje do
niego zbyt istotnej uwagi. Niestety zabezpieczenia
danego systemu oceniane są przede wszystkim
pod względem jego najsłabszego ogniwa. Formularz
logowania nie może być z pewnością
takim miejscem, warto więc poświęcić mu
sporo uwagi.
Rzadko zdarza się, że w artykułach
celowo opisywane są zagadnienia błędne
i mało bezpieczne. Celem tego tekstu jest
jednak uświadomienie Czytelnika i zapoznanie Go
z najczęściej popełnianymi błędami podczas tworzenia
kodu PHP wykorzystującego bazy danych.
Najpopularniejszą konfiguracją oprogramowania
początkujących webmasterów jest z pewnością
Apache, MySQL i PHP. Dlatego też artykuł ten traktuje
o tych właśnie technologiach.
Środowisko testowe
krok po kroku
Baza MySQL
Ponieważ tematyka SQL Injection dotyczy przede
wszystkim baz danych, warto przygotować odpowiednią
tabelę służącą do testowania dodawanego
kodu.
Tabela zawiera rekordy opisujące użytkowników
uprawnionych do autoryzacji. Wykonanie poniższego
kodu SQL powinno utworzyć tabelę zawierającą
trzy kolumny: id, login oraz pass:
CREATE TABLE \'users\' (
\'id\' INT( 11 ) NOT NULL AUTO_INCREMENT PRIMARY KEY ,
\'login\' VARCHAR( 255 ) NOT NULL ,
\'pass\' VARCHAR( 255 ) NOT NULL
);
Warto stworzyć również dwa wpisy określające
użytkowników, którzy mogą logować się do
systemu. Rekordy różnią się sposobem zapisania
hasła. Jeden z nich używa standardowej funkcji
PASSWORD, dostępnej w MySQL, drugi – czystego
tekstu pomijającego jakiekolwiek szyfrowanie.
INSERT INTO \'users\' ( \'id\' ,\'login\' ,\'pass\' ) VALUES (NULL , \'root\', PASSWORD( \'nimda\' ) );
INSERT INTO \'users\' ( \'id\' , \'login\' , \'pass\' ) VALUES (NULL , \'root\', \'nimda\');
Formularz logowania
Elementem wystawionym na ataki użytkownika jest
formularz internetowy. To właśnie dzięki niemu każdy
może wpisać złośliwy kod SQL, który może zakłócić
pracę całego serwisu WWW. Dzieje się tak,
ponieważ dane z formularza przesyłane są do skryptu
PHP korzystającego z bazy danych. Nie są one
w żaden sposób walidowane, wobec czego odpowiednia
sekwencja wpisana w pole użytkownika lub
hasła może skutecznie unieruchomić serwis.
Przykładowy formularz przedstawiony
jest na listingu 1.
Logowanie
Dane po chodzące z niego przesyłane są za pomocą metody
POST do skryptu auth.php. Formularz sam w sobie
nie jest krytycznym elementem kodu, pomaga jedynie
w przedostaniu się odpowiednio spreparowanych
danych do skryptu PHP.
Skrypt autoryzacyjny
Nadszedł czas na stworzenie „serca” systemu autoryzacji,
czyli prostego skryptu PHP pobierającego
dane z bazy i podejmującego decyzję dotyczącą
uwierzytelnienia użytkownika.
Na początku warto stworzyć prosty plik konfiguracyjny
zawierający dane umożliwiające dostęp
do bazy MySQL, w której zapisane są informacje
na temat użytkowników – listing 2.
$dbName = \"test\";
$dbLogin = \"root\";
$dbPass = \"root\";
$dbHost = \"localhost\";
$tableName = \"users\";
?>
Właściwy proces autoryzacji oddelegowany jest
do pliku auth.php, a konkretnie funkcji userAuth,
która umożliwia odczytanie informacji z bazy danych
– listing 3. Funkcja ta przyjmuje dwa argumenty:
login oraz hasło. Wartością, jaką zwraca,
jest typ logiczny opisujący stan, jakim zakończyła
się autoryzacja (true – sukces, false – porażka).
include \"db.conf.php\";
if (array_key_exists(\"pass\", $_POST) && array_key_exists(\"pass\", $_POST) )
{
$result = userAuth($_POST[\"login\"], $_POST[\"pass\"]);
if ($result)
echo \"Sukces - Uzytkownik zalogowany\";
else
echo \"Porazka - Brak autoryzacji\";
} else {
echo \"Niepoprawne wywolanie skryptu\";
}
function userAuth($login, $pass) {
global $dbHost, $dbLogin,$dbPass,$dbName, $tableName;
$connection = mysql_connect($dbHost, $dbLogin, $dbPass);
$ok = mysql_select_db($dbName, $connection);
if ($ok) {
//$sql_query = \"SELECT id FROM $tableName WHERE login=\'$login\' AND pass=\'$pass\'\";
$sql_query = \"SELECT id FROM $tableName WHERE (login=\'$login\') AND (pass=PASSWORD(\'$pass\'))\";
$result = mysql_query($sql_query, $connection);
$row = mysql_fetch_row($result);
if ($row != null)
return true;
return false;
}
print \"Brak polaczenia z baza
\";
return false;
}?
>
Wewnątrz ciała funkcji używane są argumenty pochodzące
z tablicy asocjacyjnej _POST, zawierającej
dane z przesłanego formularza.
Biorąc pod uwagę logiczny układ kodu, można
go podzielić na dwie części:
Pierwszą – decyzyjną, analizującą pola przesłane
w zapytaniu POST oraz wykonującą funkcję
userAuth. Wynik wywołania zapisany jest do
zmiennej $result, umożliwiając porównanie jej
wartości logicznej i wyświetlenie stosownych
komunikatów w przypadku poprawnej lub błędnej
autoryzacji.
Druga część – weryfikująca, odpowiedzialna
za wywołanie odpowiedniego zapytania SQL oraz
sprawdzenie, czy wskazany użytkownik ma prawo
dostępu do systemu. Do uzyskania połączenia z bazą
MySQL użyto standardowych funkcji mysql_connect
oraz mysql_select_db, odpowiedzialnych kolejno za
stworzenie instancji połączenia oraz wybór bazy danych.
Istotnym fragmentem kodu jest funkcja mysql_
query odpowiedzialna za przesłanie kwerendy
do serwera. Zapytanie SQL zapisane jest w zmiennej
$sql_query. Do celów testowych skrypt został
wyposażony w dwie różne metody sprawdzenia,
czy wskazany użytkownik istnieje w bazie. Jedna
z nich korzysta z niezakodowanych haseł, druga używa
funkcji PASSWORD. Najczęściej stosowane jest to drugie rozwiązanie, ponieważ hasła w bazie są
zazwyczaj zaszyfrowane. Zdarza się jednak, że niedoświadczony
programista nie szyfruje haseł, warto
więc również przedstawić taką możliwość. Decyzja
dotycząca sukcesu autoryzacji podejmowana
jest dzięki funkcji mysql_fetch_row.
Test poprawności działania skryptu
Gdy skrypt jest już gotowy, warto przetestować
jego działanie poprzez wpisanie danych użytkownika,
który został zdefiniowany w bazie.
Dobrym nawykiem każdego programisty jest
również próba przetestowania skryptu dla warunków,
które nie są spełnione. W tym przypadku najlepiej
podać dane użytkownika, który nie istnieje
lub złe hasło.
Analiza rozwiązania
pod kątem zagrożeń SQL Injection
Mówi się, że nie ma programów bezbłędnych, są
tylko te niewłaściwie testowane. Podobnie jest ze
skryptem stworzonym w tym artykule. Pomimo
tego, że przeszedł test sprawdzający poprawność
generowanych rezultatów, nadal zawiera bardzo
istotny błąd. Umożliwia on zalogowanie się niepowołanej
osoby do systemu. Jak można się domyślić,
błąd ten związany jest z możliwością dodania
danych do formularza.
Analizując szczegółowo kod przedstawiony na listingu 3 (konkretnie inicjalizację
zmiennej $sql_query) można zauważyć,
że generowane zapytanie SQL nie podlega żadnej
weryfikacji. Podstawiając za wartość zmiennej
$login lub $pass odpowiednią kombinację znaków
można uzyskać zupełnie nowe zapytanie.
Przykładowo, gdy do zapytania zdefiniowanego
jako:
$sql_query = \"SELECT id FROM $tableName WHERE login=\'$login\' AND pass=\'$pass\'\";
zamiast zmiennej $login wprowadzona zostanie następująca
sekwencja:
root\' --
to uzyskane zapytanie SQL będzie miało postać:
$sql_query = \"SELECT id FROM $tableName WHERE login=\' root\' -- AND pass=\'$pass\'\";
Znak podwójnego myślnika w bazach MySQL
oznacza rozpoczęcie komentarza. Skrócona forma
zapytania wygląda tak:
$sql_query = \"SELECT id FROM $tableName WHERE login=\' root\'\";
Jeżeli w systemie istnieje użytkownik o loginie
„root”, to zostanie on zalogowany bez konieczności
podania hasła. Znajomość nazwy użyt-kownika nie wymaga zbyt wielkich umiejętności.
Wystarczy rozpocząć zgadywanie od najczęściej
spotykanych kombinacji, tj.: admin, administrator,
root, moderator, mod, adm, sys itp. Dużo trudniejsze
jest zawsze odgadnięcie hasła. Na szczęście
dodany w ten sposób kod SQL znacznie uprościł
to zadanie.
Przedstawiona tutaj metoda wykorzystująca
podatność na SQL Injection nie jest jedyna. Istnieje
również możliwość wprowadzenia złośliwego
kodu poprzez hasło użytkownika (zmienna $pass).
Najłatwiej to zrobić poprzez wprowadzenie dodatkowego
warunku, który będzie zawsze spełniony.
Warunek ten należy dodatkowo złączyć za pomocą
operatora OR, tak aby AND przestało odgrywać
jakiekolwiek znaczenie w zapytaniu. Wpisanie poniższego
kodu w pole hasło:
\' OR \'1\'=\'1
wygeneruje następujące zapytanie SQL:
$sql_query = \"SELECT id FROM $tableName WHERE login=\'$login\' AND pass=\'\' OR \'1\'=\'1\'\";
Wyrażenie: AND pass=” OR '1’=’1′ jest zawsze
spełnione, więc użytkownik zostanie zalogowany
automatycznie. Podobnie jak w poprzednim przypadku,
wymagane jest jedynie podanie odpowiedniego
loginu, który istnieje w bazie.
Nie wszystkie zapytania SQL muszą być jednakowe.
Dodanie kodu SQL do systemu o zamkniętym
kodzie źródłowym jest zadaniem znacznie
trudniejszym. Nie wiedząc, w jaki sposób skonstruowane
jest zapytanie SQL, jesteśmy zmuszeni
do podjęcia różnych prób, które dopiero po
pewnym czasie mogą doprowadzić do konkretnych
rezultatów. Pocieszający jest jednak fakt, że
nawet mało doświadczona osoba może sprawdzić
czy dana strona narażona jest na tego typu
zagrożenie.
Można tego dokonać poprzez wpisywanie
do formularza niestandardowego łańcucha
znaków, to jest takiego, który zawiera apostrofy,
podwójne myślniki i znaki cudzysłowów. Jeżeli
po wysłaniu formularza otrzymamy informację
o błędzie w zapytaniu SQL lub inny dziwny
komunikat, to możemy być pewni, że formularz
nie jest walidowany i można posłużyć się nim
do dodania złośliwego kodu.
Ponieważ każdy programista najlepiej uczy się
na błędach, użyjemy zmodyfikowanej wersji zapytania
SQL, wykorzystującej tym razem funkcję
PASSWORD:
$sql_query = \"SELECT id FROM $tableName WHERE (login=\'$login\') AND (pass=PASSWORD(\'$pass\'))\";
Stworzony wcześniej kod SQL nie zadziała,
gdy dodamy go do powyższego zapytania. Spowodowane
jest to zastosowaniem kilku dodatkowych
nawiasów, które muszą być „unieszkodliwione”
za pomocą komentarza. Poprawne rozwiązanie
dla pola login zostało odpowiednio zmodyfikowane:
root\') --
Zapytanie SQL po dodaniu powyższego kodu
będzie wyglądało tak:
$sql_query = \"SELECT id FROM $tableName WHERE (login=\' root\') -- ) AND (pass=PASSWORD (\'$pass\'))\";
Podobnie jak w jednym z poprzednich przykładów,
część zapytania została skomentowana. Zinterpretowana
zostanie tylko następująca postać:
$sql_query = \"SELECT id FROM $tableName WHERE (login=\' root\')\";
Uzyskany rezultat pozwala na zalogowanie się
do systemu bez konieczności podania hasła użytkownika.
Jak łatwo się domyślić, możliwe jest również
dodanie kodu poprzez pole hasła. Zapytanie to jest
bardzo podobne do zaprezentowanego poprzednio.
W tym przypadku używane są jednak również znaki
komentarza, tak aby usunąć niepotrzebne nawiasy,
które powodowałyby błędną interpretację
kodu SQL:
\') OR \'1\'=\'1\') --
Po sklejeniu kodu zapytanie SQL będzie miało
postać:
$sql_query = \"SELECT id FROM $tableName WHERE (login=\'$login\') AND (pass=PASSWORD(\'\') OR \'1\'=\'1\') -- ))\";
Uwzględniając komentarze, całość skróci się
w następujący sposób:
$sql_query = \"SELECT id FROM $tableName WHERE (login=\'$login\') AND (pass=PASSWORD(\'\') OR \'1\'=\'1\')\";
Uwaga! Znacznik komentarza w MySQL musi
być odseparowany od treści komentarza za
pomocą przynajmniej jednej spacji. W przeciwnym
razie komentarz nie zostanie zinterpretowany.
Inne możliwe zagrożenia
Przedstawione tutaj metody wykorzystujące SQL
Injection tak naprawdę sprowadzały się tylko do
obejścia zabezpieczeń systemu logowania. Zakładając,
że za jego pośrednictwem można uzyskać
dostęp do panelu administracyjnego, istnieje prawdopodobieństwo,
że intruz zagrozi bezpieczeństwu
danych przechowywanych w systemie.
Możliwe jest jednak wykorzystanie wadliwie działającego formularza
do rzeczy niezwiązanych w żaden sposób
z przeznaczeniem naszego skryptu. Wykorzystać
do tego można fakt, że MySQL interpretuje
również wiele poleceń zawartych w jednej linii.
Przygotowując odpowiednio dodany kod, można
zmusić serwer bazy danych do wykonania operacji,
których żaden użytkownik z pewnością by nie
wykonał. Należy do nich kasowanie i czyszczenie
zawartości tabel, wstawianie dodatkowych wierszy
lub ich modyfikacja.
Wbrew pozorom nie jest to takie trudne. Znając
nazwę tabeli (w naszym przypadku Users), która
znajduje się w bazie danych, i wpisując w pole
login formularza autoryzacyjnego wartość:
\') DELETE FROM USERS --
wykonane zostanie dodatkowe polecenie SQL odpowiedzialne
za usunięcie wszystkich rekordów tabeli.
Ma ono postać:
$sql_query = \"SELECT id FROM $tableName WHERE (login=\'\') DELETE FROM USERS -- ) AND (pass=PASSWORD(\'$pass\'))\";
Uwzględniając komentarz, całość powinna wyglądać
tak:
$sql_query = \"SELECT id FROM $tableName WHERE (login=\'\') DELETE FROM USERS\";
Wykonanie polecenia SELECT nie powinno przynieść
żadnego sensownego efektu. Bardziej interesująca
jest druga część kodu, która skasuje zawartość
tabeli Users za pomocą zapytania DELETE.
Stosując odpowiednie modyfikacje przedstawionego
tutaj rozwiązania, można wykonać praktycznie
dowolne polecenie SQL. Jedynym utrudnieniem jest
jednak wymagana znajomość nazewnictwa tabel
i sposobu organizacji danych. Stosując metodę prób
i błędów lub analizując inne skrypty o otwartym
kodzie źródłowym, z pewnością można zdobyć dość
cenne informacje.
Oczywiście nie wszystkie polecenia mogą zostać
wykonane. Zależy to także od uprawnień konkretnego
użytkownika bazy danych. Tak więc wywołanie
zapytania: DROP TABLE Users, powiedzie
się tylko wtedy, gdy użytkownik MySQL ma odpowiednie
uprawnienia do usuwania tabel.
Zabezpieczanie formularzy przed SQL Injection
Mówiąc o zagrożeniach związanych z dodawaniem
kodu SQL, warto poznać metody zabezpieczeń formularzy
przed atakami zewnętrznymi. Istnieje wiele
metod, które pozwalają na wykluczenie ryzyka
SQL Injection. Najważniejsza z nich polega jednak
na walidacji danych pochodzących od użytkownika.
Już na etapie projektowania formularza powinny
być ściśle określone typy oraz rodzaje danych. Jeżeli
mają one charakter numeryczny lub można je
zdefiniować wzorcem, warto rozważyć użycie wyrażeń
regularnych.
Zakładając, że login użytkownika może być
stworzony tylko w oparciu o litery alfabetu, walidacja
może mieć następującą postać:
if (ereg(\'^[A-Za-z]+$\', $login))
{
// Dane są OK
} else {
// Sygnalizacja błędu
}
Funkcja ereg użyta w powyższym przykładzie
jest odpowiedzialna za dopasowanie wyrażenia regularnego
przekazanego w pierwszym parametrze
do zmiennej $login.
Przeprowadzona w ten sposób walidacja sprawdza
się jednak tylko dla danych wejściowych, które
są ściśle określone (tj. wiek, data urodzenia, kod
pocztowy itp.).
Bardzo często zdarza się, że użytkownik musi
mieć możliwość wpisania dowolnego ciągu znaków.
W takiej sytuacji nie można zabronić wprowadzenia
potencjalnie niebezpiecznych apostrofów
i myślników, gdyż są one często używane również
w zwykłym tekście. Z pomocą przychodzi jednak
funkcja mysql_real_escape_string(), odpowiedzialna
za kodowanie kłopotliwych znaków na ich bezpieczne
odpowiedniki.
Deklaracja funkcji jest następująca:
string mysql_real_escape_string ( string $unescaped, [resource $link ] )
gdzie:
- $unescaped – zmienna typu string, której wartość musi zostać zakodowana,
- $link – referencja do instancji połączenia z bazą danych. Gdy parametr nie zostanie podany, uwzględnione jest ostatnio otwarte połączenie za pomocą funkcji mysql_connect.
Wartością zwracaną przez funkcję jest zakodowany
ciąg znaków, który bez żadnej obawy można
przesłać do funkcji mysql_query. Proces kodowania
polega na poprzedzaniu potencjalnie niebezpiecznych
symboli znakiem backslasha (\). Podmieniane
są następujące znaki: \x00, \n, \r, \, ’, „, \x1a.
Użycie funkcji mysql_real_escape_string na ciągu
znaków, który został już wcześniej przetworzony,
spowoduje jego powtórne zakodowanie. Efektem
tego mogą być zdublowane znaki backslasha. Problem
ten występuje najczęściej wtedy, gdy dane wejściowe
były zmodyfikowane za pomocą funkcji addslashes.
Najlepiej jest wtedy usunąć wszystkie dodatkowe
znaczniki za pomocą stripslashes, a czysty tekst
zakodować za pomocą mysql_real_escape_string.
Warto również zwrócić uwagę na to, czy moduł
PHP, który mamy, ma odblokowaną opcję „Ma-gic Quotes”. Jest ona odpowiedzialna za automatyczne
dodawanie znacznika prawego ukośnika do
apostrofów oraz znaków cudzysłowu. Pozwala to
na usunięcie pewnych zagrożeń związanych z SQL
Injection w sposób automatyczny. Podczas stosowania
mysql_real_escape_string może to jednak prowadzić
do pewnych konfliktów związanych z podwojeniem
znacznika prawego ukośnika.
Magic Quotes są włączone domyślnie w większości dystrybucji PHP. Opcja ta w pewien sposób
przenosi odpowiedzialność za bezpieczeństwo skryptów na stronę serwera WWW. Można ją zablokować
lub odblokować w pliku konfiguracyjnym php.ini, a dokładnie w linii: magic_quotes_gpc.
Parametr ten przyjmuje wartość On lub Off.
Uwaga! Testowane w tym artykule skrypty uruchamiane są dla opcji:
{stala}magic_quotes_gpc = Off{/stala}
Ze względów bezpieczeństwa opcja ta powinna być jednak włączona.
Istnieje również możliwość automatycznej detekcji
wartości ustawionej w parametrze „Magic Quotes”
i podjęcia odpowiednich czynności mających
na celu wyeliminowanie powtórzeń znaków backslasha.
Można to zrobić za pomocą konstrukcji:
if(get_magic_quotes_gpc()) {
$login = stripslashes($login);
}
$login = mysql_real_escape_string($login);
Podczas usuwania zagrożeń związanych z SQL
Injection najważniejsza jest poprawna walidacja
danych. Przykładowo, jeżeli wiemy, że
dane pole tekstowe nie będzie zawierało
więcej niż X znaków, to najlepiej statycznie
ograniczyć jego wielkość. Pozwoli to na
wyeliminowanie przynajmniej części złośliwych
kodów SQL.
Na koniec warto dodać, że jeśli dane
przechowywane w bazie są później wyświetlane
na stronie internetowej, to warto
użyć funkcji htmlspecialchars, aby zamienić
wszystkie znaki specjalne HTML na ich zakodowane
odpowiedniki. W ten sposób można
uniknąć wielu niezbyt przyjemnych konsekwencji
(związanych chociażby z XSS – Cross Site
Scripting).
Zakończenie
Tematyka związana z SQL Injection jest bardzo
rozległa i trudno opisać wszystkie jej zagadnienia
w artykule. Zainteresowanych zachęcam do zapoznania
się z wielofazowymi atakami oraz metodami
wydobywania danych za pomocą SQL Injection.
Warto również dodać, że na bezpieczeństwo skryptu
ma wpływ nie tylko odpowiednia walidacja danych,
ale także przemyślana architektura aplikacji
webowej oraz odpowiednie maskowanie adresów
URL. Tego ostatniego można dokonać za pomocą
modułu mod_rewrite.