Elementrica
03Realizacje04Referencje05Firma06Aktualności07Kontakt
PLENDE

Przemyt żądań na serwerze, którego nie miało być w sieci

Testy penetracyjne
Przemyt żądań na serwerze, którego nie miało być w sieci

Klient dał nam dwie aplikacje mobilne i jedno zdanie zakresu: sprawdźcie, czy da się je złamać. Aplikacje okazały się dobrze zrobione, więc weszliśmy inną drogą.

Nie przez telefon, tylko przez serwer deweloperski, który stał w publicznym internecie z włączonym trybem debug i podatnością na desynchronizację proxy. W środku znaleźliśmy mechanizm, który pozwala przechwycić żądanie innego użytkownika.

Poniżej cała ścieżka, z mapowaniem na MITRE ATT&CK i fragmentami do odhaczenia u siebie.

Kontekst

Aplikacja mobilna była najbezpieczniejszym elementem całej układanki. To rzadki widok. Zwykle to właśnie na telefonie znajdujemy zostawione tokeny, klucze w plikach, dane w otwartym schowku. Tu nie. Dane szyfrowane lokalnie, cała komunikacja po HTTPS, binarki podpisane, tryb debug w wersji produkcyjnej wyłączony. Ktoś, kto budował te aplikacje, odrobił lekcje z bezpieczeństwa mobilnego.

Klient poprosił nas o zewnętrzny test dwóch natywnych aplikacji, na iOS i Android, oraz powiązanych z nimi domen. Pracowaliśmy w trybie grey-box: dostaliśmy schemat API i konta testowe, bez kodu źródłowego i bez kont z podwyższonymi uprawnieniami. Backend to REST API, ta sama warstwa obsługuje obie platformy, więc po stronie logiki iOS i Android nie różniły się niczym.

Wyzwanie

Skoro sama aplikacja była zrobiona porządnie, ciekawe rzeczy musiały siedzieć gdzieś obok. Mobilny pentest, w którym binarka i lokalny magazyn danych są czyste, przesuwa ciężar na to, z czym aplikacja rozmawia: API, proxy, konfigurację serwera.

Warstwa mobilna trzymała się dobrych praktyk. Haczyk był w tym, że obok wersji produkcyjnej w internecie stała wersja deweloperska tej samej aplikacji. Z konfiguracją, której nikt nie wyłącza, bo środowisko nie jest produkcyjne. Włączony tryb debug frameworka Django, a piętro niżej, w warstwie proxy, podatność na przemyt żądań HTTP. Obrona nie była głupia. Po prostu pilnowała frontu, a wejście było bocznymi drzwiami, o których łatwo zapomnieć.

Co zrobiliśmy

Zaczęliśmy od rekonesansu domen powiązanych z aplikacją i szybko trafiliśmy na serwer deweloperski. Przy każdym nieobsłużonym błędzie wyrzucał pełną stronę debug Django. Taka strona to spowiedź serwera: wypluwa wersje frameworka i bibliotek, listę zainstalowanych komponentów i middleware, ścieżki na dysku, wewnętrzne porty i usługi, a w jednym miejscu adres i login do usługi wewnętrznej, której z internetu widać nie powinno. Dla obrońcy to lista rzeczy do naprawienia. Dla atakującego gotowa mapa zaplecza.

Prawdziwe wejście było jednak w warstwie proxy. Na endpoincie logowania do panelu administracyjnego serwer był podatny na desynchronizację żądań w wariancie CL.TE. Reverse proxy czyta długość żądania z nagłówka Content-Length, backend z Transfer-Encoding: chunked. Kiedy oba nagłówki mówią co innego, jedno żądanie po drodze zamienia się w dwa, a to drugie backend obsługuje tak, jakby przyszło prosto od proxy, bez ponownej weryfikacji.

POST /admin/login/?next=/admin/ HTTP/1.1
Host: dev.klient.example
Content-Type: application/x-www-form-urlencoded
Content-Length: 6
Transfer-Encoding: chunked

0

X

Proxy widzi sześć bajtów treści i przepuszcza całość. Backend kończy żądanie na „0” i zostawia „X” jako początek kolejnego żądania w kolejce. To „X” doklei się z przodu do żądania następnej osoby, która trafi na ten serwer.

Najpierw potwierdziliśmy, że da się zmusić serwer, żeby odesłał nasze żądanie we własnej odpowiedzi. Na logowaniu do panelu wartość pola z adresem e-mail i parametr przekierowania wracały w treści odpowiedzi. To znaczyło, że przemycony fragment można później odczytać. Potem sięgnęliśmy po endpoint zapisujący dane profilu użytkownika, przechwyciliśmy żądanie innego konta testowego i zapisaliśmy jego pierwsze 150 bajtów do jednego z pól. Mówiąc wprost: cudze żądanie wpadło nam do pola, które sami mogliśmy potem odczytać.

