Rozdział 4. Sesje PHP

13 marca 2012
1 gwiadka2 gwiazdki3 gwiazdki4 gwiazdki5 gwiazdek

Ustanawianie sesji

Bezpieczeństwo sesji to bardzo szeroka dziedzina i nic dziwnego, że sesje są bardzo częstym celem ataków. Większość z nich polega na podszywaniu się pod kogoś, tzn. atakujący próbuje zdobyć dostęp do sesji innego użytkownika udając, że nim jest.

Najważniejszą rzeczą dla napastnika jest identyfikator sesji, bez którego nie da się pod nikogo podszyć. Trzy najpopularniejsze metody zdobywania identyfikatorów sesji to:

  • Przewidywanie
  • Przechwytywanie
  • Ustanawianie

Przewidywanie polega na próbie odgadnięcia poprawnego identyfikatora sesji. Generator losowych identyfikatorów w PHP działa bardzo nieprzewidywalnie, a wiec mało prawdopodobne, żeby był to najsłabszy punkt aplikacji.

Przechwytywanie sesji to najczęstszy rodzaj ataku, który jest przeprowadzany na wiele różnych sposobów. Ponieważ identyfikatory sesji są zazwyczaj przesyłane w danych cookie jako zmienne GET, techniki te koncentrują się na atakowaniu tych metod przesyłania danych. Mimo iż wykryto kilka luk dotyczących danych cookie w zabezpieczeniach przeglądarek, to większość z nich znaleziono w Internet Explorerze, a poza tym dane cookie są nieco trudniejsze do ujawnienia niż zmienne GET. Dlatego jeśli użytkownik ma włączoną obsługę cookie, to można u niego zastosować bezpieczniejszy mechanizm przesyłania identyfikatora sesji.

Ustanowienie jest najprostszym sposobem na zdobycie poprawnego identyfikatora sesji. Mimo iż łatwo się bronić przed tego typu atakami, to jeśli mechanizm obsługi sesji składa się tylko z wywołania funkcji session_start(), aplikacja jest na nie bardzo podatna.

Aby zademonstrować, jak działa ustanawianie sesji, posłużę się poniższym skryptem session.php:

<?php

session_start();

if (!isset($_SESSION['visits']))
{
    $_SESSION['visits'] = 1;
}
else
{
    $_SESSION['visits']++;
}

echo $_SESSION['visits'];

?>

Przy pierwszej wizycie na stronie powinna zostać wyświetlona wartość 1. Przy każdej następnej powinna ona być zwiększona o jeden, co określa liczbę wizyt na stronie.

Aby ustanawianie sesji się powiodło, w trakcie operacji nie może istnieć poprawny identyfikator sesji (najlepiej usunąć dane cookie). Teraz wejdź na tę stronę dodając do jej adresu URL łańcuch ?PHPSESSID=1234. Następnie korzystając z innej przeglądarki (albo nawet innego komputera) wejdź pod ten sam adres z dodatkiem ?PHPSESSID=1234. Zauważysz, że przy pierwszej wizycie nie zostanie wyświetlona liczba 1, tylko będzie kontynuowana wcześniej zapoczątkowana sesja.

Dlaczego to może być problematyczne? Większość ataków ustanowienia sesji polega na odesłaniu użytkownika za pomocą odnośnika lub przekierowania protokołowego na stronę internetową z identyfikatorem sesji dołączonym do adresu URL. Użytkownik tego nie zauważy, ponieważ strona ta będzie działać normalnie. Ponieważ atakujący sam wybrał identyfikator, to już go zna i może go wykorzystać w celu przeprowadzenia ataku typu przechwycenie sesji.

Tego rodzaju prosty atak bardzo łatwo jest odeprzeć. Jeśli nie ma aktywnej sesji odpowiadającej danemu identyfikatorowi, to należy wygenerować go dla pewności jeszcze raz:

<?php

session_start();

if (!isset($_SESSION['initiated']))
{
    session_regenerate_id();
    $_SESSION['initiated'] = true;
}

?>

Problemem z tak prostym rozwiązaniem jest to, że atakujący może po prostu zainicjować sesję dla określonego identyfikatora, a następnie użyć tego identyfikatora do przeprowadzenia ataku.

