18-10-2018 / Od 0 do pentestera

Assert w PHP

Dlaczego przekazywanie zmiennej do funkcji Assert w PHP nie jest dobrym pomysłem?

Dzisiaj przyjrzymy się jednemu z zadań z CTFa Hack.lu CTF 2018.

Dostajemy kod źródłowy aplikacji.

Naszym celem jest przekazanie takich parametrów, aby wyświetlona została ostania linijka - tak zwana flaga.

Kod zadania

Spróbujmy zatem od samego początku.

Skopiujemy plik na nasz lokalny serwer.

Włączymy również obsługę błędów - komentując linijkę z error reporting.

Dzięki temu, jeżeli skrypt zwróci jakiś błąd - będziemy o nim wiedzieć.

Najpierw musimy obejść funkcję file_get_contents.

Jest ona odpowiedzialna za pobieranie treści plików i zwracanie ich jako string.

Wynik działania tej funkcji jest porównywany z ciągiem Hello challenge.

Aby więc przejść dalej musielibyśmy znać nazwę pliku, który ma właśnie taką treść.

Jednak prawdopodobieństwo, że taki plik istnieje na atakowanym serwerze jest zerowe.

Musi zatem istnieć jakiś inny sposób ataku.

W PHP mamy tak zwane filtry1, które przekazane jako parametr do funkcji, sprawiają, że zachowuje się ona nieco inaczej.

Filtry

Jednym z takich filtrów jest data://text/plain, który przekazany jako parametr zamiast otwierania pliku o podanej nazwie, wyświetla treść zakodowaną przy pomocy base64.

Musimy zatem zakodować ciąg Hello challenge do base64, a następnie połączyć go z filtrem.

data://text/plain;base64,SGVsbG8gQ2hhbGxlbmdlIQ==

Teraz kodujemy to wszystko do kodowania rozpoznawanego przez przeglądarkę i przekazujemy jako parametr msg.

Jesteśmy o jeden krok do przodu.

Kolejnym zadaniem jest ominięcie funkcji intval, która zwraca nam int z podanego ciągu znaków.

Mamy tutaj do czynienia z konstrukcją logiczną or.

Żeby więc przejść dalej wynik funkcji intval musi być równy zmiennej $cc - czyli 1337 a dodatkowo, parametr przekazywany do funkcji intval musi być różny od 1337.

Rozwiązanie jest banalnie proste.

intval działa w taki sposób, że odczytuje ciąg znaków zaczynając od samego początku, aż natrafi na pierwszy znak niebędący cyfrą.

Zatem jeżeli przekażemy 1a - zwróci 1.

Jeżeli przekażemy 23b - zwróci 23.

Funkcja intval

W naszym więc wypadku musimy przekazać 1337, co spełni pierwszy warunek.

Drugi również będzie spełniony, ponieważ zmienna $cc jest liczbą, a parametr $k1 stringiem.

Do porównania użyto trzech znaków równości - czyli oprócz wartości sprawdzany jest również typ zmiennej.

W naszym wypadku string i int to dwa różne typy.

Trzeci krok to wyrażenia regularne.

Wiemy, że parametr key2 musi mieć dokładnie 42 znaki - a to za sprawą funkcji strlen.

Następnie przy pomocy wyrażenia regularnego sprawdzamy, czy w podanym ciągu znajdują się tylko cyfry.

Daszek oznacza że sprawdzamy ciąg od jego pierwszego znaku.

Znak dolara natomiast, że sprawdzamy cały ciąg, do jego ostatniego znaku.

Dodatkowym warunkiem jaki musimy spełnić jest to, że podany parametr nie może być liczbą.

Jak zatem sprawić, aby ciąg zawierał tylko cyfry, ale nie był liczbą?

Tutaj autorzy zadania wykorzystali bardzo sprytne rozwiązanie.

Znak dolara przez nich użyty nie jest bowiem prawdziwym znakiem dolara.

Aby pokazać tą różnice przedstawię prawdziwy znak dolara obok tego użytego przez twórców.

Fałszywy znak dolara

Znaki są podobne, ale nie takie same.

Zatem jeżeli nie użyto znaku dolara, oznacza to że ciąg musi zaczynać się od co najmniej jednej cyfry, a następnie musi wystąpić fałszywy znak dolara.

Dalsza część ciągu nie jest sprawdzana.

Tak się składa, że ciąg z fałszywym znakiem dolara nie jest traktowany jako liczba przez funkcję is_numeric.

Tworzymy więc dowolną liczbę, a następnie podajemy do niej znak dolara i wypełniamy dodatkowymi liczbami - tak aby cały ciąg był równy 42 znakom.

Pamiętajmy jednak że dolar jest zapisany w innym kodowaniu i PHP liczy go jako 3 znaki.

Pozostał jeszcze if, w którym porównujemy podaną przez nas wartość z liczbą 1337.

Używany jest tutaj podwójny znak równości, który zachowuje się nieco podobnie do funkcji intval.

Modyfikujemy więc naszą treść tak, aby zaczynała się od wartości 1337, następnie fałszywy dolar i reszta znaków.

Teraz możemy już ustawiać parametr $cc.

Dalej skrypt przy pomocy funkcji substr pobiera wszystkie znaki z parametru $cc, począwszy od litery 42.

A następnie sprawdza czy ten string jest równy hashowi sha1 z zawartości parametru $cc.

Na pierwszy rzut oka wygląda to zatem na znalezienie kolizji w funkcji $sha1.

