Preorder drugiego tomu książki sekuraka: Wprowadzenie do bezpieczeństwa IT. -15% z kodem: sekurak-book
Bezpieczeństwo aplikacji webowych: podatności w mechanizmach uploadu
Upload plików zalicza się do najczęściej występujących funkcjonalności w webaplikacjach. Zazwyczaj wiąże się z wgrywaniem na serwer obrazków bądź dokumentów. Jest zarazem miejscem, na które bardzo chętnie patrzą pentesterzy, ze względu na liczne błędy bezpieczeństwa w implementacjach. W tym artykule przedstawimy najczęściej występujące błędy oraz pokażemy, w jaki sposób mogą zostać wykorzystane. Omówimy także sposoby obrony.
Dwa spojrzenia
Na wszystkie omawiane w tym artykule implementacje będziemy spoglądać z dwóch stron:
- server-side: jak błędy w danej implementacji wykorzystać od strony serwerowej, a mianowicie jak doprowadzić do wykonania dowolnego kodu (RCE – remote code execution) po stronie serwera,
- client-side: w przypadku ataków po stronie klienta będzie chodziło albo o wykonanie XSSa, albo o możliwość wykradnięcia danych (takich jak tokeny CSRF) z poziomu innej domeny.
Dopuszczanie wszystkich typów plików
Najbardziej podstawowa implementacja, która ciągle pojawia się w webaplikacjach. Zakładamy, że aplikacja zapisuje na serwerze plik o takim samym rozszerzeniu, jakie miał oryginalny plik, a ponadto nie jest sprawdzana zawartość pliku.
Spojrzenie server-side
Rozważmy kilka możliwych rozwiązań zapisywania pliku po stronie serwera:
Plik dostępny bezpośrednio przez URL (plik znajdujący się w webroocie)
Najprostszy możliwy wariant: użytkownik wgrywa plik o nazwie hack.php, który następnie jest dostępny pod adresem np. http://www.example.com/uploads/hack.php. Bezpośrednie odwołanie się do tego URL-a prowadzi prosto do RCE w aplikacji.
Plik w webroocie, ale bez możliwości wykonywania
Istnieje możliwość zabezpieczenia się przed ww. atakiem, uniemożliwiając wykonywanie plików .php w pojedynczym katalogu. Można to zrobić dyrektywą php_flag engine off w pliku .htaccess lub jednym z plików konfiguracyjnych Apache. Dzięki temu po odwołaniu się bezpośrednio do pliku http://www.example.com/uploads/hack.php zostanie wyświetlone jego źródło, nie zostanie jednak wykonany kod. Skoro jednak atakujący może wysyłać pliki o dowolnych nazwach, może wysłać plik .htaccess o następującej zawartości:
AddHandler application/x-httpd-php .jpeg
Spowoduje to, że pliki o rozszerzeniu .jpeg będą interpretowane przez parser PHP, tym samym obchodząc zabezpieczenie. Dopuszczanie plików o dowolnych rozszerzeniach może również być przydatne w przypadku wystąpienia błędu LFI (Local File Inclusion), jeśli w innym miejscu aplikacji pojawi się kod podobny do poniższego:
<?php include 'includes/'.$_GET['module'].'.php'; ?>
Pliki poza webrootem
Rozwiązanie, w którym uploadowane pliki znajdują się poza głównym drzewem katalogów dostępnym bezpośrednio przez URLe. Na przykład webroot aplikacji może znajdować się w /var/www, zaś uploady w /var/uploads. W tym przypadku nie zadziała już sztuczka z .htaccess, jednak nadal plik może być przydatny w przypadku LFI.
Pliki o zmienionych nazwach
Czasem programiści decydują się zapisywać plik o takim samym rozszerzeniu jak zostało wysłane, jednak zmieniana jest nazwa pliku. Czyli np. zamiast hack.php na serwerze mógłby zostać zapisany plik 4093b2546a7437d1a2ca3f9b7ac662e3.php. Jeżeli nazwa nowego pliku jest później gdzieś bezpośrednio widoczna, to taki wariant nie różni się od omówionych wcześniej. Jednak nawet jeśli nazwa nie jest bezpośrednio znana, w pewnych przypadkach ataki mogą być możliwe:
- Nazwa pliku tworzona jest w sposób przewidywalny, np. jako md5(time()). Na podstawie nagłówka Date z odpowiedzi HTTP można odgadnąć poprawną nazwę pliku.
- Inne miejsce w aplikacji pozwala na listing zawartości katalogów (na przykład w aplikacjach javowych taką możliwość daje atak XXE).
- Nowa część nazwy pliku jest doczepiana do oryginalnej, tj. zamiast hack.php powstaje plik hack_d02eae87c92ec51173c3659216d87518.php. W takim przypadku zawsze warto spróbować ataków z bajtem zerowym w nazwie pliku. Nawet w nowych wersjach PHP atak tego typu może zadziałać; jeden z błędów null-byte (w funkcji move_uploaded_file) poprawiono dopiero w marcu tego roku.
Spojrzenie client-side
eżeli uploadowany plik jest dostępny bezpośrednio przez URL, istnieje szereg rozszerzeń, których wysłanie spowoduje wykonanie XSSa. Najprościej wysłać po prostu plik .html o treści:
<script>alert(1)</script>
XSS-y są możliwe jeszcze przez wiele rozszerzeń plików, które przeglądarka interpretuje jako XML, np. .xml, .xsl, .rss, .svg, .xslt i inne. Payload, który powinien zadziałać dla dowolnego pliku opierającego się o XML, to:
<?xml version="1.0" encoding="UTF-8"?> <html xmlns="http://www.w3.org/1999/xhtml"> <script>alert(1)</script> </html>
Kolejnym sposobem na wykonanie XSS-ów są pliki Flasha (.swf). Flash, poprzez klasę ExternalInterface, może odwoływać się do silnika JS w przeglądarce. Na GitHubie można znaleźć przykładową implementację wykonywania XSSów przez .swf.
Czarna lista rozszerzeń
Standardową odpowiedzią na powyższe problemy jest wprowadzenie czarnej listy rozszerzeń, innymi słowy: jawne podane rozszerzeń, które nie są dopuszczalne. W przypadku wysłania do serwera pliku o rozszerzeniu z tej listy, odpowiada on komunikatem informującym o niepoprawnym typie. Jakie problemy mogą wiązać się z takim rozwiązaniem?
Spojrzenie server-side
Zbyt uboga czarna lista
Autorzy aplikacji nie zawsze są świadomi, które konkretnie rozszerzenia skutkują wykonaniem kodu. Jeśli na czarnej liście znajduje się tylko rozszerzenie .php, być może zabezpieczenie będzie dało się obejść, używając .php5. W przypadku serwera Microsoft IIS – czarną listę składającą się z rozszerzenia .aspx, omijamy przez .asp itp.
Plik o wielu rozszerzeniach
Problem opisywany w tym punkcie jest specyficzny dla serwera Apache. Zasadniczo istnieją w nim dwa sposoby na zdefiniowanie obsługi plików .php przez parser tego języka: dyrektywy SetHandler oraz AddHandler. Przykładowy wpis w konfiguracji z użyciem tej drugiej:
AddHandler application/x-httpd-php .php
AddHandler ma jednak swoje specyficzne zachowanie, o którym wspomina dokumentacja:
Filenames may have multiple extensions and the extension argument will be compared against each of them.
Oznacza to, że w przypadku wysłania do serwera pliku test.php.jpg, gdy na serwerze nie zdefiniowano żadnego handlera dla rozszerzeń .jpg (co jest bardzo prawdopodobne), taki plik zostanie wykonany przez parser PHP.
Co ciekawe, dyrektywa AddHandler była używana w domyślnej konfiguracji w dystrybucjach Red Hat Enterprise Linux 6/CentOS 6 oraz we wcześniejszych wersjach . Dopiero w wersji siódmej, wydanej w połowie zeszłego roku, zmieniono ją na SetHandler. Na wszelki wypadek sprawdźcie swoje konfiguracje :)
Niewrażliwość na wielkość liter
Problem dotyczy w szczególności aplikacji działających na Windowsie, gdzie wielkość liter w nazwach pliku nie jest brana pod uwagę. Możemy zatem natrafić na implementację, w której na czarnej liście znajdzie się tylko rozszerzenie .aspx, zaś plik o rozszerzeniu .ASPX zostanie już przepuszczony, co, w efekcie, doprowadzi do RCE.
Spojrzenie client-side
Zazwyczaj w przypadku stosowania czarnej listy rozszerzeń, twórcy aplikacji nie rozważają problemu ataków client-side. Z dużym prawdopodobieństwem będzie można wgrać plik o jednym z rozszerzeń wspomnianych wcześniej, tj. .html, .htm, .xml, .swf itp.
Biała lista rozszerzeń
Podejście, w którym w aplikacji jawnie podano białą listę rozszerzeń, a więc listę rozszerzeń, których upload jest dopuszczalny. Dla obrazków, biała lista mogłaby przykładowo składać się z rozszerzeń: [„jpg”, „png”, „jpeg”, „gif”, „tiff”].
Spojrzenie server-side
LFI (Local File Inclusion)
W innym miejscu aplikacji może wystąpić błąd LFI.
<?php include 'includes/'.$_GET['template'];
Jako że aplikacja nie sprawdza zawartości plików, a tylko ich rozszerzenie, zuploadowany plik „obrazka” będzie mógł zostać wykorzystany do wykonania kodu PHP.
Weryfikacja zawartości pliku
Skoro aplikacja oczekuje obrazka, można dodać w niej kod weryfikujący, czy wysłany plik rzeczywiście nim jest, wykorzystując do tego jedną z bibliotek. Zasadniczo możemy tutaj wyróżnić dwa sposoby weryfikacji:
- Sprawdzenie pliku bez modyfikacji jego zawartości. Można wówczas umieścić kod PHP w tagach EXIF lub (w przypadku niektórych formatów) dopisać go po prostu na końcu pliku.
- Przetworzenie obrazka i zapisanie pliku o nowej zawartości. Istnieją jednak znane ataki, umożliwiające umieszczenie kodu PHP po przetworzeniu pliku PNG. Należy też mieć na uwadze, że mogą występować błędy w poszczególnych implementacjach (np. znany jest błąd w funkcjach imagefromgif i imagefromjpeg w PHP, pozwalający zachować część złośliwego kodu PHP).
Spojrzenie client-side
Niewłaściwa biała lista
Biała lista dopuszczalnych rozszerzeń obrazków może zawierać .svg, który de facto jest zwykłym plikiem XML parsowanym przez przeglądarkę. Wówczas można użyć kodu wcześniej wspominanego w tym artykule i wykonać dzięki temu XSS-a.
Zuploadowanie pliku Flasha
Pliki .swf umożliwiają pewien ciekawy, specyficzny atak w przypadku, gdy aplikacja weryfikuje rozszerzenia, jednak nie sprawdza zawartości pliku. We Flashu obowiązują trochę inne zasady same origin policy niż w przypadku samego JavaScriptu. Jeśli mamy przykładowo plik .swf zahostowany na domenie www.google.com, wówczas niezależnie od tego, z jakiej domeny jest on wywoływany, może pobierać dowolne zasoby (metodami POST i GET) z domeny www.google.com. Wcześniej wspominanym skryptem xss.swf można potwierdzić, że taki atak rzeczywiście się powiedzie. Co więcej, nawet jeśli plik ma rozszerzenie .jpg, można przekonać przeglądarkę, aby interpretowała go jako plik Flasha. Załóżmy, że na domenie victim.com udało się zahostować plik xss.png, którym w rzeczywistości jest xss.swf. Wówczas na domenie kontrolowanej przez atakującego należy dodać kod:
<embed allowscriptaccess="always" src="http://www.victim.com/uploads/xss.png?a=get&c=http://www.victim.com/inny_zasob" type="application/x-shockwave-flash">
Jeśli wszystko się powiedzie, w odpowiedzi powinniśmy otrzymać odpowiedź „corss domain request ok.” (literówka zamierzona, taka sama jest w kodzie skryptu).
Metody ochrony
Dotychczas skupiliśmy się wyłącznie na różnych metodach realizacji uploadu w webaplikacjach, jak jednak powinniśmy się bronić? Wszystkie ataki (czy to server-side, czy client-side) związane były z tym, że pobrane na serwer pliki znajdują się na tym samym hoście oraz tej samej domenie, co webaplikacja. Jeśli tylko jest to możliwe, należy wydzielić osobną domenę (np. jeśli aplikacja działa w domenie www.example.com, to pliki mogą być hostowane w www.example-files.com), jak również osobną instancję serwera wyłącznie do uploadów. Dzięki temu zniwelowane zostanie ryzyko związane z wykonywaniem kodu, jak również wszelkie ataki typu XSS będą odbywały się na osobnej domenie.
Nie zawsze jednak mamy środki i możliwości, by wprowadzić takie rozwiązanie. Wówczas należy pamiętać o kilku podstawowych regułach:
- Pliki zuploadowane przez użytkowników powinny być zapisywane poza webrootem,
- Pliki nie powinny być zapisywane pod taką samą nazwą pliku, jaką wysłał użytkownik. Najlepiej użyć losowego hasza. Oryginalna nazwa pliku może być przechowywana w bazie danych. Alternatywnie cały plik może być przechowywany w bazie.
- Do odpowiedzi HTTP z pobieraniem plików wgranych wcześniej należy dodać nagłówki:
- Content-Disposition: attachment – dzięki któremu przeglądarka wyświetli monit z pobieraniem pliku,
- X-Content-Type-Options: nosniff – dzięki któremu zostaną uniemożliwione pewne ataki związane ze zgadywaniem typu dokumentu przez przeglądarki (dotyczy to w szczególności Internet Explorera).
– mb
Zakolejkowane na kindleku do poczytania:) Dzięki!:D
Bardzo dobry artykuł, dzięki.
Przy okazji chciałem wskazać jedną rzecz, która zapewne nie tylko mi przeszkadza. Dlaczego na sekuraku gdy klikam środkowym przyciskiem myszy w linki w tekście to nie otwierają się one w nowej zakładce tylko tej samej zamykajac sekuraka w polowie artykulu? Oczywiscie na wszystkich innych stronach srodkowy przycisk myszy otwiera URL w nowej zakladce. Czemu ma służyć taki override tego zachowania na sekuraku? Łapie sie na to za każdym razem :/
Paul – tak to jest popsute po naszej stronie. W wolnej chwili (uff) pewnie zrobimy mały research / fixa. Chyba że ktoś na szybko podrzuci fixa?
Tak na szybko udało mi się znaleźć coś takiego:
https://wordpress.org/support/topic/middle-click-open-link-in-new-tab-is-ignored
Nie wiem na ile pomoże to wyeliminować problem z klikaniem środkowym przyciskiem myszy. Pewnie lepsze rozwiązanie dałoby się zrobić grzebiąc w kodzie plugina Slimstat.
Poleganie na rozszerzeniach w np. takim PHP to w ogóle słaby pomysł. Dodajcie coś o finfo (http://php.net/manual/en/class.finfo.php) ;)
Przy zapisywaniu lepiej rozszerzenia olać, dać plikowi totalnie losową nazwę i wrzucić go do katalogu z lepkim nobody:nobody oraz -x
Świetny artykuł.
A ja bym się bał uploadować plik bez sprawdzenia formatu. W php od dawna jest getimagesize() podające typ pliku.
Co do plików typu doc czy innych nie do otwacia na serwerze, to polecam spakowanie i zmianę nazwy :)
Warto jeszcze jedną rzecz uwypuklić mniej doświadczonym programistom: wszelka walidacja client-side powinna być traktowana tylko jako część frontendu i mieć równolegle walidację server-side.
Weryfikacja np. rozszerzenia pliku tylko po stronie klienta w javascripcie może doprowadzić do RCE w aplikacji.
Prawda. Lepiej nie robic po stronie klienta validacji wcale. Ostatnio musialem podmieniac javascripta na stronie rwe zebym mogl zresetowac haslo.
Klient posiadał stronę w starej niezałatanej joomli z niezałatanym modułem do generowania statystyk. statystyki nie były nawet używane. Ten moduł umożliwiał ładowanie obrazków, ale posiadał również błąd który umożliwiał ładowanie plików PHP (CVE-2009-4140).
Po załadowaniu pliku PHP następowało przejęcie serwisu, infekcje komputerów odwiedzających i wysyłke SPAMu, co skutowało dodatkowo blokadami na RBL i utrate wizerunku.