Dlatego, aby się przed tym obronić, należy pamiętać, że przechwycenie sesji jest przydatne tylko po tym, jak użytkownik się zaloguje lub w inny sposób uzyska podwyższony stopień uprawnień. Jeśli zatem zmodyfikujemy skrypt tak, aby ponownie generował identyfikator sesji po każdej zmianie poziomu uprawnień (na przykład po weryfikacji nazwy użytkownika i hasła), to praktycznie wyeliminujemy możliwość przeprowadzenia skutecznego ataku ustanawiania sesji.

Przechwytywanie sesji

Najczęściej spotykanym rodzajem ataku na sesje jest jej przechwytywanie, które polega na zdobyciu dostępu do sesji innego użytkownika.

Podobnie jak w przypadku ustanawiania sesji, jeśli mechanizm sesji aplikacji składa się tylko z wywołania funkcji session_start(), to aplikacja ta jest podatna na atak, choć tym razem jego przeprowadzenie nie jest takie łatwe.

Zamiast koncentrować się na zapobieganiu przechwyceniu identyfikatora sesji uwagę skupimy na tym, jak zminimalizować szkody tym spowodowane. Naszym celem jest skomplikowanie możliwości podszywania się, ponieważ każda komplikacja zwiększa bezpieczeństwo. W tym celu przeanalizujemy kroki, jakie należy wykonać, aby dokonać przechwycenia sesji. W każdym z omawianych przypadków zakładamy, że identyfikator sesji został już przechwycony.

W najprostszym mechanizmie sesyjnym samo uzyskanie identyfikatora sesji wystarczy, aby dokonać udanego ataku. Aby udoskonalić zabezpieczenia, poszukamy w żądaniu HTTP czegoś, co można wykorzystać jako dodatkowy identyfikator.

Nie należy polegać na żadnych danych na poziomie TCP/IP, np. adresach IP, ponieważ te niskopoziomowe protokoły nie są przeznaczone do wykorzystania w działaniach wykonywanych na poziomie HTTP. Jeden użytkownik może w każdym żądaniu mieć inny adres oraz wielu użytkowników na raz może mieć ten sam adres IP.

Przypomnijmy typowe żądanie HTTP:

GET / HTTP/1.1
Host: example.org
User-Agent: Mozilla/5.0 Gecko
Accept: text/xml, image/png, image/jpeg, image/gif, */*
Cookie: PHPSESSID=1234

W HTTP/1.1 wymagany jest tylko nagłówek Host, a wiec wydaje się, że poleganie na czymkolwiek innym mija się z celem. Z drugiej strony jedyne czego potrzebujemy to konsekwencja, ponieważ chcemy tylko skomplikować możliwość podszywania się pod użytkowników bez utrudniania życia tym użytkownikom.

Wyobraź sobie, że po poprzednim żądaniu nadeszło inne żądanie wysłane z innej aplikacji klienckiej (User-Agent):

GET / HTTP/1.1
Host: example.org
User-Agent: Mozilla Compatible (MSIE)
Accept: text/xml, image/png, image/jpeg, image/gif, */*
Cookie: PHPSESSID=1234

Mimo iż identyfikator w cookie jest ten sam, czy powinniśmy przyjąć, że to żądanie zostało wysłane przez tego samego użytkownika? Jest raczej mało prawdopodobne, żeby przeglądarka zmieniła nagłówek User-Agent w drugim żądaniu. Dlatego zmodyfikujemy mechanizm sesji tak, aby wykonywał dodatkowy test:

<?php

session_start();

if (isset($_SESSION['HTTP_USER_AGENT']))
{
    if ($_SESSION['HTTP_USER_AGENT'] != md5($_SERVER['HTTP_USER_AGENT']))
    {
        /* Poproszenie o hasło */
        exit;
    }
}
else
{
    $_SESSION['HTTP_USER_AGENT'] = md5($_SERVER['HTTP_USER_AGENT']);
}

?>

