H1CTF – zwycięski raport Jakuba Żoczka z Sekuraka!

02 lipca 2020, 21:20 | ctf, Teksty | komentarzy 6
: oglądaj sekurakowe live-streamy o bezpieczeństwie IT.

Wstęp:

Jedna z najpopularniejszych platform Bug Bounty, Hackerone – w ramach promocji swojego wirtualnego eventu H12006 – uruchomiła konkurs w formule Capture The Flag. Nagrodą było zaproszenie na wspomniany ekskluzywny event oraz zaproszenia do prywatnych programów bug bounty. Co ciekawe, główną nagrodę otrzymywało się nie za najszybsze rozwiązanie zadania, a za najlepsze jego udokumentowanie. Mój raport okazał się jednym z trzech wygrywających. 

Wokół zadania zbudowano historie – właściciel H1 zapomniał swojego hasła do konta, musimy je zdobyć i zatwierdzić wypłaty bounty dla Hakerów ;-). Autorom zależało na tym, żeby zadania wyglądały realistycznie i były oparte o prawdziwe znaleziska w ramach programów bug bounty. 

Sam event, który był nagrodą w konkursie właśnie się zakończył i myślę, że warto krótko wspomnieć jak wyglądał. Nie była to typowa konferencja bezpieczeństwa, a Live Hacking Event, gdzie głównym celem ataku był PayPal. Rozszerzony specjalnie na tę okazję scope programu, ciekawe bonusy i możliwość kolaboracji z innymi bug hunterami to niektóre z atrakcji, które czekały na zaproszonych gości. W ostatni dzień eventu hackerzy zaprezentowali najciekawsze znalezione podatności w formule lightnig talków. 

Hackerone zorganizował już kilka tego typu wydarzeń, ten natomiast z uwagi na sytuację epidemiologiczną na świecie po raz pierwszy odbył się w formie wirtualnej. W momencie pisania tego tekstu, w ramach tego niepowtarzalnego wydarzenia wypłacono ponad $422,000 na nagrody oraz bonusy.  

Zanim przejdziemy do szczegółowego opisu rozwiązania zadania – kilka statystyk: 

  • 4282 osób wzięło udział w CTF
  • 210 osób doszło do części mobilnej 
  • 175 dotarła do półmetka 
  • 127 dotarło do ostatniej części
  • 55 osób zdobyło flagę 

W tym artykule przedstawię krok po kroku jak poradziłem sobie z wszystkimi zadaniami.

Rozwiązanie CTF:

Rekonesans

Jako zakres do testów podano wszystkie subdomeny bountypay.h1ctf.com. Moim pierwszym krokiem było zebranie subdomen z wykorzystaniem narzędzia Certificate Search:

Losowo wybrałem serwer app.bountypay.h1ctf.com  jako pierwszy. Znajduje się na nim aplikacja webowa, posiadająca ekran logowania. Pora na aktywny rekonesans z wykorzystaniem aplikacji ffuf oraz jednej z wordlist repozytorium SecLists.

Wynik uruchomienia aplikacji ffuf:

Plik .git/HEAD  wygląda na interesujący i wskazuje na potencjalny wektor ataku.

Information Disclosure – dostęp do katalogu .git 

Znaleziony plik .git/HEAD sugeruje użycie repozytorium Git. Próba dostępu do pliku config file zdradził kilka interesujących informacji:

Publiczne repozytorium Github do którego link znajduje się powyżej zawiera jeden pliklogger.php .

Otrzymujemy informacje o kolejnym pliku dostępnym na serwerze – bp_web_trace.log :

Otrzymany ciąg znaków sugeruje endkodowanie Base64 – wskazuje na to użyty specyficzny zestaw znaków oraz == na końcu każdego z ciągów. Po zdekodowaniu otrzymujemy:

Wśród wpisów możemy znaleźć nazwę użytkownika oraz hasło. Po zalogowaniu do aplikacji ukazuje się prośba o podanie kodu (2FA).

2FA Bypass

Spójrzmy na kod HTML wyświetlonego formularza: 

