Ataki SQL-Injection czyli firewall i bezpieczna baza danych, to nie wszystko

Wojciech Dworakowski
wojtekd@ipsec.pl
Łukasz Luzar
lluzar@developers.of.pl

Wszyscy wiemy, że największym bogactwem każdej firmy są
przechowywane w niej dane. Przedsiębiorstwa czynią ogromne wysiłki, żeby
uchronić je od kradzieży, nieuprawnionej modyfikacji i zniszczenia. Wydaje się,
że wdrażanie zaawansowanych systemów ochrony fizycznej i informatycznej jest
obecnie podstawową gwarancją stabilnej działalności firmy. Jednak wraz z
rozwojem sieci Internet, coraz więcej firm decyduje się na wykorzystanie tego
medium do udostępniania wybranych zasobów dla swoich klientów.
Najpopularniejszym sposobem na realizację tego zadania jest interfejs WWW.
Niestety okazuje się, że udostępnienie takiej funkcjonalności może otworzyć
potencjalnemu intruzowi bardzo łatwą drogę do zdobycia oraz modyfikacji
interesujących go danych. Uruchomienie dostępu przez WWW do bazy danych
przedsiębiorstwa powoduje, że w praktyce firewall’e, IDS’y i
systemy monitoringu fizycznego przestają być warunkiem wystarczającym do
zapewnienia bezpieczeństwa zgromadzonych danych. W artykule postaramy się
przedstawić Państwu metodę ataku na systemy bazodanowe, która wymyka się
kontroli tradycyjnych systemów ochronnych.

Artykuł jest kierowany do administratorów baz danych i
administratorów bezpieczeństwa, a przede wszystkim do programistów i
projektantów tworzących interfejsy do baz danych. Czytając poniższy artykuł
warto zwrócić uwagę na specyfikę ataków SQL-injection:

  • Są to ataki wysokopoziomowe, a co za tym idzie, nie
    poddają się kontroli systemów firewall.
  • Nie są to zagrożenia wynikające z błędów twórców
    oprogramowania (np. Oracle), lecz błędy popełnione przez programistów
    wykorzystujących bazy danych.

Sprawia to, że ataki SQL-injection są bardzo groźne, zwłaszcza
gdy programiści i projektanci nie zdają sobie sprawy z ich istoty.

Warstwowy model aplikacji

Każdy z nas wykorzystuje w swojej pracy dane zgromadzone w
bazach danych. Zwykle nie robimy tego bezpośrednio – zadając zapytanie
SQL, lecz za pośrednictwem jakiegoś interfejsu do bazy danych. Zwykle całość
aplikacji składa się z trzech warstw:

  1. Klient
  2. Warstwa pośrednicząca
  3. Baza danych

Współcześnie stosuje się dwa podstawowe sposoby
konstrukcji dostępu do danych:

Klient-serwer:

  1. Aplikacja kliencka
  2. API bazy danych
  3. Baza danych

Przez przeglądarkę WWW:

  1. Przeglądarka WWW
  2. Aplikacja WWW + API bazy danych
  3. Baza danych

W każdym przypadku zleceń do bazy nie wydajemy bezpośrednio,
lecz za pośrednictwem określonego interfejsu. Zwykle dane do zapytania
wpisujemy w odpowiednim okienku, lub wybieramy z odpowiedniego menu. Warstwa pośrednicząca
(aplikacja www lub aplikacja kliencka), konstruuje z tych danych pełnoprawne
zapytanie SQL lub zlecenie DML i przesyła je do bazy danych.

Właśnie tutaj tkwi główny cel ataków SQL-injection.

SQL-injection?

SQL-injection to metoda ataku polegająca na dodaniu do
zapytania wykonywanego przez warstwę pośredniczącą dodatkowego zapytania SQL.
W rezultacie może to prowadzić do:

  • zdobycia danych, do których użytkownik nie jest
    uprawniony
  • zmodyfikowania danych, bądź dodania do bazy dodatkowych
    danych
  • obejścia mechanizmów opartych na pobieraniu danych z
    bazy (np. autoryzacji)

Poniżej pokażemy kilka przykładów wykorzystania techniki
SQL-injection.

W dalszej części artykułu skupimy się nad wykorzystaniem
tej techniki do atakowania interfejsów WWW do baz danych, jednak jest to tylko
przykład. Równie dobrze opisywaną technikę można zastosować do atakowania
aplikacji opartych, np. na oprogramowaniu klienckim na Windows korzystającym z
API Oracle.

