-
data publikacji: 24.09.2008
Jak zabezpieczyć skrypt PHP/MySQL? Część 1: luka Arbitrary File Download (AFD)
kategoria: Skrypty server-side autor: m1chu
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:
<?phpif ( 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.pdfPierwsza 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.phpW 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.phpSpytacie 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ę.
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.pdfhttp://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.phphttp://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.phpNa 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ć?
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.
Ś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
-
One Response to “Jak zabezpieczyć skrypt PHP/MySQL? Część 1: luka Arbitrary File Download (AFD)”
Leave a Reply
Komentarze
Kategorie
Top 10
GrandBB: OpenID na vivee
Wski: Cross Processing
Kallie Akinyooye: Krótko o… Wordpress 2.7
Agressiva: Jak za pomocą js poprawić wygląd formularzy
S.Wojnowski: Jak za pomocą js poprawić wygląd formularzy
Arek: Menu “wychodzące” zza strony
Arek: Full of colours
Giuseppe Heitner: Wstęp do programowania zorientowanego obiektowo w PHP5…
Kamil: 960 Grid System
Piotrek: Rozwijany panel z odrobiną słodyczy
- 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.
- Jak zabezpieczyć skrypt PHP/MySQL? Część 1: luka Arbitrary File Download (AFD)
- Wstęp do programowania zorientowanego obiektowo w PHP5...
- DevArt i Librio - polskie wersje językowe
- Zakładki do sidebara - podpinamy
- SABRE, czyli jak się nie dać botom
- Inspirujące strony www - sierpień 2008
- Mamy rok i troszkę innych newsów
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
Polecane strony
- WordPress Theme from Scratch – Day 1: PSD
- 20 Tutorials For Creating HDR Images
- Ask the Expert – Using Wordpress to Build Large Scale Websites with Derek Herman
- Making an Interactive Picture with jQuery
- Create a Horizontal Scrolling Website
- 800 Most Wanted Free RSS Icons for Bloggers
- 45 Most Wanted Beautiful Free Hand Drawn Fonts
- Caffe-Break Themed Blog Layout Photoshop Tutorial
- Create a nature inspired layout in photoshop
- Coding a Web Design for Speed and Quality
- Beautiful Hand Drawn Websites | Abduzeedo | Graphic Design Inspiration and Photoshop Tutorials
- 40 Fresh Creative and Inspiring Photographs
- Wordpress portfolio layout | Grafpedia
- Simple Facial Photo Retouching | Grafpedia
- Create a beauty layout in 10 steps | Grafpedia


























Świetny art :]