Możemy zauważyć parametr challenge, który wygląda jak hash MD5. Zamieniając wartość wspomnianego parametru na sumę MD5 z ciągu bD83Jk27dQ (znalezionego wcześniej w pliku bp_web_trace.log ) – możemy ponownie użyć pary challenge/answer i skutecznie ominąć mechanizm 2FA.

Ostateczne zapytanie HTTP po wpisaniu wszystkich znalezionych danych:

Odpowiedź HTTP:

Możemy zauważyć poniższą stronę:

SSRF w api.bountypay.h1ctf.com

Po zalogowaniu do aplikacji widzimy dashboard, który nie zdradza zbyt wielu informacji. Zawiera jeden przycisk (Load Transactions), który nie zwraca żadnych rezultatów. Jednak analizując wysyłane zapytania, możemy zauważyć komunikacje z endpointem  /statements :

…z następującą odpowiedzią:

Kilka kwestii, na które warto zwrócić uwagę:

  • Informacja o adresie https://api.bountypay.h1ctf.com/api/accounts/F8gHiqSdpK/statements?month=01&year=2020  w odpowiedzi HTTP
  • Ciasteczko token  zawiera dane zakodowane w base64

Zobaczmy co można znaleźć pod adresem  api.bountypay.h1ctf.com:

Kliknięcie w hiperłącze REST API najpierw wysyła zapytanie do endpointu /redirect , który zwraca kod 302 przekierowując nas na adres podany w parametrze url

Wracając do ciasteczka token – po zdekodowaniu wartości, możemy zauważyć strukturę JSON zawierającą pole account_id (którego wartość jest taka sama, jak ta znaleziona w odpowiedzi endpointu /statements). 

Zmodyfikowanie wartości account_id na F8gHiqSdpK/../../../? oraz ponowne zakodowanie w base64 całego tokenu sprawia, że aplikacja wysyła wewnętrznie zapytanie prosto do adresu https://api.bountypay.h1ctf.com/ (dzięki podatności typu Path Traversal).

Dodatkowo – odpowiedź z endpointu /statements zdradza kod HTML odpowiedzi z api.bountypay.h1ctf.com page – mamy więc tutaj do czynienia z podatnością typu SSRF.

Wersja ze sformatowanym HTML:

Dalsze działania skupiłem na próbie wysyłania zapytań do https://api.bountypay.h1ctf.com/redirect   oraz dostania się do wewnętrznych serwerów lub cloudowych adresów meta-data. Jednak bazując na eksperymentach, endpoint /redirect  zawierał whitelistę adresów, na które można było dokonać przekierowania i była ona raczej ograniczona do serwerów w domenie bountypay.h1ctf.com . Jednym z dozwolonych adresów był znaleziony w fazie rekonesansu software.bountypay.h1ctf.com . Odwiedzając ten adres z przeglądarki widzimy następującą informację:

Jednak używając znalezionej podatności SSRF mamy możliwość podejrzenia zawartości serwera:

Wersja z sformatowanym HTML:

Directory Listing na software.bountypay.h1ctf.com

Z uwagi na fakt, że możemy wysyłać zapytania na serwer software.bountypay.h1ctf.com  z wewnętrznym adresem IP zautomatyzowałem poszukiwania katalogów na serwerze co pozwoliło na znalezienie ciekawie wyglądającego /uploads . Sformatowany wynik HTML:

Bezpośrednie zapytanie o plik BountyPay.apk zakończyło się sukcesem i udało pobrać wspomniany plik.

Aplikacja Android – analiza statyczna i dynamiczna

APK to skrót od Android Application Package, jest to tak naprawdę plik ZIP, który możemy bez problemu rozpakować standardowymi narzędziami. Pakiet zawiera takie pliki jak:

  • Pliki odpowiedzialne za wygląd aplikacji
  • Manifest androidowy
  • Plik classes.dex 

Ostatni plik zawiera bytecode aplikacji. Używając aplikacji dex2jar oraz  jd-gui możemy z łatwością uzyskać dostęp do zdekompilowanego kodu Java:

Do samego kodu wrócimy później, najpierw jednak zainstalujmy aplikacje BountyPay na telefonie z Androidem. Po jej uruchomieniu widzimy następujący ekran:

Podając dowolne dane i przechodząc dalej – dostajemy się do Activity nazwanej PartOneActivity:

