
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ę.

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.
-
-
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.
-
- 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
-
- 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ć?

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; nie pusty', 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órcom 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..
Zapraszam do zapoznania się także z powiązanymi artykułami:
Wpis ten został opublikowany dnia:
czwartek, 11 Wrzesień 2008 o godzinie 1:05
w działach Hacking, MySQL, PHP, Webhosting.
Możesz śledzić rozwój tematu, w tym odpowiedzi dla tego artykułu poprzez kanał informacyjny RSS 2.0.
Możesz także zostawić swój komentarz lub trackbackować ze swojej własnej strony.
Bardzo ciekawy art :D Prawdę mówiąc nie słyszałem o tym ataku, ale przy jego pomocy paroma kliknięciami można przejąć bazę ;]
Na prawdę m1chu dobra robota, bardzo ciekawie wytłumaczony sposób ataku i co najważniejsze jak się przed nim bronić ;] Filmik też zrobiony fajnie ;]
Czekam z niecierpliwością na kolejne części i czekam na fajny arcik o wyrażeniach regularnych w PHP ;D
Pozdrawiam,
gruch4
A myślałem, że będziesz pierwszą osobą która pobije mnie za rozmiar filmiku. Robiony na szybko, jeśli uda mi się go jakoś skompresować to postaram się to zrobić.
Dzięki za miłe słowa – postaram się coś w miarę szybko skrobnąć :]
Pozdrawiam,
m1chu
Super art, bardzo mi się podoba. Widać, że nie pisany w przerwie na kawę ;)
Zauważyłem jednak małe niedopatrzenie w filtracji przed directory transversal. Dzięki czemu da się ominąć twój kod.
$file = str_replace('../', '', $file);To raczej pseudo zabezpieczenie. Jeśli $_GET['file'] == ‘…/./a’ to po przefiltrowaniu otrzymujemy ../a
Powinno się raczej wywalać komunikat po znalezieniu ../ i pominąć download.
Pomimo tego artykuł bardzo fajny, przyjemny język. ogólnie tak trzymać ;]
A rozmawiałem właśnie o tej linijce i jej logiczności na kanale IRC :]
Faktycznie masz rację. Można ją ominąć w wyżej wymieniony przez Ciebie sposób. Dziękuje za zwrócenie uwagi. Zmieniłem w obydwu typach zabezpieczania ją na użycie strpos i w wypadku błędu zaprzestanie ściągania danego pliku z odpowiednią informacją. Mam nadzieję, że teraz się nie pomyliłem :D
Pozdrawiam,
m1chu
Więcej błędów nie stwierdzono :D
Teraz tylko czekam na następną część :>
Pozdrawiam,
luq
Zgłosiłem administracji gfp.home.pl błąd, po czym miły Pan i ponownie miła Pani podziękowali i jak widzę poprawili błąd. Nie mniej jednak artykułu nie będę poprawiał, bo strona może robić za przykład w wyszukiwaniu podatnych serwisów.
Wiele już w PHP napisałem, ale nawet mój pierwszy w życiu projekt nie byłby podatny na coś takiego. Nigdy o AFD nie słyszałem, ale by napisać kod podatny na coś takiego powiedzmy szczerze: trzeba być idiotą.
Fajne, tylko popraw Zpreparowanie na Spreparowanie. :)
Huh, nie sądziłem że są tak nieprzewidujący programiści :| Przecież wszystko z $_GET czy $_POST trzeba filtrować.
I tak to jest jak bezmyslni przedsiebiorcy skorzystaja z witryny zlecenia.przez.net, lub podobnej tylko po to by zaoszdzedzic pare groszy, zamiast udac sie do odpowiedniej agencji, która sie zajmuje tym profesjonalnie.
Pelno jest przeciez ludzi, ktorzy sie oglaszaja, ze zrobia jakis projekt za grosze – a pozniej tak to wyglada.
Panowie macie rację. Niestety spora część luk dotyczy właśnie braku niż rzadziej nieodpowiedniej filtracji. Także, jeśli już to do sporej części luk można by przypiąć metkę „programista-idiota”. Po za tym tcpl co do tego co napisałeś. To jest Polska i takim samym idiotyzmem jak pogoń za papierkiem w naszym kraju i przyjmowanie do pracy względem właśnie tego często bezwartościowego świadectwa, a nie umiejętności. Takie porównanie, też na czasie. I tak samo bezmyślne w swoim założeniu.
Magnes poprawiłem. Dzięki za zwrócenie uwagi. Definitywnie mój błąd :D
Artyluł napisany przystępnie i nienużąco. Myślę, że nawet początkujący jest w stanie wszystko z niego zrozumieć.
PS. Popraw zdanie „Stąd moja krótkie tłumaczenie w powyższym paragrafię.” bo Ci się w nim aż dwie literówki wkradły :)
Dziękuje za wskazanie błędu Attis. Poprawiłem :]
Moja propozycja
$plik = isset($_GET['filename']) ? $_GET['filename'] : NULL; if ($plik === NULL) { throw new Exception('Brak parametru'); } $katalogDownload = '/sciezka/do/mojego/katalogu'; $zadanyPlik = realpath($katalogDownload.DIRECTORY_SEPARATOR.$plik); if ($zadanyPlik === false || dirname($zadanyPlik) !== $katalogDownload) { throw new Exception('Nieprawidłowa ścieżka do pliku); } //dalej pobieranie plikuDzieki temu mam pewność, że plik który chciałem pobrać rzeczywiście znajduje się w odpowiednim katalogu, niezależnie od zawartości przesłanego parametru.
Mam kiepskie doświadczenia z używaniem zabezpieczeń w formie „lista plików w bazie danych.” ponieważ jak zaczyna coś nie pasować i kombinujemy ręcznie to nagle zaczyna się okazywać, że mamy problemy ze spójnością „baza” – „katalog”. Jednak moim zdaniem warto byłoby po prostu filtrować przez basename czy realpath.
Jeśli zarządza się plikami z poziomu skryptu, a nie z poziomu np. phpMyAdmin to nie ma żadnego problemu ze spójnością danych na linii katalog – baza. O ile tenże skrypt jest poprawnie napisany. W tym drugim wypadku też wystarczy pamiętać, aby usunąć i odpowiedni wiersz z tabeli, i odpowiedni plik.
Wszystko to nie zmienia faktu, że jest to z podanych dotychczas przykładów najbardziej bezpieczny i pewny sposób na uniknięcie powyższej luki. Z konkretnego powodu. Użytkownik ściąga tylko te pliki na których pobranie mu pozwolimy. We wszelkich innych przypadkach ograniczamy go co najwyżej do użytkowania plików z danego katalogu/katalogów, ewentualnie o zadanych rozszerzeniach. Oczywiście można by np. ręcznie, z poziomu skryptu deklarować dozwolone do użytku pliki z ich rozszerzeniami, ale to pomysł idący na około, wręcz idiotyczny.
Nie mniej jednak solucja z realpath() (dokładniejszy zamiennik str_replace() pokazanego w moim drugim przykładzie) w w.w. zastosowaniu też chroni przed luką dopóki administrator serwera nie popełni błędu i nie wrzuci nieodpowiednich plików do katalogu z downloadem.
[...] Jak zabezpieczyć skrypt PHP/MySQL? Część 1: luka Arbitrary File Download (AFD) (11.09.2008) [...]
Ciekawy artykuł. Do poprawki zdanie z PSa. Zamiast „Błędy zgłosiłem twórcą stron” ma być „Błędy zgłosiłem twórcom stron”. To popularny błąd u ludzi, którzy cenią tzw. hiperpoprawność. Niestety – ortograf jak się patrzy :)
Faktycznie. Twórcą możesz być Ty, ja, każda pojedyncza osoba, a nie grupa. Błąd oczywiście poprawiłem. Dzięki za jego wskazanie :]
Co do hiperpoprawności. Nie widzę przeciwwskazań jeżeli ktoś się takową dewizą kieruje, o ile nie robi tego z jakimś przesadnym chełpieniem. Sam na pewno taką osobą nie jestem, bo po prostu robię za dużo błędów (szczególnie w kwestii znaków interpunkcyjnych). Po prostu staram się pisać tak, żeby dało się to czytać bez włączania jakiegoś dodatkowego deszyfratora ;]
Szczerze mówiąc to nie bardzo wiem po co zaproponowałeś takie sposoby zabezpieczeń?
W podanym przez Ciebie podatnym przykładzie:
[...] header('Content-Length: ' . filesize('./podkatalog/kolejny/' . $_GET['file'])); [...] readfile('./podkatalog/kolejny/' . $_GET['file']); // ściągnięcie plikuWystarczyło by przed na początku zrobić:
$file = strreplace("..", "", $_GET['file']); if(!file_exists('./podkatalog/kolejny'. $file) { exit(); } // reszta koduPopraw mnie proszę, jeśli się mylę…
Ok, jest to jakieś zabezpieczenie @Grzegorz. Chroni jednak przed poruszaniem się po katalogach nadrzędnych w stosunku do wywoływanego skryptu. Co jednak, jeżeli plik znajdować się będzie w jakimś podkatalogu?
$file = strreplace("..", "", "/zaglebiony/plik.exe"); if(!file_exists('./podkatalog/kolejny'. $file) { exit(); } // reszta koduCo, jeżeli znajdować się w nim będą powiedzmy i pliki wykonywalne, i PHP? Warto ograniczyć wtedy możliwość pobierania wzorcem lub poprzez określenie dozwolonych rozszerzeń. Ostatecznie, aby pozbyć się zagrożenia w postaci przekazywania parametru w postaci encji lub postaci #%% warto filtrować wejściowe dane. Wszystko to jest opisane w artykule (część przykładów). Te dodatki mogą się wydać drobnostkami, ale lepiej dmuchać na zimne.
W sumie racja – nie pomyślałem o takim przypadku. Pewnie dlatego, że jeszcze nigdy nie spotkałem się z takim layoutem plików, że w katalogu ./A są pliki do pobrania, a w ./A/B konfiguracja połączenia z bazą.
Myślę, że takie zabezpieczenia należy robić „szyte na miarę” zabezpieczanego systemu. Ale fajnie, że podałeś sposób ogólny. Czekam na kolejne z serii zabezpieczania skryptów PHP :)
A czy nie wystarczy takie zabezpieczenia przed pobieraniem pliku z innego katalogu:
?
@Grzegorz:
Tak. Chociażby dla projektów o szerokim i zarobkowym przeznaczeniu powinno się indywidualnie wprowadzać system zabezpieczeń. To co podaje ja, to tak jak napisałeś, ogólne sposoby, przykłady. Zawsze powtarzam, że nie chodzi o to, aby je kopiować, tylko żeby dzięki nim zrozumieć istotę problemu i bawić się samemu. Co do artykułu…tworzy się :] Właśnie dziś. Niestety notoryczny brak czasu powoduje, że nie mogę zasiąść do tego raz, a porządnie (chociażby na kilka dni), a muszę robić „od czasu, do czasu”.
@Pawel:
Generalizując można by stwierdzić, że tak – wystarczy. W praktyce, może być jednak niedostateczną obroną. Dlaczego? W pewnych przypadkach funkcja ta zwraca niepoprawny wynik. Przykład:
Można więc wprowadzany parametr spreparować, o ile nie wykona się odpowiedniej filtracji, poprzez usunięcie ciągu po rozszerzeniu pliku.
Inną sprawą jest to, że nawet w najmniejszym stopniu nie kontrolujemy tego, jakie pliki można pobierać z danego katalogu. Dopóki ktoś, kto ma dostęp do niego (nieważne nawet, czy autoryzowany, czy nie) nie wrzuci do niego niepożądanych treści, powinno być ok. Co jednak w przeciwnym przypadku?
Witam
Na stronie http://www.rozenek.com/polski,198 opisalem podobny problem:
if(strpos($_REQUEST['img_name'], '/')===false && strpos($_REQUEST['article_id'], '/')===false) { header("Content-type: image/" . end($extensions)); readfile("/usr/home/sq8bgq/rozenek_img/" . $_REQUEST['article_id'] . "/" . $_REQUEST['img_name']); } else { echo "ty kradzieju"; }### if(strpos($_REQUEST['img_name'], ‘/’)===false && strpos($_REQUEST['article_id'], ‘/’)===false)
### chroni przed atakiem typu: http://www.rozenek.com/,../../../../../../etc/passwd
Gdyby ktoś miał kiedyś problem z nieprawidłową wielkością pobieranego pliku, chociażby w moich, wyżej wymienionych przykładach, to radzę spróbować dodać na początku skryptu ob_start(), a przed samym readfile() linię ob_end_clean().