Konta nie przejęliśmy. Zakres kończył się dokładnie tam, gdzie robi się najciekawiej, a 150 bajtów cudzego żądania to jeszcze nie cudze konto. Ale resztę drogi opisaliśmy krok po kroku. W API był endpoint przyjmujący pole tekstowe z określoną dolną granicą długości i bez górnej. Taki brak limitu zamienia pole w bufor: można w nim zaparkować znacznie dłuższy przemycony fragment, a potem go odczytać, razem z tokenem autoryzacji należącym do innego użytkownika. Stąd do przejęcia konta jest już blisko.

Cały łańcuch w języku MITRE ATT&CK:

Technika (MITRE ATT&CK)Zastosowanie w teście
T1592.002 Gather Victim Host Information: SoftwareOdczyt wersji frameworka, bibliotek i integracji ze strony debug
T1595.002 Active Scanning: Vulnerability ScanningWykrycie desynchronizacji CL.TE i błędnych konfiguracji na hoście dev
T1190 Exploit Public-Facing ApplicationWykorzystanie desynchronizacji na publicznie wystawionym serwerze deweloperskim
T1557 Adversary-in-the-MiddlePrzechwycenie żądania innego użytkownika przez zakolejkowanie przemyconego żądania
T1589.002 Gather Victim Identity Information: Email AddressesEnumeracja istniejących adresów e-mail przez różne odpowiedzi rejestracji
T1539 Steal Web Session CookieŚcieżka do przejęcia tokenu innego użytkownika, wydłużona brakiem limitu czasu sesji

Przy okazji rejestracja zdradzała, które adresy e-mail już są w systemie. Nowy adres zwracał 201 i zakładał konto, adres istniejący 400 z informacją, że taki użytkownik już istnieje. Dwie różne odpowiedzi pozwalają wprost enumerować konta: można spokojnie zbudować listę prawdziwych kont i dopiero na nią kierować dalsze ataki, choćby password spraying.

POST /rejestracja HTTP/2
Host: dev.klient.example
Content-Type: application/json

{"email":"nieznany@klient.example"}   ->  201 Created      (konto założone)
{"email":"istnieje@klient.example"}   ->  400 Bad Request  (adres już w systemie)

Do tego doszła jeszcze jedna rzecz z warstwy sesji: token uwierzytelniający dawał się użyć ponownie po długim okresie bezczynności. Sam w sobie drobiazg, ale w parze z przechwytywaniem cudzych żądań wydłuża okno, w którym przejęty token wciąż działa.

Rezultat

Z całego testu wyszło jedno ryzyko wysokie i sześć średnich, plus dłuższy ogon drobiazgów. Ryzyko wysokie to opisany łańcuch: publiczny serwer deweloperski, tryb debug i desynchronizacja proxy, które razem dają komuś z zewnątrz dostęp do cudzych żądań. Warstwa mobilna, ta, którą klient przejmował się najbardziej, przeszła test czysto.

Rekomendacje zostawiliśmy jako listę czynności do odhaczenia, z konkretnym działaniem przy każdej pozycji:

  • Wymusić HTTP/2 na całej ścieżce komunikacji, a tam gdzie trzeba zejść do HTTP/1.1, walidować przepisane żądanie względem specyfikacji i zamykać połączenie TCP przy każdej niejednoznaczności.
  • Zdjąć serwer deweloperski z publicznego internetu albo schować go za białą listą adresów IP lub za VPN.
  • Wyłączyć tryb debug na wszystkim, co osiągalne spoza sieci wewnętrznej.
  • Ustawić serwerowe unieważnianie sesji po czasie bezczynności i po czasie bezwzględnym, tak żeby stary token po prostu przestawał działać.
  • Ujednolicić komunikaty rejestracji i logowania, żeby odpowiedź dla adresu istniejącego i nieistniejącego wyglądała identycznie.
  • Nałożyć górny limit długości na pola tekstowe przyjmowane przez API, żeby żadne z nich nie mogło posłużyć za bufor.

Ten test dobrze pokazuje pewną pułapkę. Zespół włożył sporo pracy w bezpieczeństwo aplikacji mobilnej i miał rację, robiąc to. Rzecz w tym, że atakujący nie wchodzi tam, gdzie włożyłeś najwięcej pracy. Wchodzi tam, gdzie akurat stoi zapomniany serwer z konfiguracją z zeszłego kwartału. Ile Twoich środowisk deweloperskich jest w tej chwili osiągalnych z publicznego internetu? Jeśli nie znasz tej liczby na pamięć, ustalmy ją razem.

Aplikacja dopięta na ostatni guzik, a wejście i tak było z boku. Znasz wszystkie swoje drzwi?

Umów rozmowę z konsultantem, wspólnie omówimy zakres testu aplikacji mobilnych i ich zaplecza (API, proxy, serwery dev) oraz to, jak taki łańcuch mógłby wyglądać u Ciebie.

Poprzedni
Jedno konto operacyjne, pełne przejęcie platformy
Następny
Pięć podatności High, zero drogi do środka

Wszystkie realizacje zanonimizowano sektorowo, bez nazw, dat i danych pozwalających zidentyfikować klienta, zgodnie z poufnością. Nie publikujemy żadnych prawdziwych podatności ani danych technicznych klienta.