W dalszej części użyłem narzędzia Drozer pomagającej w dynamicznej analizie aplikacji, której wynik znajduje się poniżej. 

Bazując na powyższym możemy znaleźć 5 wyeksportowanych aktywności. Rzućmy na nie okiem:

Jest to moment, w którym warto spojrzeć do kodu. W pliku  PartOneActivity.java możemy znaleźć następujący fragment:

Z pomocą aplikacji Drozer (lub bezpośrednio z shell’a) możemy spróbować uruchomić activity z następującymi parametrami, co skutkuje przejściem do kolejnego activity:

Krok pierwszy za nami, spójrzmy na kod w pliku PartTwoActivity.java :

Po szybkiej analizie po raz kolejny używamy Drozera, analogicznie jak w poprzednim przypadku:

W aplikacji pojawia się ukryty wcześniej formularz:

Kod odpowiedzialny za walidację formularza:

Aplikacja pobiera dane z formularza i porównuje je z wartością zapisaną w bazie Firebase. W plikach z preferencjami możemy znaleźć access_token do wspomnianej bazy i próbować połączyć się z nią bezpośrednio. Moglibyśmy też próbować ominąć mechanizm  SSL Pinning i spróbować podejrzeć dane w komunikacji sieciowej. Innym sposobem byłoby użycie narzędzi typu Frida czy Objection żeby podejrzeć wartość, która jest porównywana w formularzu. Ja zamiast tego użyłem szybszej metody i spróbowałem tam wartość X-Token znalezioną w pliku PartThreeActivity.java  co zadziałało:

Po poprawnym wypełnieniu formularza przechodzimy do ostatniej części – PartThreeActivity. Jest to stanowczo najcięższa część z całej części Androidowej z uwagi na zobfuskowany kod:

Na początek zająłem się inicjacją zmiennych:

Następnym krokiem było porównywanie wartości:

ozpisując wartości zmiennych szczegółowo:

  • localObject4  powinna mieć wartość PartThreeActivity
  • localObject4  jest pobierana ze zmiennej localObject2 (jako base64)
  • localObject2  jest wartością parametru three
  • localObject5  powinna mieć wartość on
  • localObject5  jest pobierana ze zmiennej localObject1 (jako base64)
  • localObject1  jest pobierana z parametru switch
  • localStringBuilder  powinna mieć taką samą wartość jak zmienna  paramAnonymousDataSnapshot
  • paramAnonymousDataSnapshot  powinna mieć taką samą wartość jak localObject3 
  • localStringBuilder  powinna mieć wartość złożoną ze znaku X- oraz str – zakładam, że to po prostu X-Token
  • localObject3  jest wartością parametru header

Biorąc pod uwagę powyższe kończymy ten etap realizując zapytanie pod następujący adres URL:

Aplikacja ukazuje nam kolejny formularz, w którym powinniśmy podać „leaked hash”:

Przeglądając pliki utworzone w systemie Android możemy odnaleźć plik /data/data/bounty.pay/shared_prefs/user_created.xml z następującą zawartością:

Uzupełniając wspomniany formularz o wartość TOKEN otrzymujemy komunikat, że rozwiązaliśmy zadanie Androidowe oraz, że znalezione informacje przydadzą się w kolejnych zadaniach. 

Informacja o rozwiązaniu zadania:

OSINT

W pewnym momencie trwania CTF na Twitterowym profilu Hackerone pojawił się retweet wskazujący na profil BountyPay.

Jeden z tweetów mówi o nowym pracowniku – Sandrze! Jej profil możemy znaleźć patrząc na osoby obserwujące BountyPay HQ:

W jedynym swoim wpisie na Twitterze Sandra informuje, że to jej pierwszy dzień w nowej pracy, jest bardzo podekscytowana na dowód czego wrzuca zdjęcie swojej firmowej plakietki wraz z identyfikatorem pracownika – STF:8FJ3KFISL3

Ta informacja jest bardzo cenna i przyda się później. :)

Information Disclosure – API

Wracamy z naszymi poszukiwaniami do API. Ponownie enumerując katalogi z użyciem narzędzia ffuf możemy znaleźć następujący endpoint:

