0b10 | 0b01 exploit
Od dłuższego czasu staram się nie używać pojęcia “test penetracyjny” do określenia tego, czym się głównie zajmuję. Wolę określenia typu security assessment , czy coś w stylu testowanie bezpieczeństwa lub weryfikacja mechanizmów bezpieczeństwa. Nie chodzi mi o pogłębianie/porządkowanie chaosu pojęciowego (walka z używaniem pojęcia “audyt” w kontekście, do którego kompletnie nie pasuje to taka współczesna wersja walki z wiatrakami), chodzi mi natomiast o podkreślenie różnic w celach.
Cofnijmy się może do starszego wpisu, You just can't test everything. Pokazałem tam, jak można dla pewnej funkcji wygenerować kilka(naście) scenariuszy, których możliwość przeprowadzenia chcemy zweryfikować. Oczywiście każdy z tych scenariuszy możemy próbować realizować na kilka różnych sposobów. Niech przykładem będzie tutaj możliwość modyfikacji cudzego szablonu przelewu. Możemy to osiągnąć na wiele sposobów, na przykład:
- błąd kontroli dostępu (manipulacja identyfikatorami globalnymi),
- przejęcie sesji właściciela szablonu przelewu,
- odgadnięcie/złamanie/wyłudzenie hasła właściciela przelewu,
- przygotowanie ataku CSRF,
- przygotowanie ataku typu ui redressing ,
- wykorzystanie podatności XSS (stored) i osadzenie payloadu modyfikującego szablon przelewu,
- wykorzystanie podatności XSS (reflected), przygotowanie ataku,
- (...) pewnie by się jeszcze coś znalazło ,
Powyższa lista jest w pewnym sensie “listą scenariuszy, które pozwalają zrealizować scenariusz”. Można spróbować znaleźć root cause , słabość, właściwie podatność, której istnienie pozwala na realizację tych scenariuszy. Listę takich przyczyn można przedstawić na przykład tak (w pewnych przypadkach będzie to mapowanie 1:1):
- błąd kontroli dostępu,
- brak flagi httpOnly ,
- brak flagi Secure ,
- wykorzystanie nieszyfrowanego kanału komunikacji,
- brak mechanizmu uniemożliwiającego/spowalniającego atak brute force na hasło użytkownika,
- brak mechanizmu wymuszającego złożoność hasła,
- brak uwierzytelnienia dwuskładnikowego,
- brak nagłówka X-FRAME-OPTIONS,
- brak walidacji danych wejściowych,
- brak właściwego encodingu danych wyjściowych,
- XSS,
- SQL Injection,
- brak unikalnego tokenu w żądaniu,
- nieprawidłowa weryfikacja unikalnego tokenu w żądaniu,
- przechowywanie hasła w formie jawnej,
- przechowywanie hasła w postaci hasha łatwego do złamania,
- wykorzystanie “słabego” mechanizmu odzyskiwania hasła,
- (...) też by się jeszcze pewnie coś znalazło ,
Część z tych podatności ma charakter “lokalny”. Błąd kontroli dostępu w module komunikacji, który pozwala mi czytać/usuwać cudze wiadomości nie pozwala mi modyfikować cudzego szablonu przelewu (pominę tu pomysły “a może w wiadomościach jest hasło użytkownika lub coś co pozwoli (...)”). XSS istniejący w tym samym module już mi na to pozwoli (co najmniej na kilka sposobów), choć może się okazać, że jedynym potencjalnym atakującym będzie pracownik banku (XSS na samego siebie pomijam, choć teoretycznie byłby możliwy przy pomocy clickjacking, albo, co może mieć większe prawdopodobieństwo sukcesu, przy pomocy CSRF). Spory potencjał dałoby mi również SQL Injection w tym lub dowolnym innym module.
Załóżmy, że aplikacja ma problem, z walidacją danych wejściowych i encodingiem danych wyjściowych czego skutkiem są liczne XSSy. Czy dla każdego scenariusza (tego głównego), dla każdego przypadku XSS trzeba udowadniać, że woda jest mokra? Innymi słowy, czy dla każdej znalezionej podatności trzeba przygotować PoC tylko po to, by pokazać, że rzeczywiście da się ją wykorzystać? Albo nie czepiajmy się już tak bardzo tych XSSów, równie dobrym przykładem jest brak unikalnego tokenu (CSRF) czy brak nagłówka X-FRAME-OPTIONS (clickjacking). Szczerze mówiąc uważam, że udowadnianie oczywistych oczywistości może być stratą czasu i pieniędzy. To takie zrywanie nisko wiszącej wisienki na 100 różnych sposobów, podczas gdy wisienek do zerwania jest dużo, dużo więcej. Czasami tylko trzeba podskoczyć, przynieść drabinę albo po prostu podejść do drzewa z innej strony.
Moim zdaniem bardziej efektywnym podejściem jest nastawienie się na identyfikację jak największej ilości podatności. Postaram się to pokazać na obrazkach.
Załóżmy, że schemat po lewej stronie opisuje sposoby osiągnięcia przez atakującego swojego celu. Są to sposoby zidentyfikowane , co oczywiście wcale nie musi znaczyć, że wszystkie.
Jeden ze sposobów osiągnięcia celu przez atakującego zakłada jednoczesne zaistnienie warunków, które znajdują się na gałęzi oznaczonej na czerwono. Przerwanie tej ścieżki w dowolnym miejscu spowoduje, że osiągnięcie celu w ten sposób , będzie niemożliwe.
Wyobraźmy sobie teraz, że w przypadku testowanej aplikacji rzeczywiście istnieje taka ścieżka. Można skupić się po pierwsze na udowodnieniu, że ona działa (przygotowanie działającego PoC). Można również poświęcić sporo czasu na generowanie kolejnych scenariuszy. Tak, aż całość będzie wyglądać jak na kolejnym schemacie.
Jak widać pojawiło się kilka dodatkowych ścieżek, które oznaczone są kolorem niebieskim. Wszystkie one mają jednak część wspólną – kilka słabości, które muszą istnieć, by atak się udał.
Odejdźmy teraz od abstrakcyjnych symboli i wstawmy w ich miejsce trochę bardziej konkretne przykłady. Co się stanie, jeśli słabością noszącą numer 1 jest na przykład:
- brak flagi httpOnly,
- brak flagi Secure,
- brak unikalnego tokenu (brak ochrony przed CSRF),
- brak nagłówka X-FRAME-OPTIONS (brak ochrony przed clickjacking),
- (...),
Właściwie wszystkie z wymienionych wyżej problemów mają charakter globalny. Powodują globalny problem (np. praktycznie każda formatka w aplikacji będzie podatna na CSRF), ale również można je rozwiązać stosunkowo małym nakładem pracy i rozwiązanie będzie globalne. Mówiąc o małym nakładzie pracy chodzi mi o to, że ustawienie flag Secure, httpOnly czy nagłówka X-FRAME-OPTIONS sprowadza się właściwie do modyfikacji konfiguracji serwera, z którego korzysta aplikacja. Trochę większy nakład środków wymaga ochrona przed CSRF (chyba, że też sprowadza się do włączenia jakiejś flagi konfiguracyjnej w frameworku, w którym napisana jest aplikacja).
Zwykle jest tak, że czas, który można poświęcić na testy, jest ograniczony. Oczekiwanym rezultatem testów jest zmniejszenie poziomu ryzyka związanego z używaniem tej aplikacji, a nie znalezienie kilkudziesięciu sposobów na zrobienie tego samego, które te sposoby różnią się wartością artystyczną.
W przypadku przykładowego schematu atakujący może osiągnąć swój cel na 3 różne główne sposoby. W rzeczywistości tych sposobów jest więcej, ale są trzy gałęzie wychodzące z korzenia. Jeśli one zostaną “przecięte”, atakujący nie osiągnie swojego celu. Rozbudowywanie do granic możliwości (i granic wyobraźni) pierwszej gałęzi nie może odbywać się kosztem pozostałych dwóch.
Wszystko jest proste w teorii. W praktyce sprawy robią się bardziej skomplikowane. Hipotetycznie załóżmy, że nie ma możliwości załatania problemu oznaczonego numerem 1. Wówczas tą gałąź można “odciąć” blokując scenariusze 1.1, 1.2 oraz 1.3. W takim przypadku sensowne jest szukanie scenariuszy 1.4, 1.5, (...), ponieważ one obejdą zabezpieczenia na poziomie 1.[1-3]. Podobnie sens ma szukanie scenariuszy “najwyższego poziomu”. Raz opracowane “drzewa” nie są dane raz na zawsze i wymagają regularnego przeglądania i uzupełniania.
Kolejną komplikacją jest fakt, że słabość, która w jednym drzewie znajduje się w drugim czy trzecim poziomie, albo w którym do jej wykorzystania konieczne jest jednoczesne zaistnienie drugiej słabości, sama w sobie jest wystarczająca do osiągnięcia innego potencjalnego celu atakującego.
Dobrym przykładem takiej sytuacji może być XSS. Do pozyskania identyfikatora sesji potrzebne będzie jednoczesne zajście warunków:
- XSS,
- brak flagi httpOnly,
Lub:
- XSS,
- możliwość wysłania żądania z metodą TRACE,
- serwer obsługuje metodę TRACE,
Lub:
- XSS,
- identyfikator sesji wypisywany w treści strony,
Lub: (...)
Sam XSS (lub szerzej – html injection) jest natomiast wystarczający choćby do:
- automatyzacji aplikacji (wykonanie akcji w kontekście użytkownika, potencjalnie obchodząc zabezpieczenie przed CSRF),
- dystrybucji malware (osadzenie iframe z wrogim kodem),
- (...),
- Niespodzianka - patrz niżej,
Skoro mowa o drzewkach puszczających nowe gałązki, to w kontekście przejmowania identyfikatorów sesji, flag Secure i httpOnly oraz XSS warto wspomnieć o takiej nowej gałązce właśnie. Ta gałązka nazywa się BEAST, patrz: Man-in-the-Middle Attack Against SSL 3.0/TLS 1.0. Flaga httpOnly może być ustawiona, podobnie jak flaga Secure oraz nagłówek HTTP Strict Transport Security, wystarczy możliwość wstrzyknięcia własnego kodu oraz... kilka innych warunków, które powodują, że atak nie jest trywialny do przeprowadzenia i świat się nie zawalił w chwili opublikowania tej informacji, ale mimo wszystko lepiej się zabezpieczyć. Przy okazji – ta podatność też ma charakter “globalny” :)
I tak, dla każdego znalezionego XSS można próbować udowadniać, że i w tym przypadku uda się uzyskać cookie mimo flagi httpOnly. Bazinga!
Podsumowując – moim zdaniem eksploatowanie (exploitowanie) do granic możliwości każdej/jednej ze znalezionych podatności nie jest podejściem efektywnym. Zajmuje to czas, który można wykorzystać do identyfikacji kolejnych słabości/podatności w badanej aplikacji. Zwłaszcza, jeśli podatność “na warsztacie” ma charakter “globalny” i jest łatwo usuwana “globalnie”. Wskazane jest pokrycie testami jak największej “powierzchni” aplikacji, choć tu również można wprowadzić pewne priorytety, które mogą uwzględniać na przykład to, jak wiele użytkowników (i jak bardzo (nie)zaufanych) do danej funkcji ma dostęp. Jeśli natomiast chodzi o wprowadzane zabezpieczenia, warto prześledzić jakie warunki muszą być spełnione, by atak zadziałał i ciąć jak najbliżej korzenia. A w trakcie testów trzeba dokładnie sprawdzić, czy zastosowane mechanizmy są wystarczająco skuteczne.
(Stwierdziłem, że w tej chwili kończę ten wpis, bo zaczął się za bardzo rozrastać...)
Oryginał tego wpisu dostępny jest pod adresem 0b10 | 0b01 exploit
Autor: Paweł Goleń