Mamy problem z szeroko pojętą jakością oprogramowania. My, czyli nasza cywilizacja. Wciąż nie jesteśmy w stanie stworzyć oprogramowania, które nie będzie zawierało błędów. Nie jesteśmy też w stanie wychwycić wszystkich błędów na etapie testowania. I nie, nie chodzi mi tu (tylko) o błędy związane z bezpieczeństwem. Skutki błędów mogą być zarówno spektakularne (np. eksplozja rakiety Ariane 5), jak i tragiczne (np. ofiary śmiertelne źle działającego sprzętu medycznego). Mogą też być ciekawe, ostatnio przeczytałem, że jednym z ograniczeń odchodzących właśnie do historii wahadłowców było to, że misja nie mogła się przeciągać z grudnia na styczeń, choć tu nie wiem, czy jest to błąd, czy "świadome" ograniczenie. Błędem na pewno było zawieszanie Windows przy zbyt dużym uptime: Computer Hangs After 49.7 Days, choć z drugiej strony niewątpliwym sukcesem było osiąganie takiego uptime...
You just can't test everything
Kilka ciekawych przykładów błędów mogę podać też z własnego doświadczenia:
Pierwszy przykład: oprogramowanie, które nagle nie było w stanie działać poprawnie, klient nie był w stanie połączyć się z serwerem. Wszystko działało doskonale w testach, ale przy wdrożeniu u klienta najnormalniej tchórzliwie odmawiał współpracy. Okazało się, że powodem była sieciowa odległość między klientem i serwerem (magiczne pojęcie RTT). Klient wysyłał SYN, serwer odpowiadał SYN/ACK, ale zanim odpowiedź doszła do klienta, klient stwierdzał, że serwer nie odpowiada i kończył swe działanie.
Drugi przykład: wyjątkowo ciekawy przypadek błędu typu Time-of-check Time-of-use. Tu w celu wywołania błędu należało wysyłać żądania do aplikacji wystarczająco szybko. Jeśli sieć między klientem i serwerem nie przekroczyła pewnej granicznej wartości, serwer miał na na tyle dużo czasu między żądaniami, by obsługa żądania skończyła się przed rozpoczęciem obsługi kolejnego. Ciekawe rzeczy zaczynały się dziać dopiero wtedy, gdy obsługa kolejnych żądań zaczęła zachodzić na siebie.
Trzeci przykład: błędy w operacjach składających się z wielu (n) kroków. Jeśli w kroku n-1 cofnęło się do kroku n-3, a następnie przeskoczyło do kroku n, aplikacja pozwalała na coś, na co pozwalać nie powinna.
Czwarty przykład: błędy występujące w wyniki manipulacji więcej niż jednym parametrem żądania. Nie, nie jakiejś intuicyjnej manipulacji. Na przykład niech będą dwa parametry, z których jeden przyjmuje wartości z zakresu A-D a drugi 0-9 i nagle okazuje się, że przy kombinacji X X dzieje się coś, ale już przy X Y to samo coś się nie dzieje.
Piąty przykład: aplikacja nie działa jednemu użytkownikowi (osobie). Ale błędu nie można odtworzyć. Na tym samym użytkowniku (konto w aplikacji), takim samym systemie, tej samej przeglądarce wszystko działa. Jedyną różnicą jest kolejność języków (preferencja) wysyłana przez przeglądarkę. I właśnie to ona powoduje błąd...
Przykładów takich błędów można znaleźć wiele. Nie sposób przetestować każdy możliwy przypadek. Mimo testów QA aplikacja może zawierać błędy funkcjonalne, które będą objawiać się w specyficznych sytuacjach. Mimo testów bezpieczeństwa w aplikacji mogą znajdować się podatności, które znajdzie ktoś inny. W optymistycznym przypadku poinformuje o nich właściciela aplikacji, ale równie dobrze może z błędów skorzystać i poinformować o tym cały świat. Może też o nich nie informować, lecz wykorzystać je dla własnej korzyści. Dlatego nie można poprzestać na budowaniu mechanizmów bezpieczeństwa i ich testowaniu. Potrzebny jest jeszcze monitoring (detekcja) i reakcja, ale to już trochę inny temat.
Wracając do tematu testowania. Skoro nie można przetestować wszystkiego, co w takim razie testować? To jest bardzo dobre pytanie, niestety trudno na nie udzielić równie dobrej odpowiedzi. A i odpowiedź nie musi się spodobać każdemu. Pod rozwagę chcę poddać jeden hipotetyczny przykład.
Załóżmy, że jedną z funkcji testowanej aplikacji (niech to będzie bankowość internetowa) są szablony przelewów. Użytkownik ma możliwość zdefiniowania szablonu przelewu, w którym określa nazwę i numer konta odbiorcy. Szablon może być oznaczony jako zaufany, wykonanie operacji w oparciu o szablon zaufany nie wymaga dodatkowej autoryzacji. W pozostałych przypadkach operacja stworzona w oparciu o taki szablon musi być autoryzowana. Również modyfikacja szablonu zaufanego wymaga autoryzacji.
Mając taki opis tego jak dana funkcja powinna funkcjonować, można wygenerować następujące przypadki testowe:
- oznaczenie szablonu jako zaufanego bez autoryzacji,
- modyfikacja szablonu zaufanego bez autoryzacji,
- wykonanie operacji przelewu w oparciu o szablon niezaufany bez autoryzacji,
Ponieważ z aplikacji prawdopodobnie korzysta więcej niż jeden użytkownik, można również wymyślić serię testów związanych z kontrolą dostępu, na przykład:
- podgląd cudzego szablonu przelewu,
- stworzenie komuś szablonu,
- stworzenie komuś szablonu zaufanego,
- modyfikacja cudzego szablonu,
- modyfikacja cudzego szablonu zaufanego,
- usunięcie cudzego szablonu,
Idźmy jeszcze dalej:
- osadzenie XSS w szablonie przelewu,
- SQLi przy definiowaniu szablonu przelewu,
- definiowanie szablonu przy pomocy CSFR,
- definiowanie szablonu przy pomocy ui-redressing,
Zastanówmy się teraz przez chwilę nad skutkami "sukcesu" w poszczególnych wymienionych scenariuszach.
Scenariusze (1), (2) i (3) sprowadzają się do obejścia wymogu autoryzacji transakcji. Autoryzacja transakcji jest jednym z kluczowych mechanizmów bezpieczeństwa w bankowości internetowej, więc podatności pozwalające na jej obejście należy uznać za dość istotne.
Druga grupa scenariuszy (4), (5), (6), (7), (8) i (9) związana jest z kontrolą dostępu. Co do zasady jeden użytkownik nie powinien mieć dostępu do danych innego użytkownika. Trudno jednak uznać wszystkie wymienione scenariusze za równie istotne. W mojej subiektywnej wycenie ich hierarchia wyglądałaby mniej więcej tak:
- (7), (8), (5), (6),
- (4),
- (9),
Tworzenie komuś lub modyfikacja cudzych szablonów, w tym szablonów zaufanych, jest zła. Bez problemu jestem w stanie wyobrazić sobie atak, w którym ktoś modyfikuje cudze szablony podmieniając tylko numer konta odbiorcy. A potem czeka, aż na jego konto zaczną spływać comiesięczne przelewy za prąd, gaz, internet, kablówkę, (...).
Problemem może być również scenariusz (4). Możliwość dostępu do cudzych danych można uznać za ujawnienie tajemnicy bankowej, a to już może wiązać się z wymiernymi problemami, nie tyle dla klienta końcowego, co dla samego banku. Powiedzmy sobie szczerze - kontrole nie należą do przyjemności.
Scenariusz (9) można wykorzystać złośliwie - przez noc można usunąć wszystkim klientom wszystkie zdefiniowane przelewy. Z jednej strony nic się nie dzieje, pieniądze nie są wyprowadzane z kont klientów, tajemnica bankowa nie jest ujawniana (no chyba, że w trakcie usuwania pojawia się formatka potwierdzenia z danymi usuwanego szablonu). Z drugiej strony następnego dnia rano mamy tłum średnio szczęśliwych klientów, którym nagle zniknęły szablony i w celu wykonania przelewu muszą mozolnie wklepać wszystkie dane jeszcze raz. To przekłada się na "czarny PR", co z kolei może być zyskiem samym w sobie dla atakującego.
A co z kolejnymi czterema scenariuszami? W tym miejscu dochodzi się do mojego ulubionego "to zależy". Jeśli jest błąd kontroli dostępu (5), (6), (7) lub (8), to wówczas można osadzić payload XSS innemu klientowi. Jeśli takiego błędu nie ma, mamy umiarkowanie przydatny self-xss (tak, wiem: Exploiting the unexploitable XSS with clickjacking). Ale do tego przydałby się jeszcze (13) - aplikacja jako całość musiałaby być podatna na ui-redressing, co z kolei dość łatwo sprawdzić (nagłówek X-FRAME-OPTIONS).
Teoretycznie mogłaby być również taka sytuacja, w której reguły walidacji danych opisowych przelewu wprowadzanych przez szablon są inne niż przy definiowaniu przelewu "odręcznie" (formatka przelewu). Wówczas teoretycznie mogłaby zaistnieć możliwość atakowania innych klientów bankowości przez payload osadzony w tytule przelewu. Ale jeśli z innych testów wiemy, że co do zasady aplikacja stosuje prawidłowy encoding danych na wyjściu, to prawdopodobieństwo XSS spada. Oczywiście może się tak zdarzyć, że zajdą łącznie dwa zdarzenia: akurat w tym miejscu (definiowanie szablonu) nie będzie walidacji danych wejściowych i akurat gdzieś tytuł przelewu (lub inna dana opisowa) nie będzie prawidłowo kodowana na wyjściu.
SQLi to praktycznie zawsze zdarzenie mocno niepożądane, dlatego scenariusz (11) jest istotny do sprawdzenia. Ale znów inaczej sytuacja przedstawia się w przypadku, gdy ta podatność występuje w aplikacji w innym miejscu, a inaczej, gdy do tej pory wszystko wskazuje na prawidłową realizację warstwy dostępu do danych.
Scenariusze (12) i (13) są na szczęście łatwe do sprawdzenia. Albo aplikacja zawiera jakąś ochronę przed CSRF, albo nie zawiera. Albo ustawia nagłówek X-FRAME-OPTIONS (świadomie pomijam framebusting), albo nie. Prawie. Nie mogę wyjść z podziwu jak często token anti-CSRF jest stosowany wybiórczo (czasem na formatce się pojawia, czasem nie). W jeszcze głębsze zdumienie wprawia mnie to, że czasem token nie jest weryfikowany, mimo tego, że na formatce się pojawia. Sprawdzać działanie tokenu anti-CSRF na każdej formatce? A może przetestować gruntownie ten mechanizm na jednej formatce i później zweryfikować spójność działania na kilku inny (losowo?) wybranych? Jeszcze lepiej nie tych losowo wybranych, ale w tych miejscach, gdzie ewentualne istnienie CSRF ma rzeczywisty wpływ na ryzyko.
To tylko przykład ile scenariuszy testowych można wygenerować dla jednej, stosunkowo prostej funkcji aplikacji. Nawet nie tyle scenariuszy testowych, co potencjalnych celów, które może chcieć osiągnąć atakujący. Samych scenariuszy testowych sprawdzających poszczególne sposoby osiągnięcia wyznaczonego celu, może być wiele. Aplikacja takich funkcji może mieć kilkanaście, kilkadziesiąt, kilkaset. Ile to scenariuszy (celów), ile przypadków testowych?
Skoro nie można przetestować wszystkiego, dobrze jest w jakiś sposób racjonalizować obszary, które podlegają testowaniu, jak również cele atakującego, które są sprawdzane. Dobrze jest sobie wynotować co zostało sprawdzone, a co nie. Być może kiedyś przyjdzie czas i na te mniej ważne rzeczy, jak już te o najwyższym wpływie na ryzyko zostaną sprawdzone i poprawione. Tak w temacie: Risk-based testing.
I w ramach bonusa: CVE-2011-1281: A story of a Windows CSRSS Privilege Escalation vulnerability. Ile lat miał kod, w którym błąd występował? Ile w tym czasie zostało znalezionych błędów w Windows, w tym również w tym podsystemie? Aż w końcu przyszedł taki j00ru, i go wydłubał :)
Dziękujemy za publikację - Trackback z dotnetomaniak.pl
Przesłany: Jul 15, 12:38
Bardzo wiele zapytań ofertowych dotyczących "audytu bezpieczeństwa" (w rzeczywistości przedmiot zamówienia z audytem ma niewiele wspólnego) zawiera założenie/wymaganie odnośnie zastosowania "metodologii black box". Moim zdaniem w zdecydowanej większości w
Przesłany: Aug 18, 21:50
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 m
Przesłany: Oct 04, 07:14