Bezpośrednie zapytanie do endpointu /api/staff  zwraca informacje o brakującym tokenie:

Używając wartości Token pozyskanej w zadaniu Androidowym i nagłówka X-Token uzyskujemy kolejne dane:

Zmiana metody HTTP na POST informuje o brakującym parametrze:

Używając poprzednich parametrów z odpowiedzi JSON i podmieniając staff_id na identyfikator Sandry uzyskujemy jej login i hasło:

Privilege Escalation w aplikacji Staff

Mając login i hasło Sandry, przechodzimy do kolejnej aplikacji – Staff mieszczącej się pod adresem  staff.bountypay.h1ctf.com

Aby dobrze zrozumieć podatność, która się tu znajduje – zacznijmy od przyjrzenia się plikowi /js/website.js :

Najważniejsze elementy, które możemy zauważyć:

  • Jedna z funkcji wysyła zapytanie do /admin/upgrade?username=....  dając użytkownikowi prawa administratora.
  • Istnieje endpoint /admin/report?url=... służący do powiadamiania administratorów, że na podanej stronie “coś jest nie tak”.
  • Na samym końcu – sprawdzana jest wartość location.hash oraz wyzwalane są eventy click  bazujące na selektorach CSS (tab1/tab2/tab3/tab4).

Wszystkie powyższe będą potrzebne, aby podnieść swoje uprawnienia. Zacznijmy od prostego odwołania się do endpointu /admin/upgrade?username=sandra.allison :

Niestety nie przyniosło to oczekiwanego rezultatu.

Na dole strony znajdziemy hiperłącze nazwane „Report This Page”, które wywołuje  poniższy komunikat:

Zatwierdzenie formularza powoduje wysłanie następującego zapytania HTTP:

Parametr url zawiera adres odwiedzanej strony zakodowany w base64. Bazując na wcześniejszym komunikacje, odwoływanie się do endpointów zaczynających się od /admin będzie ignorowane (co udało się potwierdzić różnymi testami). Nadal jednak pamiętajmy o tym mechanizmie, ponieważ będzie on jednym z elementów potrzebnym, aby uzyskać wyższe uprawnienia.

Rozglądając się po aplikacji możemy też zauważyć, że aplikacja używa parametru template do wyświetlania kolejnych stron. W wyniku eksperymentów zauważyłem, że użycie kilku parametrów template wraz z nawiasami kwadratowymi powoduje wyświetlenie kilku widoków na jednej stronie.  Przykładowo – dla /?template[]=login&template[]=home  pokaże jednocześnie formularz logowania oraz stronę główną: 

Dodatkowo gdy na stronie logowania podamy parametr username  – nazwa użytkownika w formularzu zostanie wypełniona. Przykładowo dla  /?template=login&username=test.test :

Ostatnim elementem jest sekcja Profile gdzie użytkownik może zmienić wyświetlaną nazwę oraz avatar.

Przykładowe zapytanie HTTP z wprowadzonymi zmianami:

Wartości profile_name  oraz profile_avatar  mogą mieć dowolne wartości – zostaną one zapisane w profilu użytkownika, a następnie wyrenderowane – na przykład w widoku Tickets . Dla przykładu podanie hackerone jako wartość parametru profile_avatar, a następnie przejście do sekcji Tickets  wyrenderuje taki kod:

Jak widzimy profile_avatar wyświetla się jako część atrybutu  class. Wróćmy do fragmentu w pliku website.js :

Bazując na powyższym:

  • Funkcja zostanie uruchomiona w momencie kliknięcia na element z selektorem  upgradeToAdmin .
  • Nazwa użytkownika dostarczona jako parametr do /admin/upgrade  pobierana jest z wartości elementu <input name="username"> .
  • Całość musi zostać uruchomiona przez administratora.

Finalnie:

  • Mamy kontrole nad selektorami CSS przez modyfikację parametru profile_avatar – na wartości upgradeToAdmin oraz tab2.
  • Musimy wyświetlić stronę zawierającą zarówno widok Ticket oraz stronę logowania – po to, żebyśmy mieli jednocześnie element <input name="username"> oraz elementy z naszymi własnymi selektorami CSS – na jednej stronie.
  • Musimy ustawić wartość parametru username na sandra.allison.
  • Musimy wyzwolić event click – co osiągniemy przez dodanie #tab2 (location.hash) w adresie URL .
  • Na samym końcu – musimy zaraportować utworzony URL administratorowi tak, żeby w niego kliknął i zmienił uprawnienia Sandry na administratora. 

