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.
0b10 | 0b01 exploit
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,
- XSS,
- możliwość wysłania żądania z metodą TRACE,
- serwer obsługuje metodę TRACE,
- XSS,
- identyfikator sesji wypisywany w treści strony,
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ć...)
1) Sprawdzenie na ile aplikacja jest zgodna z najlepszymi praktykami inżynierskimi. W takim przypadku nie mamy do czynienia z testem penetracyjnym tylko skanem podatności lub oceną bezpieczeństwa i 80% wyników dostarczy nam automat. Tu wpadają wszystkie brakujące httpOnly i podobne "podatności". PoC nie jest wymagany. Jestem zwolennikiem robienia takich testów na etapie rozwijania aplikacji choćby i co tydzień. A jeszcze lepiej przy pomocy SCA bo nie trzeba mieć do tego działającego prototypu.
2) Sprawdzenie czy ta KONKRETNA instancja aplikacji jest podatna na REALNE ataki - czyli czy można z niej ukraść dane albo wzbogacić XSSem czy malware. Jest to test penetracyjny w jego historycznym i dosłownym znaczeniu. Odpowiada na pytanie: czy tę aplikację można dopuścić do użytku? Oczekiwałbym tutaj konkretnego PoC. I zdecydowanie nie chciałbym tutaj widzieć odpowiedzi w stylu "istnieje potencjalna możliwość iż zostanie to pod pewnymi warunkami ewentualnie wykorzystane". Na liście podatności nie chciałbym widzieć "brak httpOnly", bo to niczego nowego nie wnosi, tylko czy da się ukraść sesję czy nie.
Stosując terminologie inżynierską w obu przypadkach mamy do czynienia z pomiarami. W pierwszymi przypadku pomiarem zgodności z najlepszymi praktykami inżynierskimi. Wynik ten może być skorelowany z ryzykiem aplikacji ale luźno i w mglisty sposób. Z realnym pomiarem ryzyka aplikacji mamy w drugim przypadku.
Mnie bardziej podoba się podejście sugerowane przez ASVS. Moim zdaniem daje ono szerszą perspektywę, niż podejście, które z dużym prawdopodobieństwem sprowadzi się do znalezienia jednej/kilku najbardziej oczywistych podatności i eksploitowania ich do granic możliwości.
W każdym razie temat jest istotny i wydaje mi się, że można taką dyskusję uskutecznić np. w ramach OWASP. Podobnie jak tematy, o których ostatnio pisałeś - jak NIE zamawiać testów
Stąd prosta operacja, którą zrobiłem u siebie polegała na przedefiniowaniu zakresu i celu testów. Test 3rd party jest płacony po staremu, od dnia, ale celem nie jest już "znalezienie podatności wg OWASP Top 10" (klasyka tego typu zamówień) tylko wskazanie realistycznych scenariuszy ataku. PoC jest mile widziany (zwłaszcza jeśli jest to jeden URL) ale nie kluczowy. Chodzi o to, żeby zespół nie skupiał się na wyszukiwaniu łatwych do znalezienia bzdur w stylu "brakująca flaga httpOnly" w celu zapchania raportu, tylko wskazał REALNE RYZYKA aplikacji. Dla ułatwienia PRZED testem podsyłam im raport z Burpa czy AppScana, żeby mieli świadomość czego NIE muszą wykazać w rapocie. Jeśli nie w raporcie napiszą nic - to super, tym lepiej!
Przed tym eksperymentem przeprowadziłem rozmowę z naszym dostawcę w celu wyjaśnienia im o co mi chodzi. Co ciekawe, ich reakcja była dość entuzjastyczna i wcale nie wyglądali na zmartwionych tym, że w idealnym przypadku raport mógłby być teoretycznie pusty przez co ja mógłbym teoretycznie pomyśleć, że nic nie zrobili
Do tej pory wyniki tego eksperymentu są dobre. Jeśli raport zawiera 1-2 realne dziury z PoC to developerom można to wytłumaczyć czarno na białym, bez budowania mało przekonujących scenariuszy rodem z The Incredible Machine ("brak httpOnly teoretycznie grozi tym, że gdybyście potencjalnie używali..."). A ja oczywiście mam też raport z AppScana czy Burpa i jeśli ma to sens to dodanie httpOnly też zalecam, albo w pewnych przypadkach wymagam.
ASVS - jak najbardziej. Próbuję to też wprowadzić ale im bardziej próbuję tym bardziej zakresy ASVS rozjeżdżają mi się z rzeczywistością. Koncepcja różnych poziomów dokładności testowania jest bardzo dobra (podobnie zresztą jest w Common Criteria) ale poziomy ASVS wydają mi się zbudowane trochę sztucznie i na siłę.
Po prostu, jeśli testuje się X to wiadomo, że przy okazji przetestuje się też Y - a w ASVS są one sztucznie rozdzielone na różne poziomy. Podział na testy ręczne i automatyczne też jest sztuczny - przecież nikt nie robi testów w 100% ręcznych, a test automatem ma sens tylko wtedy gdy ktoś te wyniki ręcznie zweryfikuje (zwłaszcza przy SCA). Mam pewien zbudowany ad hoc podział tych technik na różnych poziomach wg dokładności i kosztu ale muszę nad nim jeszcze popracować.
W opisanym przez Ciebie przypadku podejście "znajdź realny scenariusz" ma szanse powodzenia. W pewnym stopniu jest to poskładanie klocków, które generuje na wyjściu skaner w coś, co można realnie użyć. To trochę tak, jakby z tych klocków próbować ułożyć całe drzewko.
Ja wolę, mimo wszystko, podejście od strony weryfikacji istnienia mechanizmu bezpieczeństwa i jego poprawnego działania. Ale takie podejście świetnie jest uzupełniane przez to, które opisałeś. Być może przy oryginalnych "drzewkach" coś nie zostało uwzględnione lub zmieniła się sytuacja/stan wiedzy na tyle, że drzewko musi zostać przebudowane.