Teraz atakujący nie tylko musi podać poprawny identyfikator sesji, ale również przesłać odpowiedni nagłówek User-Agent. To trochę komplikuje sprawę, a więc aplikacja stała się nieco bezpieczniejsza.

Czy można to jeszcze poprawić? Najczęściej używanym sposobem na zdobycie danych cookie jest wykorzystanie luk w zabezpieczeniach przeglądarki, np. Internet Explorer. Sztuka polega na zmuszeniu ofiary do wejścia na specjalnie spreparowaną stronę, za pomocą której atakujący zdobywa nagłówek User-Agent. Aby temu zapobiec, trzeba zastosować jakąś dodatkową technikę.

Wyobraź sobie, co by było gdybyśmy wymagali, aby użytkownik w każdym żądaniu przesyłał nagłówek User-Agent zaszyfrowany algorytmem MD5. Wówczas atakujący nie mógłby tak po prostu odtworzyć nagłówków znajdujących się w żądaniu ofiary, ale konieczne by było przesłanie dodatkowych informacji. Odgadnięcie konstrukcji takiego tokenu nie byłoby trudne, ale można to utrudnić poprzez dodanie do sposobu konstruowania tokenu nieco losowości:

<?php

$string = $_SERVER['HTTP_USER_AGENT'];
$string .= 'SHIFLETT';

/* Dodanie innych danych */

$fingerprint = md5($string);

?>

Pamiętając o tym, że identyfikator sesji jest przekazywany w cookie oraz, że do ujawnienia tego cookie (i prawdopodobnie wszystkich nagłówków HTTP) potrzebne jest przeprowadzenie udanego ataku, wartość $fingerprint powinniśmy przekazać jako zmienną URL. Musi ona znajdować się we wszystkich adresach URL, jakby była identyfikatorem sesji, ponieważ obie wartości będą potrzebne do tego, aby sesja mogła być kontynuowana (i wszystkie testy muszą zakończyć się pozytywnie).

Aby uprawnionych do korzystania z aplikacji użytkowników nie traktować jak kryminalistów, jeśli test się nie powiedzie, po prostu wyświetlaj prośbę o podanie hasła. Jeśli w skryptach znajdzie się błąd powodujący błędne podejrzewanie użytkownika o podszywanie się pod kogoś, prośba o podanie hasła jest najmniej szkodliwym sposobem na wyjście z tej sytuacji. W istocie użytkownikom może nawet podobać się takie dodatkowe zabezpieczenie.

Istnieje wiele różnych technik komplikacji podszywania się pod użytkowników i ochrony programów przed przechwyceniami sesji. Mam nadzieję, że zastosujesz jakieś inne zabezpieczenia oprócz wywołania funkcji session_start() oraz może wpadniesz na jakiś własny pomysł. Pamiętaj tylko, aby maksymalnie utrudnić życie złym ludziom i maksymalnie je ułatwić tym dobrym.

Niektórzy eksperci uważają, że nagłówek User-Agent nie jest wystarczająco stabilny, aby używać go w sposób opisany w tym poradniku. Przytaczają argument, że serwer proxy HTTP działający w klastrze może zmodyfikować nagłówek User-Agent inaczej niż inne serwery proxy w tym klastrze. Mimo iż nigdy sam tego nie doświadczyłem (i nie mam problemu z używaniem nagłówka User-Agent) jest to argument, który może być wart rozpatrzenia.

Wiadomo też, że treść nagłówka Accept potrafi się zmieniać z żądania na żądanie w przeglądarce Internet Explorer (w zależności od tego, czy użytkownik odświeży stronę), a więc jego nie należy używać do opisanych celów.

Autor: PHP Security Consortium

Źródło: http://phpsec.org/projects/guide/4.html

Tłumaczenie: Łukasz Piwko

Treść tej strony dostępna jest na zasadach licencji CC BY-NC-SA 2.0

1 komentarz

  1. Czy prawo Unii Europejskiej zobowiązujące do informowania o plikach cookie dotyczy też session_start()?

    Jeśli tak, to jak obejść to zobowiązanie? Niewidoczne obrazki, ukryte zmienne w plikach html czy przekazywanie dodatkowych łańcuchów w GET?

    Odpowiedz

Dyskusja

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *