Wpis z mikrobloga

#programowanie #zagadkihakerskie

Wczoraj wrzuciłem zagadkę hakerską, w której należało wejść na stronę podając odpowiednie hasło, mając nawet dostęp do źródła tej strony. W tym wpisie wyjaśnię dokładnie jak należało przeprowadzić "atak" i z czego to wynika.

Zanim przejdę konkretnie do treści zagadki, krótkie wprowadzenie do zachowania PHP w przypadku użycia operatorów

==
lub

!=
do porównywania. Jeżeli z użyciem tych operatorów są porównywane wartości typu

string
, które jednak wyglądają jak liczby to PHP wcześniej dokonuje zamiany tych wartości na liczb, a dopiero potem porównuje. Tak więc porównanie w postaci

('255' == '255')
powoduje, że oba stringi

'255'
są wpierw zamieniane na liczby, dopiero potem porównywane. Wydaje się to niegroźna cecha, ale zaczyna ona być problematyczna, gdy dokładnie przeanalizujemy co to znaczy, że ciągi znaków "wyglądają jak liczby". Otóż znaczy to, że także porównanie postaci

('255' == '0xff')
także zwraca wartość

TRUE

Jeszcze innym sposobem zapisu liczb jest notacja wykładnicza, np. zapis

4e35
jest jednoznaczny z

4*10^35
. Zauważamy jednak, że jeżeli cechą (czyli tą częścią zapisu przed "e") jest

0
, to niezależnie od tego co jest dalej, cała liczba też jest równa 0. A więc

0e345
to jest 0, podobnie jak np.

0e457
też jest równe 0. To powoduje, że następujące porównanie stringów w PHP też zwraca

true
:

('0e456' == '0e123')
.

Przejdźmy już teraz do samej zagadki. W kodzie źródłowym została zdefiniowana stała

SECRET_PASSPHRASE
o wartości

"Teg0!HasL4.Ni3_D4!Si3##ZgadnAc!!39720403"
. Dalej w kodzie brany jest pod uwagę hash md5 tej wartości, a konkretniej jego pierwszych 16 znaków. Ten hash to dokładnie

0000e468235440361a9ab73ca7c60018
, zaś biorąc jego pierwszą połowę uzyskujemy

0000e46823544036
. Czy coś wam to przypomina? No właśnie! Na początku mamy

0
, później

e
, a później wartość liczbową. A więc jeżeli znajdziemy inny string postaci (jako regex):

0+e[0-9]+
(co najmniej jedno "0", "e", co najmniej jedna cyfra) to (co wynika z tego o czym pisałem akapit wcześniej) dla PHP te stringi będą równe.

Znalezienie wartości, której hash będzie spełniał ten warunek nie jest zbyt skomplikowane. W zasadzie wystarczy brać po prostu kolejne wartości liczbowe i sprawdzać ich hasze. Napisałem prosty program w Pythonie, który błyskawicznie wyszukuje trzy wartości, których pierwsze 16 znaków hasza md5 jest w postaci

0+e[0-9]+
. Pierwszą z nich jest

"7554"
, której hash md5 wynosi

"0e16366727185813f59d4a9467878901"
, a pierwsza połowa tego hasha to

"0e16366727185813"
. Upewnijmy się jeszcze, że PHP faktycznie będzie myślał, że ta wartość jest równa pierwszej połowie hasza z

SECRET_PASSPHRASE
: (poniższe polecenie można po prostu wpisać w konsoli):

$ php -r 'var_dump("0e16366727185813" == "0000e46823544036");'

bool(true)

Może dla nas te stringi nie są takie same, ale dla PHP już tak :) Pozostaje więc tylko przejść na stronę Hakerium, wpisać w pole "7554" i cieszyć się z ukończenia zagadki.

Jak się bronić przed podobnym ryzykiem we własnych aplikacjach? Nie stosować operatorów

==/!=
tylko

===/!==
. Ta pierwsza para prawie nigdy nie jest dobrym rozwiązaniem, bo praktycznie zawsze chcemy porównywać wartości tego samego typu, a ta druga para nam to zapewnia. Sprawdźmy:

$ php -r 'var_dump("0e16366727185813" === "0000e46823544036");'

bool(false)

A więc tak jak powinno być :)

Jedyną osobą, której udało się zalogować był @koziolek i dla niego gratulacje :). Dla reszty, mam nadzieję, będzie to pouczająca lekcja i zaraz sprawdźcie czy wasz kod w PHP nie jest na taką przypadłość podatny ;-)

Jeśli macie jakieś pytania, śmiało pytajcie.
  • 3
Jako dodatkową ciekawostkę przeciwko operatorowi

==
podam jego nieprzechodniość. Z matematyki wiemy, że jeśli spełniona jest równość a=b i a=c to z tego wynika, że b=c. Czy tak jest zawsze w PHP?

php > var_dump("wykop" == 0);

bool(true)

php > var_dump("wykop" == TRUE);

bool(true)

php > var_dump(0 == TRUE);

bool(false)