Poniższe przykłady mają na celu zilustrować problem. Tak
więc nie będziemy ograniczali się tylko do Oracle. Zajmiemy się raczej
przypadkiem ogólnym: Baza danych + Aplikacja WWW.

Przykład 1 – pozyskanie danych

Podstawę działania SQL-injection zaprezentujemy atakując
jeden ze skryptów demo dostarczonych wraz z Oracle 9i. Skrypt ten nazywa się
sample2.jsp i jest dostępny w standardowej instalacji Oracle 9i via WWW, pod
adresem URL:

http://nasz_serwer/demo/sql/tag/sample2.jsp

Jest to prosty skrypt JSP, który pokazuje wykonanie
zapytania do bazy. Skrypt pobiera od użytkownika dane do zapytania przez prosty
formularz składający się z jednego pola tekstowego. Np. jeżeli w pole
wpiszemy: sal=800, to otrzymamy listę pracowników, których wynagrodzenie
(pole SAL) wynosi 800. Oczywiście rezultaty są produkowane przez odpowiednie
zapytanie SQL skierowane do bazy danych. Jeżeli przeanalizujemy kod to okaże
się, że to co użytkownik wpisuje w formularz web-owy jest
„doklejane” do zapytania:

SELECT ename, sal FROM scott.emp WHERE ___

Jeżeli w formularzu użytkownik wpisze „sal=800”
to skrypt skonstruuje zapytanie:

SELECT ename, sal FROM scott.emp WHERE sal=800

Zapytanie SQL jest tworzone za pomocą konkatenacji dwóch ciągów
znaków. Jeden jest zapisany na stałe w skrypcie, zaś drugi jest pobierany od
użytkownika. Użytkownik może wpisywać w formularz web-owy dowolne ciągi
znaków. Sprawdźmy co się stanie jeśli użytkownik wpisze w formularz następujący
ciąg:

sal=800 union select username, user_id from all_users

W wyniku działania skryptu JSP (sklejenia dwóch ciągów
znaków) zostanie wykonane zapytanie:

SELECT ename, sal FROM scott.emp WHERE sal=800
UNION SELECT username, user_id FROM all_users

W rezultacie użytkownik (intruz) uzyskał dostęp do widoku
all_users, a więc wykonał działanie na które twórca interfejsu nie zezwolił
bezpośrednio.

Oczywiście powyższy przykład jest bardzo prosty, jednak świetnie
ilustruje istotę ataku typu SQL-injection. Istotą tych ataków jest
wykorzystanie możliwości dopisania swojego zapytania do zapytania
przewidzianego przez interfejs dostępu do danych.

Uwagi:

  • Celem ataku był widok all_users. Było to możliwe dzięki
    temu, że użytkownik, z którego przywilejami zostało wykonane zapytanie,
    miał dostęp do tego widoku. Warto zauważyć, że uprawnienia te w żaden
    sposób nie były potrzebne do realizacji projektowanej funkcjonalności
    omawianego skryptu.
  • Z reguły, gdy potrzebna jest podobna funkcjonalność,
    nie stosuje się zapytań bezpośrednio wykonywanych w ciele skryptu, tak
    jak w powyższym wypadku. Zamiast tego używa się procedur PL/SQL które
    realizują zapytania. Oczywiście źle zaprogramowane procedury mogą również
    być podatne na ataki SQL-injection.
  • W powyższym przykładzie do doklejenia zapytania
    wykorzystaliśmy mechanizm UNION SELECT jednak metod doklejania obcych
    zapytań jest o wiele więcej.

Przykład 2 – modyfikacja danych

Przykład 1 ilustrował możliwość wykonania arbitralnego
zapytania za pomocą metody SQL-injection. Na tym możliwości tej metody się
nie kończą. W sprzyjających warunkach może ona posłużyć do zmodyfikowania
danych w bazie.

Weźmy pod uwagę fragment kodu wzięty z prawdziwej
„produkcyjnej” aplikacji. Jest to typowa strona www służącą do
uwierzytelniania się. Nasz przykład jest oparty na skryptach PHP i dostępie
do bazy PostgreSQL. Źródło strony www jest przedstawione na listingu 1.

LoginPage.php
<HTML>
<BODY>

<FORM ACTION=LoginPage2.php>

Uzytkownik: <INPUT NAME="username"><BR>
Haslo: <INPUT NAME="password"><BR>
<INPUT TYPE="submit" VALUE="Zaloguj">
</FORM>

</BODY>

Listing 1

Jest to prosty formularz, który pobiera od użytkownika
parametry „username” i „password” i wywołuje skrypt
LoginPage2.php, który ma za zadanie sprawdzić w bazie danych, czy podane przez
użytkownika login i hasło są prawidłowe.

Na listingu 2 jest przedstawiona kluczowa część tego
skryptu – funkcja przeszukująca bazę danych.

LoginPage2.php

// funkcja do autoryzacji
    function MyAuth( $conn,$username,$password) {

            $query =
"SELECT id FROM users WHERE";
            $query .= "
username = ‘" . $username . "' AND ";
            $query .= "
password = ‘" . $password . "'";
            $res =
pg_query( $query);

            if (pg_num_rows(
$res) == 1) {
                       
$row = pg_fetch_array( $res);
                       
$id = $row[‘id'];
            } else {
                       
$id = 0;
            }
            return $id;
}

Listing 2

Krytycznym fragmentem powyższego kodu jest zapytanie:

"SELECT id FROM users WHERE username = ‘"
. $username .
"' AND password = ‘"
. $password .
"'";

Podobnie jak w poprzednim wypadku zapytanie jest konstruowane
przez sklejenie części stałych, zaszytych w programie ze zmiennymi
wprowadzonymi przez użytkownika przez formularz www. Dla większej przejrzystości,
każdą ze sklejanych części przedstawiliśmy w osobnej linii.

Operatorem konkatenacji w języku PHP jest znak kropki.
Cudzysłowy ograniczają poszczególne ciągi znaków (string), a znaki
apostrofu ( ‚ ) zabezpieczają fragmenty danych wprowadzane przez użytkownika.

Atak

Jeżeli użytkownik do formularza LoginPage.php wprowadzi
następujące dane:

        Użytkownik: ‘; delete from users--
        Hasło:

To kluczowy fragment kodu PHP, po wstawieniu wprowadzonych
danych będzie wyglądał następująco:

"SELECT id FROM users WHERE username = ‘"
. ‘; delete from users-- .
"' AND password = ‘"
. .
"'";

Jakie funkcje spełniają poszczególne części
wprowadzonego zapytania?:

‘ Zakończenie pojedynczego cudzysłowu, w który
są ujmowane dane wprowadzane z wewnątrz.

; Zakończenie zapytania i rozpoczęcie nowego.

Oznacza początek komentarza. W ten sposób usuwa
się dalszą część kodu, która nie jest intruzowi potrzebna, a może
wywołać błąd składniowy i uniemożliwić wykonanie ataku.

Po przetworzeniu kodu i zinterpretowaniu powyższych znaków,
otrzymujemy dwa zapytania:

SELECT id FROM users WHERE username = "; delete from users

W rezultacie, zawartość tablicy users zostanie usunięta,
uniemożliwiając innym użytkownikom na dostęp do systemu.

Uwagi:

  • W opisany sposób można doklejać dowolne zapytania, na
    jakie pozwala API danej bazy danych. W tym SQL, DML oraz wywołania
    procedur.
  • W przypadku Oracle powyższa metoda jest nieskuteczna,
    gdyż składnia SQL Oracle nie dopuszcza do wykonywania kilku komend w
    jednej linii.

Przykład 3 – ominięcie mechanizmów uwierzytelniających

Pozostańmy przy ostatnio rozpatrywanym fragmencie kodu.
Postaramy się pokazać, jak umiejętnie manipulując wprowadzanymi danymi, można
ominąć zaprojektowany mechanizm uwierzytelniający.

Funkcja uwierzytelniająca przedstawiona na listingu 2,
wykonuje zapytanie i sprawdza ilość rekordów zwróconych przez to zapytanie.
Jeżeli zapytanie zwróciło dokładnie jeden rekord, to funkcja stwierdzi że użytkownik
wpisał poprawny login oraz hasło i zwróci identyfikator użytkownika.

Zastanówmy się co będzie jeśli użytkownik wprowadzi następujące
dane w formularzu www:

Username: ‘ or 1 = 1--

Po zinterpretowaniu kodu, skrypt wykona zapytanie:

SELECT id FROM users WHERE username = " OR 1 = 1

W rezultacie intruz będzie mógł zalogować się z prawami
pierwszego użytkownika w bazie.

W wypadku tego skryptu można zastosować o wiele groźniejszą
metodę i spróbować zalogować się jako wybrany użytkownik. Wystarczy w pole
username wpisać:

Username: administrator'--

Wykona się zapytanie:

SELECT id FROM users WHERE username = «administrator'

Uwagi:

  • Powyższe przykłady SQL-injection stosują
    wykomentowanie części kodu. Istnieją również inne metody.
  • Do tego typu zastosowań można również wykorzystać
    metodę doklejania zapytań przez UNION SELECT

Problem – struktura danych

Aby ataki były skuteczne, włamywacz musi znać jak najwięcej
szczegółów dotyczących struktury bazy danych, a w szczególności nazw tabel
i kolumn, na których wykonywane są zapytania budowane bezpośrednio z danych
wejściowych otrzymanych przez użytkownika.

Informacje o strukturze bazy, intruz może pozyskać np. za
pomocą analizy opisu błędów zwracanych przez bazę danych.

Metoda ta polega na celowym konstruowaniu zapytań, które
przy wykonaniu zwrócą błąd. Czasami informacja o błędzie może zawierać
dane o strukturze bazy.

Jeżeli w powyższym przykładzie, do formularza, w pole
username zostanie wpisany ciąg znaków:

Username: ‘ HAVING 1 = 1--

To zostanie wykonane zapytanie:

SELECT id FROM users WHERE username = " HAVING 1 = 1

Zapytanie to jest poprawne składniowo, jednak niepoprawne ze
względu na strukturę danych. W rezultacie baza zwróci komunikat:

Attribute users.id must be GROUPed or used in an aggregate function.

W ten sposób intruz uzyskał informacje o nazwie tablicy
oraz o nazwie pierwszej kolumny.

Uwaga:

  • Nazwy kolejnych kolumn można uzyskać np. przez odpowiednie
    doklejenie klauzuli GROUP BY.

Jak się zabezpieczyć?

Powyższe przykłady ilustrują tylko kilka zastosowań
techniki SQL-injection. W praktyce obszary jej użycia są bardzo rozległe.

Ataki te są tym bardziej groźne, że nie można się przed
nimi ustrzec za pomocą tradycyjnych środków ochrony infrastruktury IT. Współcześnie
stosowane firewalle to zwykle filtry pakietowe typu stateful inspection. Operują
one na 3 i 4 warstwie protokołów OSI. W związku z tym, mogą jedynie przepuścić
bądź zablokować ruch do określonej usługi. Nie mogą natomiast sprawdzać,
co jest przesyłane w kanale komunikacyjnym do tej usługi. Nie mogą więc
wykryć i zablokować ataków SQL-injection, które są kodowane na wyższych
warstwach. W przypadku interfejsów www do baz danych są to zlecenia GET i POST
protokołu HTTP.

Również mechanizmy IDS (Intrusion Detection System)
pozostają w tym wypadku bezradne. IDS-y działają wychwytując w ruchu
sieciowym pewne sygnatury ataków. Ataki SQL-injection są tworzone przez intruzów
jednorazowo, na potrzeby konkretnego zadania, w związku z tym trudno jest
opracować uniwersalne sygnatury takich ataków.

Jak w takim razie bronić się przed tą groźną metodą
ataku? Gros odpowiedzialności ciąży tutaj na projektantach i programistach
interfejsów dostępu do danych. Powinni oni stosować zasady bezpiecznego
programowania, dzięki którym ich aplikacje będą odporne na tę klasę ataków.
Główne przykazania bezpiecznego programowania przedstawiamy
w ramce:

Zasady bezpiecznego programowania interfejsów
do danych:

  • Wszystkie dane pobierane z zewnątrz powinny być
    sprawdzane odpowiednimi procedurami.
    Procedury te powinny przepuszczać tylko te znaki, które są
    dopuszczalne w danym kontekście. Powinny również odfiltrowywać pewne słowa
    specjalne takie jak np. SELECT. W powyższych przykładach uniemożliwiłoby
    to umieszczenie znaków specjalnych takich jak cudzysłów, apostrof i podwójny
    myślnik, a co
    za tym idzie – skonstruowanie działającego ataku.
  • Stosować zasadę najmniejszych przywilejów.
    W fazie projektowania należy zdefiniować najmniejszy
    zestaw informacji niezbędny do wykonania danego zadania. Przywileje dla użytkowników
    i modułów należy ustalać według tych założeń. Szczególną uwagę
    należy zwrócić na prawa dostępu do wewnętrznych struktur bazy danych.
    W przykładzie 1 można było np. odczytać zawartość
    widoku all_users, chociaż przywileje te były zbędne do prawidłowego działania
    programu.
  • Ściśle zdefiniować operacje jakie może
    wykonywać użytkownik za pośrednictwem interfejsu dostępu do danych.
    Operacje te należy precyzyjnie udokumentować.
    W przypadku operacji modyfikacji lub dodawania rekordów w bazie danych,
    należy zamknąć ich działanie w osobnych funkcjach, odseparowując
    operacje dostępu do bazy danych od warstwy prezentacji. Każda taka funkcja
    powinna mieć ściśle zdefiniowane parametry wejściowe – według
    pierwszej zasady.
  • Bezwzględnie należy usuwać wszelkie pliki
    zbędne do działania aplikacji.
    Bardzo często zdarza się, że programiści edytując
    dla przykładu plik adduser.php zapominają iż w trakcie pracy zachowali
    sobie jego wcześniejszą wersje pod nazwą adduser.php.bak lub adduser.bak.
    W takim przypadku potencjalny intruz może bardzo łatwo odszukać taki plik
    i uzyskać z niego szczegółowe informacje na temat struktury i procedur
    dostępu do bazy danych.

Cel: interfejs dostępu do danych

SQL-injection jest metodą ataku zwykle kojarzoną z
aplikacjami www. Warto zauważyć, że tę metodę da się zastosować nie tylko
w tym kontekście, ale i wszędzie tam, gdzie są pobierane dane z zewnątrz i
wykorzystywane do tworzenia zapytań do bazy. Np. jeżeli aplikacja bankowa jest
stworzona w modelu klient – serwer, a interfejs jest aplikacją napisaną
na platformę Windows, która korzysta ze zdalnej bazy za pośrednictwem
odpowiedniego API (np. ODBC), to również może być podatna na ataki
SQL-injection.

Ogólnym celem ataków SQL-injection są interfejsy dostępu
do danych. W wypadku Oracle tego typu interfejsy mogą być tworzone przy użyciu
różnych technologii:

  • JSP
  • ASP
  • PHP
  • XML, XSL i XSQL
  • Javascript
  • VB, MFC i inne bazujące na API ODBC
  • Portal i stare WebDB
  • Reports, Discoverer, Oracle Applications
  • Języki 3GL i 4GL: C, OCI, Pro*C, COBOL
  • Tradycyjne skrypty Perl i CGI
  • i inne

Żadna z tych technologii sama z siebie nie zabezpiecza przed
atakami SQL-injection. Co więcej, przeważnie intruz nie musi dokładnie znać
języka w jakim został napisany interfejs, żeby go skutecznie zaatakować.

Podsumowanie

Na przykładzie SQL-injection pokazaliśmy, że powszechnie
stosowanie mechanizmy obronne (firewall, IDS, itd) nie zawsze mogą nas obronić
przed skutecznym atakiem. Naszym celem było uzmysłowienie Państwu, że nic
nie jest w stanie zastąpić dobrej znajomości i czynnego stosowania pryncypiów
bezpiecznego programowania.

Ta klasa ataków pokazuje też, jak ważne jest zrozumienie
podstaw i istoty działania mechanizmów bazodanowych z których korzystamy.
Niestety, na skutek rozwoju technologii developerskich, powszechnego stosowania
generatorów kodu i zachłystywania się nowymi możliwościami środowisk
programistycznych, często o tych podstawach się zapomina. A to stwarza doskonałe
środowisko dla intruzów.

Przed bardzo dużym wyzwaniem stoją w tym wypadku
administratorzy bezpieczeństwa. Muszą oni polegać na odpowiedzialności i
wiedzy developerów, którzy tworzą aplikacje bazodanowe działające wewnątrz
strzeżonych przez nich sieci. Jedynymi narzędziami jakie mają oni do
dyspozycji jest audyt kodu oraz testy penetracyjne.

Jeżeli obawiamy się że znajomość zasad bezpiecznego
tworzenia środowisk bazodanowych i aplikacyjnych nie jest najmocniejszą stroną
zespołu projektowego, którym dysponujemy, to należy zastanowić się nad współpracą
z zewnętrznymi ekspertami w tej dziedzinie.

W następnym odcinku postaramy się przedstawić Państwu
rozwiązanie techniczne, które ogranicza podatność na SQL-injection nie
ingerując w kod aplikacji.

Bibliografia: