W każdym systemie operacyjnym istnieje coś takiego jak drzewo katalogów. W nich umieszczone są pliki. W tym także i nasze skrypty. Często zdarza się, że wczytujemy z tego drzewa różne pliki przy pomocy PHP. Możemy je edytować, zapisywać, usuwać lub po prostu wczytywać do wykonania. Czasem zdarza się, że na podstawie różnych danych ustalamy co wczytać lub zapisać.
W takiej sytuacji pobieramy dane od użytkownika, łączymy z jakimś ciągiem znaków i wywołujemy skrypt. I tutaj błąd! Nigdy nie należy ufać danym przesłanym przez użytkownika. Postaram się wytłumaczyć zasadę działania całego błędu na przykładzie prostego skryptu wczytującego odpowiednie moduły:
<?php
function getModule($sModuleName)
{
require_once($sModuleName.'.php');
$module = new $sModuleName;
return $module;
}
getModule($_GET['module']);
?>
Wydawałoby się, że wszystko jest w porządku. Nasz mały skrypt pobiera nazwę modułu, wczytuje plik z klasą i tworzy obiekt tej klasy. Jednak nie wszystko jest w porządku. Co się stanie w przypadku, gdy ktoś o złych zamiarach, zamiast zwykłego parametru poda coś w stylu „http://example.com/somebadscript”? Przypadkiem wczytamy plik z zewnętrznego serwera, a następnie wykonamy z niego kod. Teraz atakujący ma pełną władzę nad tym, co dzieje się w skrypcie, posiada dostęp do naszego połączenia z bazą danych... Jeden błąd, a tak tragiczny w skutkach!
Zacznijmy zabezpieczać. Na początek ograniczmy dostęp do folderów spoza naszego serwera. Jak to zrobić? Bardzo proste. Dwie najlepsze metody to wyłączenie w php.ini dyrektywy allow_url_include oraz dodanie przed ścieżką do pliku odwołania do lokalnego katalogu. Warto wykonać obydwie te czynności, jednak wystarczy właściwie zmienić skrypt w sposób pokazany poniżej:
<?php
function getModule($sModuleName)
{
require_once('./'.$sModuleName);
$module = new $sModuleName;
return $module;
}
getModule($_GET['module']);
?>
Od tej pory nie wykonamy już żadnego zewnętrznego skryptu. Jednak dziura nadal istnieje! Atakujący nadal posiada dostęp do wszystkich plików na naszym serwerze... Dlaczego? Przecież może podać ścieżkę do katalogu nadrzędnego! Wczytajmy sobie teraz /etc/passwd:
GET script.php?module=../.../../../etc/passwd
Skutek po raz kolejny okazuje się opłakany. Treść naszego /etc/passwd zostaje przekazana atakującemu. W ten sposób można dostać się do niemal każdego pliku w systemie (szczególnie narażony jest Windows, który dość kiepsko radzi sobie z uprawnieniami – nie wiem jak to wygląda w wersji serwerowej). Kontynuujmy zatem łatanie naszego skryptu. Musimy się zastanowić, co tak naprawdę pozwala atakującemu na podglądanie naszych plików. Odpowiedź jest bardzo prosta. Aby dostać się do pliku wystarczyło odwołać się do nadrzędnego katalogu. Zrobienie tego polegało na użyciu ciągu znaków "../" w parametrze zapytania.
Kontynuujmy więc łatanie naszego skryptu. W PHP istnieje bardzo przyjemna funkcja basename(), która pozwala na ograniczenie dostępu do pojedynczego pliku z lokalnego katalogu. Jak działa? Usuwa po prostu wszystkie dane o katalogach url'ach z parametru i zwraca taką okrojoną nazwę. Wykorzystajmy to:
<?php
function getModule($sModuleName)
{
require_once('./modules/'.basename($sModuleName).'.php');
$module = new $sModuleName;
return $module;
}
getModule($_GET['module']);
?>
W ten sposób zabezpieczony skrypt pozwoli na wczytanie plików tylko z katalogu modules. Nie ma możliwości „wyjścia z katalogu” przy pomocy directory traversal. Istnieją oczywiście inne metody zabezpieczania się przed tym atakiem. Jednym z nich jest używanie instrukcji warunkowej dla każdego modułu lub sprawdzanie występowania nazwy danego modułu w z góry zdefiniowanej tablicy modułów.
Pokazałem jedynie dwa przykłady z całej listy możliwości ataku. Należy pamiętać, że dane od użytkownika to nie tylko tablice $_GET i $_POST. Więcej na ten temat z pewnością umieszczę na blogu w oddzielnym wpisie. Poza tym należy szczególnie uważać podczas stosowania także takich funkcji jak fopen(), file() czy unlink(). Na zakończenie warto podsumować zdobyte informacje.
RFI (Remote File Inclusion) to błąd polegający na wczytaniu zewnętrznego pliku z serwera atakującego i wykonaniu go. Sposób na zabezpieczenie skryptu jest banalny, wystarczy wszystkie nazwy plików filtrować dzięki funkcji basename(). Jeżeli w ogóle nie potrzebujemy wczytywania plików PHP z zewnętrznych domen, możemy ustawić dyrektywę php.ini allow_url_include na false.
Directory Traversal to błąd pozwalający atakującemu na modyfikację lub wczytywanie pliku spoza dozwolonych. Polega na wykorzystaniu znaków powrotu do poprzedniego katalogu lub przejścia do katalogów podrzędnych w adresie pliku do wczytania.
Jakie jeszcze znacie metody użycia Directory Traversal? Spotkaliście się kiedyś z tego typu błędami w jakichś serwisach? A może sami go popełniliście/popełnialiście?
PS: Dodałem trackback do wpisu BTM'a o RFI.