Artykuły:

  • data publikacji: 24.09.2008

    Jak zabezpieczyć skrypt PHP/MySQL? Część 1: luka Arbitrary File Download (AFD)

    kategoria: Skrypty server-side autor: m1chu

    Jak zabezpieczyć skrypt PHP/MySQL? Część 1: luka Arbitrary File Download (AFD)

    Postanowiłem podejść do sprawy zabezpieczeń w wielu wpisach. Pisanie o bezpieczeństwie w jednym byłoby długie i cholernie monotonne. Zaczniemy więc tym razem niestandardowo, bo od ataku typu Arbitrary File Download (szczerze większej publikacji o tym nie znalazłem) - z reguły rzadszym, ale bardzo niebezpiecznym, a to wszystko jak sama nazwa mówi z powodu możliwości ściągnięcia dowolnego pliku na serwerze na którym wykonywany jest odpowiedni plik php.


    Tłumacząc na polski nazwę tej luki otrzymalibyśmy mniej lub bardziej dosłownie translację w stylu "Pobieranie dowolnego pliku". Stąd moje krótkie tłumaczenie w powyższym paragrafie.

    Z teoretycznego punktu widzenia dokładniej wygląda to tak, że za pomocą parametru w adresie pliku przeznaczonego do ściągania innych plików z serwera możemy spreparować tak adres, aby pozwolił on nam ściągnąć pliki domyślnie przez programistę nieprzeznaczone do takiego zabiegu. Wiąże się to niedostateczną, a najczęściej po prostu z brakiem filtracji elementu (np. zmiennej) odpowiedzialnego za pobieranie argumentu.

    Pokażę prosty przykład pliku (nazwijmy go download.php) który jest podatny na tego typu atak:

    <?php

    if ( isset($_GET['file']) )
    {
            header('Cache-control: private');
            header('Content-Length: ' . filesize($_GET['file']));
            header('Content-Type: application/octet-stream');
            header('Content-Disposition: attachment; filename=' . basename($_GET['file'])); // nagłówek ustawiający zawartość jako załącznik
       
            readfile($_GET['file']); // ściągnięcie pliku
    }

    ?>

    I pomimo, że intencją programisty było ściąganie dzięki temu plikowi np. dokumentów PDF, to my dzięki jego niefrasobliwości bez problemu możemy wykorzystać go do... pobierania pozostałych plików PHP. Co najważniejsze - nieprzeparsowanych! Po prostu czysty kod PHP w takiej formie jaką twórca strony umieścił na serwerze. Jedyne co nas w tym wypadku ogranicza to funkcja readfile która standardowo nie nadaje się do pobierania dużych plików, a także czas wykonywania skryptu i wielkość bufora ustawiona w pliku konfiguracyjnym PHP.

    Wracając do przykładów. Na wyimaginowanym odnośniku powiedzmy linkującego do dokumentu PDF wyglądało by to tak:

    www.strona_internetowa.pl/catalog/download.php?file=specyfikacja.pdf

    Pierwsza myśl która Wam może przyjść do głowy to podmienić po prostu specyfikacja.pdf na inny plik. Standardowo mógłby być to index.php, gdyż ściągając jego zawartość otrzymamy informację o nazwach innych powiązanych z nim plików. Niekoniecznie jednak plik ten musi się znajdować w tym samym katalogu co download.php, czy specyfikacja.pdf.

    W pierwszym przypadku widzimy, że plik PHP którym ściągamy pozostałe źródła znajduje się w podkatalogu. Dlatego dla pewności możemy wypróbować dwa spreparowane linki:

    www.strona_internetowa.pl/catalog/download.php?file=index.php // plik znajdowałby się wtedy pod /catalog/index.php
    www.strona_internetowa.pl/catalog/download.php?file=../index.php // plik znajdowałby się wtedy bezpośrednio w strona_internetowa.pl/index.php

    W drugim punkcie sprawa wygląda tak, że plik specyfikacja.pdf nie musi się znajdować w tym samym katalogu co download.php pomimo, że w odnośniku nie jest podana do niego inna ścieżka. Wystarczy, że twórca strony w w.w. przeze mnie kodzie zmieni linijkę z readfile (a także linię odpowiedzialną za pobieranie wielkości pliku) na:

    <?php
    [...]
            header('Content-Length: ' . filesize('./podkatalog/kolejny/' . $_GET['file']));
    [...]
            readfile('./podkatalog/kolejny/' . $_GET['file']); // ściągnięcie pliku

    ?>

    W takim wypadku jego pliki PDF są po prostu ściągane z:

    www.strona_internetowa.pl/catalog/podkatalog/kolejny/

    I tu rodzi się kolejne rozwiązanie - używanie przejść do katalogów nadrzędnych. Jeśli w tym wypadku chcielibyśmy ściągnąć index.php z głównego katalogu to wystarczy po prostu wpisać taki adres w przeglądarce:

    www.strona_internetowa.pl/catalog/download.php?file=../../../index.php

    Spytacie może:

    W takim bądź razie skąd ja mam wiedzieć, gdzie wreszcie znajduje się ten mój pożądany plik?

    Jeśli zaczynasz się w to bawić to najprościej będzie robić to metodą prób i błędów poprzez przejścia do podkatalogów i katalogów nadrzędnych. Jeżeli któreś z kolei nie zadziała to albo skrypt jest zabezpieczony, albo po prostu nie ma dostępu do pliku którego szukasz na serwerze (tak może być niekiedy z poszukiwaniem np. systemowego /etc/passwd). Innym sposobem jest użycie jakiegoś downloadera plików w stylu wget, czy innych Windowsowych zamienników, po czym analiza struktury katalogów serwisu. Zabieg w miarę prosty (chodzi o użytkowanie tych narzędzi) więc chyba tłumaczyć nie muszę.

    Przykład działania AFD

    Jak to wygląda już tak kompletnie w praktyce życiowej. Wiele osób twierdzi, że nie jest łatwo znaleźć taki błąd. Fakt, co nie zmienia faktu, że nie jest to niemożliwe. Specjalnie dla Was dzisiaj w nocy za pomocą Google wyszukałem pięć stron podatnych na ten atak. Jedną z nich wybiorę jako przykład ukazania wyszukania strony, błędu i ostatecznie jego edukacyjnego użycia.

    1. Przeszukujemy Google...

    Możemy to oczywiście zrobić na różne sposoby. Osobiście użyje tejże wyszukiwarki, a stronę znajdywać będę poprzez frazy wyszukiwania, np. takie:

    inurl:.eu (getfile.php OR get.php OR file.php OR download.php)
    inurl:home.pl (getfile.php OR get.php OR file.php OR download.php)

    Tłumacząc, zostaną wyszukane wszelkie zindeksowane strony w domenie .eu (europejskiej) lub home.pl z co najmniej jednym plikiem podanym w nawiasie. Oczywiście można rzucić światło imaginacji i podać więcej plików które mogłyby być podanym wyżej przeze mnie źródłem ściągania, ale nam wystarczą tylko te.

    2. Znalezienie celu...

    Już na pierwszej stronie pierwszej frazy, i drugiej stronie drugiej frazy możemy znaleźć obiekty potencjalnie narażone na atak.

    http://anonymouse.org/cgi-bin/anon-www.cgi/http://www.ejls.eu/download.php?file=./issues/2007-12/MohrContiniUK.pdf

    http://anonymouse.org/cgi-bin/anon-www.cgi/http://gfp.home.pl/www/news/file.php?file=AGP_notka_prasowa.doc

    3. Spreparowanie parametru...

    Wystarczy tylko wejść pod jeden z tych adresów i spróbować ściągnąć dla przykładu index.php. Co się rzuca od razu w oczy to fakt, że w drugim przypadku prawdopodobnie plik przez nas pożądany będzie znajdować się dwa katalogi wyżej niż aktualnie jesteśmy.

    http://anonymouse.org/cgi-bin/anon-www.cgi/http://www.ejls.eu/download.php?file=index.php

    http://anonymouse.org/cgi-bin/anon-www.cgi/http://gfp.home.pl/www/news/file.php?file=../../index.php

    4. Co dalej?

    No właśnie. Nie zrobię Wam tu do końca kursu jak włamać się komuś na stronę. Bo posiadając dostęp do plików można bez problemu dostać się do bazy (o ile takowa istnieje), a co za tym idzie po prostu zostawić "OWNED" (o phishingowym wykorzystaniu chyba nie trzeba wspominać). Powiem tylko tyle, że znając PHP można bez problemu w takim momencie poznać strukturę plików, z tym co napisałem powyżej także ściągnąć je, a w tym te odpowiadające za konfiguracje czy hasła.

    http://anonymouse.org/cgi-bin/anon-www.cgi/http://gfp.home.pl/www/news/file.php?file=../../skins/default.skin.php // inny przykład linku wyciągniętego z źródła index.php
    http://anonymouse.org/cgi-bin/anon-www.cgi/http://www.ejls.eu/download.php?file=download.php // ściągnięcie pliku download.php

    Na koniec kwestia która jest priorytetem i kulminacją tego wpisu.

    Jak się do ku**y zabezpieczyć przed tym?!

    Przede wszystkim należy filtrować dane. Zawsze należy, bo nawet jak nie trzeba to prócz większej ilości operacji nic więcej się serwisowi nie stanie z tego powodu. Taka drobna dygresja. Pytanie brzmi, jak to zrobić?

    Przykład działania AFD

    I tu już wszystko zależy od inwencji webmastera. Pokaże dwa przykłady w zależności od podawanego argumentu.

    Ściąganie pliku poprzez ID i wykorzystanie bazy danych MySQL.

    Przede wszystkim musimy utworzyć prostą tabelę. Zakładam, że wszystkie pliki będą się znajdować w jednym katalogu (powiedzmy download).

    CREATE TABLE files ( file_id SMALLINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, file_name VARCHAR(255) NOT NULL ) COMMENT = 'file_id; 0-65535; klucz glowny : file_name; 0-255; niepusty', MAX_ROWS = 65535;

    Nasz zabugowany wyżej plik możemy zamienić na coś takiego (użyłem czystych funkcji PHP dla MySQL, jeśli ktoś zna rozszerzenie MySQLi albo moduł PEAR DB to nie będzie miał problemu z przepisaniem sobie tego):

    <?php
    require_once './config.php'; // plik konfiguracyjny z tablica danych do bazy danych
    require_once './mimes.inc.php'; // plik z funkcja rozpoznajaca typy mime (function getMimeType(file))

    $file                   = ( isset($_GET['file']) ? intval($_GET['file']) : 0 ); // filtracja parametru file zawierajacego domyslnie w sobie liczby calkowite

    if ( $file ) // jesli $file jest rozne od 0 tzn ze w parametrze zostalo wpisane ID w postaci cyfry
    {
            if ( $connection = mysql_connect($db['host'], $db['user'], $db['pass']) === false ) // laczenie z baza danych za pomoca danych z tablic $db pliku config.php
            {
                    print 'Blad polaczenia z baza danych: ' . mysql_error();
                    die();
            }
            mysql_select_db($db['base']); // wybor bazy danych

            $fetched                = mysql_fetch_assoc(
                    mysql_query('SELECT count(file_id) as counted, file_name
                            FROM files
                            WHERE file_id = '
    . $file . '
                            GROUP BY file_id
                                    LIMIT 1'
    )
                    ); // zapytanie zliczajace i zwracajace liczbe rekordow o file_id = $file oraz nazwe pliku o podanym ID
            if ( $fetched['counted'] == 1 ) // jesli powyzsze zapytanie zliczylo 1 wiersz tzn ze ID jest prawidlowe
            {
                    // szybka, dodatkowo filtracja poprzez zamiane encji na znaki i usuniecie ewentualny /
                    // UWAGA! filtracja dokladna wpisywanych danych powinna byc przeprowadzona przy wprowadzaniu pliku do bazy             
                    if ( strpos($fetched['file_name'], '../') === false && file_exists('./download/' . $fetched['file_name']) === true ) // sprawdzenie istnienia pliku
                    {
                            // wyslanie odpowiednich naglowkow
                            header('Cache-control: private');
                            header('Content-Length: ' . filesize('./download/' . $fetched['file_name']));
                    header('Content-Type: ' . getMimeType($fetched['file_name'])); // wyslanie odpowiedniego typu MIME pliku zwracanego przez funkcje getMimeType (do napisania ;])
                    header('Content-Disposition: attachment; filename=' . basename($fetched['file_name']));
       
                    // odczytanie pliku
                    readfile('./download/' . $fetched['file_name']);
                    }
                    else {
                            print 'Plik o podanym ID nie istnieje!'; // nie znaleziono pliku o podanym ID na serwerze, blad!
                    }
            }
            else {
                    print 'Niepoprawne ID pliku!'; // nie znaleziono pliku o podanym ID, blad!
            }
            mysql_close(); // zamkniecie polaczenia
    }
    else {
            print 'Brak ID pliku!'; // nie znaleziono pliku o podanym ID, blad!
    }
    ?>

    Należy pamiętać, że ważnym aspektem zachowania bezpieczeństwa jest także odpowiednia filtracja przy dodawaniu rekordów z nazwami plików do bazy.

    Przykład działania AFD

    Ściąganie pliku po jego nazwie.

    Tym razem zabezpieczenie będzie trzeba zrobić bardziej intuicyjnie i oparte tylko na odpowiednim filtrowaniu. Wszystko zależy tutaj od tego skąd chcemy ściągać pliki. Przede wszystkim powinniśmy zabronić jakiegokolwiek pobierania z katalogów nadrzędnych, a także z katalogów podrzędnych w których znajdują się pliki będące integralną częścią strony. Najwygodniej oczywiście byłoby ustawić jeden, konkretny katalog do ściągania. Nie mniej jednak my skupimy się na ograniczeniu do n-tej ilości podkatalogów. Ostatecznie należy pamiętać także, aby w katalogach do których będzie miał dostęp użytkownik z poziomu skryptu pobierającego nie było plików innych niż zdatne do takiego ściągnięcia, no i ewentualnie index.html z pustą stroną.

    <?php
    function afdFiltration($file, $a_extns, $pattern = '')
    {
            $file                                   = html_entity_decode(urldecode($file)); // usunięcie postaci procentowej znaków i zdekodowanie encji na znaki

            // sprawdzenie, czy wystepuje w ciagu ../ i ..\
            if ( strpos($file, '../') !== false || strpos($file, '..\') !== false )
            {
                    return '
    ';
            }
           
            if ( $pattern != '
    ' )
            {
                    if ( !preg_match($pattern, $file) ) // jesli podano wzor to sprawdzenie czy pokrywa sie on z podanym plikiem
                    {
                            return '
    ';
                    }
            }
           
            $extension                              = strtolower(end(explode(".", $file))); // rozszerzenie pliku
            if ( in_array($extension, $a_extns) === false ) // sprawdzenie czy rozszerzenie pliku pokrywa sie z dozwolonymi rozszerzeniami w skrypcie
            {
                    return '
    ';
            }
           
            if ( file_exists($file) === false ) // sprawdzenie istnienia pliku
            {
                    return '
    ';
            }

            return $file; // zwrocenie przefiltrowanego adresu pliku
    }

    require_once './mimes.inc.php'; // plik z funkcja rozpoznajaca typy mime (function getMimeType(file))

    $allowed_extensions             = array('pdf', 'txt', 'c'); // dozwolone rozszerzenia plikow
    $pattern                                = '
    '; // opcjonalny wzor do sprawdzenia poprawnosci wprowadzonego adresu pliku

    $file                                   = ( isset($_GET['file']) ? afdFiltration($_GET['file'], $allowed_extensions, $pattern) : '' ); // wywolanie funkcji filtrujacej, gdy podano argument file

    if ( $file != '' ) // w wypadku pozytywnego przejscia filtracji
    {
            header('
    Cache-control: private');
            header('
    Content-Length: ' . filesize($file));
            header('
    Content-Type: ' . getMimeType($file)); // wyslanie odpowiedniego typu MIME pliku zwracanego przez funkcje getMimeType (do napisania ;])
            header('
    Content-Disposition: attachment; filename=' . basename(strtolower(end(explode("/", $file)))));
       
            // odczytanie pliku
        readfile($file);
    }
    else {
            print '
    Nieprawidłowa nazwa lub ścieżka do pliku!'; // nie znaleziono pliku o podanym ID, blad!
    }
    ?>

    Zasada powyższego, przykładowego skryptu jest prosta. Filtrujemy w trzech poziomach. Zmieniając encje na znaki usuwamy ciąg ../ z adresu. Sprawdzamy opcjonalnie adres do pliku wg. zadanego przez nas wzorca (o ile takowy podamy) i ostatecznie porównujemy rozszerzenie pliku z dozwolonymi przez nas osobiście rozszerzeniami. Przykładem wzorca dla wyrażenia regularnego będzie np.:

    $pattern                                = '/^([a-zA-Z0-9\_\-]+)\\.([a-z0-9]{2,4})$/';

    Pozwala ono na ściąganie dowolnego pliku zawierającego w sobie duże i małe litery, cyfry oraz znaki _ i -, a także rozszerzenie będące małymi literami i/lub cyframi o długości od dwóch do czterech znaków.

    Na koniec przekazuje kilku minutowy filmik obrazujący znalezienie i użycie błędu. Zapraszam tutaj.

    Uprzejmie proszę też o komentarze, jak wygląda ten tutorial, czy jest przydatny i przede wszystkim czy taka forma i taka ilość informacji będzie odpowiednia dla przyszłych artykułów z tego zakresu.

    PS: wszelkie znalezione i ukazane błędy zostały przedstawione tylko i wyłącznie w formie edukacyjnej. Nie odpowiadam za formę wykorzystania ich przez użytkowników. Błędy zgłosiłem twórcą stron, więc prędzej czy później mogą one zostać załatane. Proszę o rozsądne ich wykorzystanie przy kształceniu własnych możliwości i NIE NISZCZENIE pracy autorów tych stron..

    copyright © 2008, m1chu

    udostępnione na licencji CC dla vivee.info i m1chu.eu

    Udostępnij ten artykuł:
    • Print
    • Digg
    • del.icio.us
    • Facebook
    • Mixx
    • Google Bookmarks
    • Gwar
    • RSS
    • Technorati
    • Twitter
    • Wykop

  • This website uses IntenseDebate comments, but they are not currently loaded because either your browser doesn't support JavaScript, or they didn't load fast enough.

    One Response to “Jak zabezpieczyć skrypt PHP/MySQL? Część 1: luka Arbitrary File Download (AFD)”

    1. dtteam pisze:

      Świetny art :]

    Leave a Reply

Komentarze

Kategorie

Top 10

  • CMSy Artykuły dotyczące różnego rodzaju systemów zarządzania treścią CMS.
  • Flash Podstawowe informacje pomocne przy tworzeniu animacji w Adobe Flash.
  • Fotografia Samouczki dotyczące nie tylko robienia zdjęć ale też ich cyfrowej obróbki.
  • Grafika Ogólnie pojęta grafika komputerowa, od inspiracji, po tworzenie layoutów oraz mniejszych form graficznych.
  • Inne tutoriale wordpressowe Wszystkie inne zagadnienia dotyczące WordPressa.
  • Inspiracje Inspirujące materiały graficzne z dziedziny projektowania stron www, projektów DTP, digital painting, itp.
  • Obróbka zdjęć Techniki retuszu fotografii cyfrowych.
  • Rysunek Tworzenie rysowanych ilustracji w Adobe Photoshop.
  • Skóry do Wordpressa Darmowe oraz płatne – najlepsze skóry do WordPressa.
  • Skrypty client-side
  • Skrypty server-side
  • Tutoriale
  • Webdesign Tutoriale z zakresu projektowania stron www i grafiki użytkowej na rzecz internetu.
  • Wieczór z Open Source
  • Wordpress Ulubieniec naszej publiczności CMS WordPress: nowości, tricki, wtyczki, skóry i wszystko to co może przydać się przy korzystaniu z tego systemu.
  • Wtyczki do Wordpressa Recenzje oraz instrukcje najbardziej popularnych i najbardziej niezbędnych pluginów do WordPressa.
  • XHTML/CSS Ciekawostki z dziedziny kodowania stron www: XHTML, CSS, jQuery.

Najnowsze newsy

Wieczór z Open Source 2010

Jak co roku WSINF organizuje konferencję Wieczór z Open Source.
Chciałbym zaprosić was na tegoroczna konferencję Wieczór z Open Source 2010! Czytaj dalej

Grafart.org i WACOM zapraszają na konkursy!

Witam!

Myślę, że czas najwyższy nadmienić, że za niedługi czas zostanie zorganizowany pierwszy z trzech konkursów, w których główną nagrodą będą tablety firmy WACOM!
Za miejsca drugie oraz trzecie nagrodami będą kubki i koszulki firmy WACOM oraz magazyny graficzne PSD PHOTOSHOP oraz COMPUTERARTS.
Czytaj dalej

Zapraszamy na forum graficzne Graffika.pl

Każda osoba interesująca się grafiką komputerową ma czasami ochotę porozmawiać o swoich pracach, posłuchać rad, krytyki i pochwał innych osób. Idealnym miejscem na realizowanie takich potrzeb jest forum graficzne Graffika.pl.
Czytaj dalej