Projekt
Pewien czas temu pracowałem nad projektem dla sporej firmy, której danych nie chcę w tym momencie ujawniać. Na potrzeby tego wpisu możesz sobie wyobrazić aplikację, która pozwała użytkownikom wrzucać i wyświetlać różnego rodzaju aktualności – w tym dane statystyczne i liczbowe. W tym celu zaimplementowaliśmy możliwość wrzucania na serwer plików. Pliki te z założenia (wymagania od klienta) miały być archiwami ZIP, w środku których znajdowały się wyłącznie pliki tekstowe. Nazwa pliku była identyfikatorem, a treść najczęściej miała format CSV, czyli były to różne liczby lub tekst oddzielone od siebie przecinkami. Po wgraniu takiego archiwum, system miał je automatycznie rozpakować, a następnie od razu wyświetlić użytkownikowi ich treść.Potencjalne dziury
Spostrzegawczy czytelnik na pewno dostrzega kilka miejsc, w których może znajdować się potencjalny błąd bezpieczeństwa:- pliki w archiwum nie są tekstowe – wykonanie kodu?
- nazwy plików wyświetlane na stronie – miejsce na XSS?
- treść pliku wyświetlana na stronie – XSS?
Omówienie kodu
Jest to uproszczona wersja tego fragmentu aplikacji napisana w PHP ( :( ). Pominąłem kilka nieistotnych operacji:<?php uploadedsheetarchive=uploaded_sheet_archive = uploadedsheetarchive=_FILES['sheet_file']['tmp_name']; if (uploaded_sheet_archive) { sheets_dir = sys_get_temp_dir() . '/' . uniqid('archive', true); system('unzip -qq ' . escapeshellarg($uploaded_sheet_archive) . ' -d ' . $sheets_dir); $uploaded_sheets = array_values(array_diff(scandir($sheets_dir), array('.', '..'))); foreach($uploaded_sheets as $key => $file) { echo '<section>'; echo '<h2>File ' . ($key+1) . '</h2>'; echo '<pre>' . htmlentities(file_get_contents($sheets_dir . '/' . $file)) . '</pre>'; echo '</section>'; } system('rm -rf ' . escapeshellarg($sheets_dir)); } ?>
<form method="post" enctype="multipart/form-data" action="upload.php"> <input type="file" name="sheet_file"> <input type="submit" value="Send" accept=".zip"> </form>
Zamiast system
można równie dobrze użyć exec
, a zamiast unzip
— tar
. Zależnie od tego co oferuje akurat serwer, warto spróbować zamiennie kilku opcji.
- 5: sprawdź czy był wgrywany plik
- 6: wygeneruj ścieżkę do tymczasowego folderu o losowej nazwie, aby w nim rozpakować archiwum
- 8: rozpakuj plik używając systemowego unzip
- 10: pobierz listę plików z archiwum, pomiń
.
oraz..
- 12–17: dla każdego pliku, wyświetl jego treść
- 19: skasuj rozpakowane archiwum
- 24–17: formularz do wrzucania plików
escapeshellarg
, htmlentities
). Tak wygląda skrypt po uruchomieniu:
Błąd
Na czym polega błąd? Częściowo na bezmyślnym skopiowaniu kodu ze StackOverflow („Jak rozpakować ZIP w PHP?”), częściowo na użyciu funkcji system, ale głównie na braku weryfikacji czy archiwum nie zawiera symlinków.Symlink
Co to jest symlink? Skrót od symbolic link – jest to wskazanie na inny plik lub folder. Można powiedzieć „skrót”, choć technicznie rzecz biorąc to różne pojęcia. W każdym razie, symlink wskazuje na inny plik. Gdy próbujemy otworzyć symlink, to tak jakbyśmy otwierali ten inny plik – super, prawda? :)Symlink do /etc/passwd
Co się stanie, jeśli w archiwum umieścimy symlink wskazujący na przykład na plik /etc/passwd
? Sprawdźmy to! Jeśli chcesz sama/sam przetestować działanie skryptu, skopiuj powyższy kod do pliku i w tym samym folderze uruchom serwer PHP poleceniem php -S localhost:8081
. Następnie odwiedź stronę http://localhost:8081/upload.php .
Na początek poprawne archiwum zawierające dwa pliki tekstowe. W drugim pliku umieściłem dodatkowo fragment HTML-a, aby pokazać, że znaczniki są poprawnie ignorowane:
Teraz preparuję złośliwe archiwum z symlinkiem do /etc/passwd
:
ln -s /etc/passwd ./moj-link
zip --symlinks -r -X archiwum.zip inny-plik.txt plik1.txt moj-link
Widoczna jest treść plik /etc/passwd
, a w nim – nawet konto roota oraz wiele innych ciekawych rzeczy…
Inne zastosowania?
Właściwie możliwe jest teraz odczytanie dowolnego pliku z dysku. Oczywiście, poprawna konfiguracja użytkowników i uprawnień powinna to uniemożliwić. Czy wtedy atak jest bezużyteczny? Ależ nie! Nadal możemy przecież, metodą prób i błędów, albo bazując na jakichś innych przesłankach, odczytać dowolne pliki źródłowe aplikacji – w tym potencjalnie np. hasła do bazy danych.Przykładowo, jeśli wiesz, że ten aplikacja na hostingu od MyDevil.net to najprawdopodobniej ścieżka do głównego folderu to /home/moj-login/domains/moja-domena.com/public_html
– łatwo można się o tym dowiedzieć jeśli po prostu założy się tam konto lub poczyta dokumentację. A wtedy spreparowanie odpowiedniego symlinka nie jest trudne i po wgraniu archiwum odczytujemy np. kod źródłowy pliku upload.php
:
Jak naprawić błąd?
Zrezygnować z wywołań funkcjisystem
na rzecz innych, wbudowanych. Wpuszczenie niepowołanego kodu do funkcji system
może mieć katastrofalne skutki. W tym przypadku do rozpakowania archiwum można skorzystać z klasy ZipArchive
:
$zip = new ZipArchive;
$res = $zip->open($uploaded_sheet_archive);
$zip->extractTo($sheets_dir);
$zip->close();
Ale przede wszystkim w tym przypadku: sprawdzać czy plik nie jest przypadkiem symlinkiem zanim się go otworzy – np. przez funkcję is_link()
.
Podsumowanie
Chciałem napisać o tym ciekawym wektorze ataku, gdyż naprawdę natrafiliśmy na taki błąd, a korzystanie z funkcjisystem('unzip …')
jest nagminne – nie tylko w PHP, ale też z jej odpowiedników w wielu innych językach (również NodeJS!). Mam nadzieję, że i Ciebie zainteresował ten sposób atakowania aplikacji.
Ale przede wszystkim morał z tego jest nieco inny: Nie kopiuj bezmyślnie kodu z Internetu :) Czytaj dokumentację, sprawdzaj dokładnie co robi dany kod i staraj się przewidzieć wszelkie konsekwencje.