mgr inż. Grzegorz Kraszewski – TECHNOLOGIE INTERNETOWE 3 – wykład 6: „Bazy danych i bezpieczeństwo”. 6. Bezpieczeństwo przy współpracy z bazami danych 6.1. Idea ataku „SQL injection” Atak znany jako „SQL injection” jest możliwy wtedy, gdy użytkownik ma bezpośredni wpływ na postać zapytania wysyłanego do bazy danych. Dzieje się tak najczęściej wtedy, gdy zapytanie zawiera elementy wstawiane z formularza HTML. Odpowiednio preparując treść informacji wysyłanych przez formularz, wrogi użytkownik jest w stanie wejść w posiadanie nie przeznaczonych dla niego informacji. Istnieje wiele sposobów wykonania takiego ataku, tutaj omówię jedynie dwa najpopularniejsze, pierwszy za pomocą polecenia OR języka SQL, drugi za pomocą polecenia UNION SELECT. 6.2. Prosty atak przy pomocy polecenia OR Atak taki może być użyty np. w formularzu logowania na stronę internetową, jeżeli użytkownicy i hasła przechowywane są w tabeli bazy danych. Dzięki niemu możemy przejść pozytywnie proces logowania nie znając ani żadnej zarejestrowanej nazwy użytkownika, ani żadnego hasła. Typowy fragment kodu wrażliwego na taki atak będzie wyglądał następująco (jest to fragment skryptu odbierającego dane z formularza logowania): $user = $_POST['username']; $pass = $_POST['password']; mgr inż. Grzegorz Kraszewski – TECHNOLOGIE INTERNETOWE 3 – wykład 6: „Bazy danych i bezpieczeństwo”. $zapytanie = "SELECT userid FROM userzy WHERE userid=\"$user\" AND pass=\"$pass\""; $wynik = mysql_query($zapytanie); $odpowiedz = mysql_fetch_assoc($wynik); $zalogowany = $odpowiedz['userid']; Powyższy fragment kodu sprawdza zgodność hasła i umieszcza nazwę zalogowanego użytkownika w zmiennej $zalogowany. A o to w jaki prosty sposób można obejść stronę logowania. Wrogi użytkownik wpisuje następujące teksty w formularzu logowania: Użytkownik: Hasło: " OR ""=" " OR ""=" W efekcie do bazy danych zostanie wysłane następujące zapytanie: SELECT userid FROM userzy WHERE userid="" OR ""="" AND pass="" OR ""="" Takie zapytanie zwróci całą tabelę, w efekcie zostaniemy zalogowani jako pierwszy użytkownik. mgr inż. Grzegorz Kraszewski – TECHNOLOGIE INTERNETOWE 3 – wykład 6: „Bazy danych i bezpieczeństwo”. 6.3. Atak przy pomocy UNION SELECT Polecenie UNION SELECT pozwala na połączenie w jedną odpowiedź dwóch zapytań – w przypadku wykorzystania tej możliwości do ataku, pierwsza część zapytania jest nieistotna (jest to zapytanie do tabeli dostępnej użytkownikowi). Będąc wrogim użytkownikiem formułujemy pierwszą część w taki sposób, aby zwróciła zero wierszy. Druga część zapytania natomiast skierowana jest do tabeli, której dane chcemy przejąć. Siłę takiego ataku zwiększa fakt, że o ile użytkownik skryptu ma (na skutek niedopatrzenia administratora bazy i/lub serwera WWW) odpowiednie uprawnienia w bazie danych, to może zdobyć w ten sposób dostęp do bazy systemowej (w MySQL jest to baza 'mysql' z tabelami takimi jak 'mysql.user', 'mysql.db' i tak dalej). Załóżmy, że mamy do czynienia z blogiem internetowym, do którego wpisów można się dostać poprzez następujący URL: http://jakis.host.pl/mojblog?wpis=xx gdzie xx to numer wpisu. Zapytanie do bazy danych pobierające odpowiednie dane dotyczące wpisu wygląda tak: $zapytanie = "SELECT data,tytul,tresc FROM blog WHERE wpis={$_GET['wpis']}"; Zaatakujmy stronę, wpisując jako adres URL następujący tekst: mgr inż. Grzegorz Kraszewski – TECHNOLOGIE INTERNETOWE 3 – wykład 6: „Bazy danych i bezpieczeństwo”. http://jakis.host.pl/mojblog?wpis=-2 UNION SELECT user,pass FROM users Spowoduje to wysłanie do bazy danych następującego zapytania: SELECT data,tytul,tresc FROM blog WHERE wpis=-2 UNION SELECT user,pass FROM users Kolorem zielonym oznaczono tę część zapytania, która poprzez podanie nieistniejącego wpisu daje zero wierszy, zatem „przyjazna” część zapytania została zneutralizowana. Kolorem czerwonym natomiast oznaczona jest „wroga” część zapytania, sięgająca po dane do zupełnie innej tablicy. Jej pierwszy wiersz zostanie pokazany jako wpis do bloga, o ile użyto przy pobieraniu wiersza funkcji mysql_fetch_row(), ale i mysql_fetch_assoc() nie stanowi przeszkody, można wszak zapytać: UNION SELECT user AS data, pass AS tytul FROM users W ten sposób mamy pierwszego użytkownika i jego hasło. A co z następnymi? Nic prostszego, oto sięgamy po następnego: http://jakis.host.pl/mojblog?wpis=-2 UNION SELECT user,pass FROM users WHERE NOT userid="jan" gdzie rzecz jasna jan to identyfikator pierwszego użytkownika. W ten sposób używając dodatkowo AND, możemy powyciągać ilość użytkowników ograniczoną w zasadzie mgr inż. Grzegorz Kraszewski – TECHNOLOGIE INTERNETOWE 3 – wykład 6: „Bazy danych i bezpieczeństwo”. dopuszczalną długością URL-a w przeglądarce (ale kto powiedział, że musimy generować te zapytania przeglądarką?...). Wrogi użytkownik może w ten sam sposób sięgnąć do tabel systemowych bazy, zawierających dane o użytkownikach bazy, bazach, tabelach i prawach dostępu. Na przykład w MySQL mamy między innymi: mysql – baza systemowa, ● mysql.user – tabela użytkowników i ich przywilejów ● mysql.db – tabela baz danych, ● mysql.tables_priv – tabela praw dostępu do tabel. ● O ile administrator nie był na tyle czujny, żeby zablokować SELECT do tych tabel (że nie wspomnę o innych przywilejach) dla użytkownika systemu, w kontekście którego wykonywany jest skrypt, wrogi użytkownik ma szerokie pole do działania... 6.4. Przeciwdziałanie Uniemożliwienie ataku „SQL injection” polega na zablokowaniu możliwości bezpośredniego umieszczania łańcuchów tekstowych podawanych poprzez metody GET i POST protokołu HTTP w zapytaniach do bazy danych. Najskuteczniejszą metodą walki z takimi atakami jest stosowanie wyłącznie zapytań o treści statycznej (niezależnej od informacji przychodzących z sieci), jednak nie zawsze jest to możliwe. Można też przeszukiwać teksty zewnętrzne umieszczane w zapytaniach do baz danych pod kątem słów kluczowych języka SQL, jednak po pierwsze istnieje ryzyko pominięcia jakichś słów, po drugie tekst może zawierać takie słowo w kontekście zwykłych danych (np. mgr inż. Grzegorz Kraszewski – TECHNOLOGIE INTERNETOWE 3 – wykład 6: „Bazy danych i bezpieczeństwo”. użytkownik o nazwie 'union'). Generalnie wiadomo – im więcej sprawdzeń tym lepiej. Istnieją również gotowe rozwiązania do walki z SQL injection w samym PHP. 6.5. Magic Quotes Jest to automatyczna opcja w PHP, która w tablicach $_GET[] i $_POST[] automatycznie poprzedza wszystkie apostrofy, cudzysłowy, odwrotne ukośniki i znaki o kodzie 0 odwrotnym ukośnikiem. Powoduje to, że znaki te nie są traktowane przez MySQL (i inne bazy) jako znaki specjalne. Uniemożliwia to wykonanie większości ataków SQL Injection (ale nie wszystkich). Jest to opcja włączana globalnie w ustawieniach PHP. Główną zaletą Magic Quotes jest automatyzm – początkujący użytkownik nie zapomni o zabezpieczeniu. Oprócz tego jednak znajdziemy szereg wad. Po pierwsze poczucie fałszywego bezpieczeństwa – Magic Quotes nie zabezpieczają przed wszystkimi atakami. Po drugie dane są przetwarzane niezależnie od tego, czy faktycznie kierowane są do zapytań SQL – powoduje to niepotrzebny spadek wydajności skryptu i zwiększenie zapotrzebowania na pamięć. Dodatkową wadą są problemy z przenośnością – w przypadku gdy przenosimy skrypt z miejsca gdzie Magic Quotes są włączone do miejsca gdzie są wyłączone i odwrotnie. Stan Magic Quotes można sprawdzić z poziomu skryptu funkcją get_magic_quotes_gpc(). mgr inż. Grzegorz Kraszewski – TECHNOLOGIE INTERNETOWE 3 – wykład 6: „Bazy danych i bezpieczeństwo”. 6.6. mysql_real_escape_string() Ta specyficzna dla MySQL funkcja powinna być stosowana do przetwarzania wszystkich danych zewnętrznych (zwłaszcza z tablic $_GET[] i $_POST[]) wstawianych do zapytań kierowanych do bazy MySQL. Pierwszym parametrem funkcji jest przetwarzany łańcuch tekstowy, a drugim zasób identyfikujący nawiązane połączenie z bazą danych (wynik mysql_connect()). Stąd wniosek, że funkcji tej można użyć tylko mając nawiązane połączenie. Wymaganie to wynika z faktu, że funkcja bierze pod uwagę kodowanie znaków używane przez bazę (np. może to być UTF-8). Jeżeli nie podamy połączenia, przyjmowane jest domyślnie ostatnio nawiązane połączenie. Przykład użycia: $user = mysql_real_escape_string($_POST['user']); $pass = mysql_real_escape_string($_POST['pass']); $zapytanie = "SELECT user FROM users WHERE user=\"$user\" AND pass=\"$pass\""; $wynik = mysql_query($zapytanie);