Finalny URL:

Finalny URL do raportu:

Wysłanie powyższego daje nam uprawnienia Administratora:

Mamy teraz login i hasło konta Marten’a! 

2FA Bypass – część druga

Używając znalezionych danych do logowania w pierwszej aplikacji (App Portal) oraz po ponownym pominięciu mechanizmu 2FA możemy zauważyć informacje o płatnościach:

Klikając w przycisk Pay  – pojawia się kolejny system 2FA, który musimy pominąć:

Przycisk Send Challenge wyzwala następujące zapytanie HTTP:

Parametr app_style zawiera adres URL do stylu CSS. Gdy zmodyfikujemy tę wartość na adres własnego serwera, w logach możemy zauważyć, że zapytanie wykonane jest przez Headless Chrome:

Po kilku testach okazuje się, że Headless Chrome odwiedza stronę, generującą kod 2FA, na której dołączony jest styl CSS, który możemy kontrolować (podawany jako część parametru app_style). Możemy zatem użyć techniki eksfiltracji danych przez CSS aby otrzymać kod 2FA.

Po pierwsze musimy odkryć, jak nazywa się atrybut w kodzie HTML, do którego chcemy uzyskać dostęp. W tym celu wygenerowałem następujący plik CSS:

W logach mojego serwera pojawił się wpis wskazujący, że pierwsza litera atrybutu to „c”:

Korzystając z powyższej techniki ustaliłem, że nazwa atrybutu to code_X – gdzie X to numer od 1 do 7, wskazujący na pozycję znaku w kodzie 2FA. Dla utrudnienia, kolejność elementów z poszczególnymi znakami wyświetlana była losowo. Aby uzyskać finalny kod 2FA musimy znać nie tylko wartość, ale też pozycje. Finalny kod do generowania pliku CSS wygląda następująco:

Przechodząc dalej otrzymujemy poszczególne litery kodu oraz ich dokładną kolejność, co możemy wykorzystać do ominięcia mechanizmu 2FA:

To doprowadza nas do najbardziej wartościowej części zadania flagi:

Podsumowanie:

Przygotowana zadania bardzo ciekawe, a samo rozwiązywanie CTF traktuję jako bardzo dobrą zabawę pod szyldem cyberbezpieczeństwa. Największe wyzwanie jednak sprawiło mi przygotowanie powyższego raportu (oryginalna wersja w języku angielskim dostępna jest na stronie Hackerone https://hackerone.com/reports/895202).

 

Dla zainteresowanych na Youtube dostępna jest rozmowa ze zwycięzcami, na temat zadań, rozwiązań oraz ogólnych odczuć dotyczących opisywanego CTFa: https://www.youtube.com/watch?v=AAQHEl3b05M

 

Przy okazji – zachęcamy do zerknięcia na ofertę pentestową Securitum – Jakub jest jednym z naszych pentesterów :-)

 

— Jakub Żoczek (@zoczus), hakuje w Securitum

Spodobał Ci się wpis? Podziel się nim ze znajomymi:



Komentarze

  1. Marek

    Gratulacje!! Świetny raport, pokazujący nie tylko techniki i narzędzia, ale przede wszystkim tok myślowy. Mało jest takich publikacji w polskim internecie, dzięki za podzielenie się!

    Odpowiedz
  2. Andrew

    Gratulacje! Świetna robota.

    Odpowiedz
  3. nn

    WOW! Gratulacje!

    Odpowiedz
  4. Nism0

    Zadanie bardzo podobne do maszyny „Player2” z HTB (poziom Insane).
    Gratulacje!

    Odpowiedz
  5. Kot Rademenes

    Świetnie opisane, więcej takich publikacji na Sekuraku!

    Odpowiedz
  6. w4cky

    Swietna robota, pozdro zoczus :)

    Odpowiedz

Odpowiedz