Owszem, zapewne jest to możliwe, ale ludziom z Google2, którzy posiadali o wiele więcej zasobów zajęło to ponad rok.

Musimy zatem poszukać alternatywnego rozwiązania.

Jeżeli przyjrzymy się manualowi PHP zobaczymy, że funkcja sha1 oczekuje jako parametru $cc ciągu znaków.

Funkcja SHA1

Co się zatem stanie jeżeli przekażemy tam tablicę?

W PHP jeżeli nazwa argumentu kończy się nawiasami kwadratowymi - jest ona traktowana jako tablica.

Jeżeli przekazujemy do funkcji sha1 tablicę - zwraca ona wartość null.

substr zwraca false jeżeli ciąg jest za krótki - tak jak w tym przypadku.

Jeżeli popatrzymy sobie na tabelkę3, widzimy w niej że null z fałszem daje prawdę.

Jak działa porównywanie ciągów w PHP

Ot, cała magia PHP.

Następna linijka jest bardzo zwodnicza. Inaczej bowiem wygląda w przeglądarce, a inaczej w naszym edytorze tekstu.

Czy widzicie różnicę? Jak to możliwe?

Do sprawdzenia kodu użyjemy narzędzi deweloperskich Google Chrome.

Widzimy tutaj linijkę z ampersandem, hashem i wartością 8238.

Szybkie wyszukiwanie w Internecie pokazuje nam, że jest to tak zwany znak "Right-To-Left"4, używany głównie w językach arabskich.

Right-To-Left

Po jego użyciu, kolejne znaki są wyświetlane w odwróconej kolejności.

Widać to po najechaniu na nie w konsoli.

Tutaj są one wyświetlane we właściwej kolejności, przeglądarka jednak odwraca je.

Odwrócone znaki w praktyce

Nasz edytor tekstu wyświetla je prawidłowo.

Użyta jest tutaj jednak dziwna składnia. Dwa znaki dolara przed wartością a?

W PHP to tak zwane "Variable variables".

Najprościej zademonstrować to na przykładzie, popatrzmy na Stackoverflow5.

Variable variables

Wyświetlamy tutaj zawartość zmiennej $name.

Najpierw więc bierzemy jej wartość - czyli real_variable, a następnie poszukujemy zmiennej o dokładnie tej nazwie i dopiero ją wyświetlamy.

W tym wypadku real_variable ma wartość test i dokładnie taka wartość pojawi się na naszym ekranie.

Wróćmy do kodu. Dwa znaki dolara użyto tutaj ze zmienną a.

Zmienna a linijkę wyżej ma przypisaną wartość b.

Popatrzmy więc na wartość zmiennej b, czyli 2.

Czyli aby przejść dalej wartość k1 musi być równa 2.

Niestety już wcześniej ustawiliśmy wartość key1 na 1337 - aby spełnić poprzednie warunki.

Nie możemy zatem sprawić aby zmienna raz była równa 1337 a drugi raz 2.

I tutaj na ratunek przychodzi nam linijka z foreach.

Przy jej pomocy możemy bowiem nadpisać wartość dowolnej zmiennej, ponieważ jest tam używana konstrukcja variable variables.

Tym razem wystarczy w parametrze nazwanym k1 przekazać wartość 2.

W ten oto sposób w linijce 40 wartość k1 zostanie ustawiona na 2.

Pozostał nam ostatni warunek do spełnienia.

Funkcja assert sprawdza czy podany warunek jest spełniony.

Jeżeli nie - kończy wykonanie programu.

Widoczne jest to w ostatniej linijce z błędem, gdzie widzimy że 42 nie jest równe Array - czyli tablicy.

42 to wartość zmiennej $bb a array to wartość zmiennej $cc.

Nie możemy tutaj zmodyfikować wartości $cc - ponieważ jest ona potrzebna wcześniej podczas omijania funkcji sha1.

To może dać nam do myślenia, że należy zmienić wartość zmiennej $bb.

Możemy ją bowiem zmienić używając konstrukcji variable variables.

Spróbujmy zatem zmienić jej wartość na array.

Tym razem otrzymaliśmy komunikat, że nie można ze sobą porównać Array z Array.

Dziwne prawda?

Zmienił się też komunikat.

Linijkę wyżej - syntax error.

Syntax error

Pojawia się wtedy, kiedy przekazany do PHP kod jest nieprawidłowy.

Ale przecież jeszcze chwilę temu tej linijki tutaj nie było?

Skąd się ona wzięła?

Jeżeli przyjrzymy się bliżej funkcji assert6, wyczytamy tam że ciąg znaków przekazany do niej jest ewaluowany jako kod PHP.

Assert

Czyli assert można porównać do funkcji eval.

Konstrukcja array === array jest nieprawidłowa. Spróbujmy zatem wrzucić tam jakiś prawidłowy kod.

Na przykład z użyciem funkcji die(), która to wyświetla przekazany do niej parametr i kończy wykonanie skryptu.

Dzięki temu nawet jeżeli dalsza część będzie nieprawidłowa - funkcja powinna zadziałać.

Spróbujmy zatem wyświetlić zawartość flagi - dorzucając do ciągu podwójny slash, oznaczający że reszta danych jest komentarzem.

Brawo. Właśnie wyświetliliśmy zawartość flagi.

Bardzo ciekawe i nietrywialne zadanie, pokazujące niebezpieczeństwo używania konstrukcji assert.

=creation