Sprawa wydaje się prosta, a przypadek testowy może wyglądać następująco:
- zaloguj się do aplikacji,
- wykonaj jakąś
informację operację (np. otwórz formatkę listującą dane),
- wyloguj się z aplikacji,
- odtwórz wcześniej wykonaną operację,
Jeśli przy odtworzeniu wcześniej zapisanego żądania dane zostaną wyświetlone - jest problem. Założenie jest takie, że po wylogowaniu identyfikatory sesji nie mogą zostać użyte ponownie. To znaczy mogą, ale w najlepszym przypadku powinny być traktowane jako identyfikatory sesji nieuwierzytelnionego użytkownika.
Wszystko jest pięknie, jeśli mamy prostą i jasną sytuacje - jest jedno cookie sesyjne z identyfikatorem sesji, znajomość wartości tego identyfikatora daje dostęp do sesji użytkownika (pozwala działać w jego kontekście), oczywiście pod warunkiem, że dana sesja jest właśnie zalogowana. Sytuacja nieco komplikuje się, gdy pojawiają się rozwiązania typu SSO (single sign-on). Wówczas pojawia się więcej cookies i unieważnienie sesji wcale nie oznacza, że nie można użyć innych identyfikatorów, by taką sesję "wskrzesić". A właśnie, te "inne identyfikatory" niekoniecznie są identyfikatorami.
Jednym z możliwych podejść w przypadku SSO jest wystawianie tokenu użytkownika. Tego tokenu użytkownik może użyć w celu uwierzytelnienia się w kolejnych aplikacjach, już bez podawania nazwy użytkownika i hasła. Token ten często zawiera dane o tożsamości użytkownika i jego uprawnieniach. Zwykle całość jest szyfrowana, konieczne jest również zapewnienie integralności tokenu oraz możliwość weryfikacji jego autentyczności. Oczywiście taki token jest ograniczony czasowo, może być ważny od kilku sekund do kilku godzin (lub nawet jeszcze dłużej).
By uzyskać dostęp do sesji użytkownika w takich przypadkach wymagane jest posiadanie zarówno "zwykłego" identyfikatora sesji, jak również tokenu. W chwili wylogowania oba identyfikatory są usuwane z przeglądarki, dodatkowo identyfikator "zwykłej" sesji jest unieważniany po stronie serwera, a jej zawartość jest niszczona. Co jednak, gdy ktoś posiada zapisane wartości identyfikatorów (tokenów)?
Najpierw może dodatkowe wyjaśnienie. Przez pojęcie "zwykłej" sesji rozumiem ten mechanizm, który pozwala programiście zapisać (do sesji) jakieś dane i korzystać z nich w dowolnym momencie, przez cały czas jej trwania. W sesji mogą, ale nie muszą znajdować się informacje o tożsamości użytkownika. Nie muszą, bo w części rozwiązań informacja o tożsamości użytkownika jest odtwarzana przy każdym kolejnym żądaniu na podstawie danych zawartych w tokenie (tym drugim "identyfikatorze"). Jeśli aplikacja przechowuje dane o tożsamości użytkownika i jego uprawnieniach w sesji, to jej zniszczenie powoduje, że jej identyfikator nie może być użyty do wykonania żadnych działań w kontekście użytkownika, do którego ta sesja należała. Identyfikator będzie wówczas wskazywał na nieistniejącą sesję (wówczas stworzona zostanie nowa sesja, w części przypadków z nowym identyfikatorem), lub "pustą" sesję, która nie zawiera żadnych danych. Tyle teoria. Dość typowym błędem była jednak taka implementacja wylogowania użytkownika, która tylko usuwała/zmieniała identyfikator sesji w przeglądarce użytkownika, sam obiekt sesji wciąż jednak istniał po stronie serwera, wciąż zawierał wszystkie dane i był dostępny w przypadku użycia odpowiedniego (wcześniejszego - tego sprzed zmiany) identyfikatora sesji. I właśnie istnienie takiej sytuacji ma sprawdzać omawiany punkt ASVS - wylogowanie ma być skuteczne.
Pora na schody. To, co nazwałem tokenem może być zaimplementowane na wiele sposobów. W tym przypadku chodzi mi o przypadek, w którym pewien serwer/moduł wydaje użytkownikowi "poświadczenie" jego tożsamości. Użytkownik potwierdza tożsamość tylko raz pewnemu "nadzorcy" i otrzymuje od niego "plakietkę", którą prezentuje innym modułom. Moduły nie weryfikują już tożsamości użytkownika, sprawdzają natomiast czy "plakietka" została wydana przez kogoś, komu ufają i czy jest jeszcze ważna. Działa to dokładnie tak samo, jak dowód osobisty albo prawo jazdy. Osoba weryfikująca tożsamość sprawdza dokumenty (ich autentyczność, ważność) i poprzez zaufanie do wystawcy dokumentów przyjmuje, że Jan Kowalski to Jan Kowalski. Problem w tym, że Jan Kowalski mógł utracić dokument (na przykład zgubić), niezbyt uczciwy znalazca może jednak spróbować je wykorzystać. Dlatego pewnym dodatkowym elementem weryfikacji ważności dokumentu jest jego sprawdzenie w rejestrach, na przykład w rejestrze dokumentów zastrzeżonych. Podobnym mechanizmem są listy CRL dla certyfikatów, które, jak się okazało, umiarkowanie sprawdzają się w praktyce.
Problem z wieloma rozwiązaniami korzystającymi z takich tokenów polega na tym, że nie pozwalają one na unieważnienie wydanego poświadczenia. Użytkownik może korzystać z otrzymanego tokenu tak długo, jak długo jest on ważny. To z kolei oznacza, że w wielu przypadkach nie ma możliwości skutecznego wylogowania się z aplikacji. Albo inaczej - wylogowanie następuje, ale na podstawie tego tokenu można (na szczęście tylko przez ograniczony czas) ponownie zalogować się do aplikacji bez znajomości nazwy użytkownika i hasła. Tak jest między innymi w przypadku ASP.NET i Forms Authentication, albo środowiska WebSphere i LtpaToken2.
Jaki w takim przypadku powinien być rezultat weryfikacji omawianego wymagania? Co uznać za sesję? Czy istotne jest dla nas to, co zostanie zapisane do "zwykłej" sesji, czy raczej fakt, że dysponując tokenem będziemy w stanie stworzyć nową sesję, w której będziemy mogli działać w kontekście naszej ofiary?
Przy okazji warto dodać, że ten token w wielu przypadkach nie jest chroniony tak, jak powinien. Dla przykładu prosty scenariusz - załóżmy, że w mojej organizacji wykorzystywana jest domena threats.pl i poszczególne aplikacje dostępne są pod adresami app1.threats.pl, app2.threats.pl, (...). Cookie SSO, by spełniło swoje zadanie, będzie wystawione na domenę .threats.pl, co zgodnie z same-origin policy dla cookies oznacza, że zostanie on wysłany również na adres evil.threats.pl, który może być kontrolowany przez osobę o nie do końca czystych intencjach. Teraz wystarczy, by wybrana ofiara odwiedziła ten serwer - jej przeglądarka wyśle cookie SSO do serwera atakującego, a atakujący będzie w stanie uwierzytelnić się jako ofiara w dowolnej aplikacji akceptującej cookie SSO. Do tego jeszcze można dodać brak flag secure i httpOnly na cookie, co pozwala na poznanie jego wartości również w inny sposób (XSS, podsłuch ruchu sieciowego).
Samo rozwiązanie SSO może zostać zaimplementowane na wiele sposobów. Część z nich problemy, o których tutaj pisałem, nie dotyczą. Czasami za to pojawiają się inne. Na przykład jednym z innych rozwiązań jest jawne wydzielenie aplikacji "nadzorcy" odpowiedzialnej za uwierzytelnienie użytkownika i przekazanie informacji o jego tożsamości i uprawnieniach do właściwej aplikacji. Samo uwierzytelnienie wygląda mniej więcej tak:
- użytkownik próbuje uzyskać dostęp do aplikacji app1,
- aplikacja app1 stwierdza, że użytkownik jest nieuwierzytelniony i przekierowuje go do aplikacji gatekeeper,
- w aplikacji gatekeeper użytkownik uwierzytelnia się i otrzymuje:
- token do aplikacji gatekeeper, który może użyć przy logowaniu do innej aplikacji,
- token do aplikacji app1,
- użytkownik jest przekierowany do aplikacji app1,
W przypadku, gdy użytkownik chce uzyskać dostęp do aplikacji app2 cała zabawa wygląda dokładnie tak samo, przy czym użytkownik nie musi uwierzytelniać się po raz kolejny, bo może się przedstawić aplikacji gatekeeper tokenem, który otrzymał od niej wcześniej. Jeśli komuś ten schemat wydaje się znajomy, to prawdopodobnie ma rację :)
Token dla aplikacji gatekeeper jest zwykle ustawiany w taki sposób, by był wysyłany wyłącznie do tej aplikacji. Problem polega na tym, że często poszczególne aplikacje (app1, app2 oraz gatekeeper) dostępne są na jednym serwerze (np. app.threats.pl), ale w różnych ścieżkach. No i niestety pojawia się tutaj drobny problem wynikający z różnic w same-origin policy dla cookies i dla dostępu JavaScript do DOM. XSS w dowolnej aplikacji może doprowadzić do przejęcia tokenu do aplikacji gatekeeper. Może, bo trzeba pamiętać o fladze httpOnly.
Spotkałem się kiedyś z inną ciekawą sytuacją. Każda aplikacja miała swój własny adres. Cookie były oznaczone odpowiednimi flagami, a i tak udało mi się zalogować do dowolnej aplikacji w kontekście ofiary. W jaki sposób? To było ciekawe złożenie kilku różnych podatności.
Punktem wyjścia był XSS (stored) w dość często odwiedzanym przez typowego użytkownika miejscu. Osadziłem tam payload, który powodował odwołanie się przeglądarki ofiary do aplikacji, do której chciałem uzyskać dostęp (załóżmy, że to była aplikacja app3). Aplikacja przekierowywała nieuwierzytelnionego użytkownika do aplikacji gatekeeper. Jeśli użytkownik już z niej korzystał, posiadał odpowiedni token, wystawienie tokenu do docelowej aplikacji odbywało się automatycznie. Konkretnie generowana była strona HTML na której był osadzony formularz zawierający wystawiony token. Formularz ten był automatycznie wysyłany na adres podany w parametrze przez aplikację app3 przy przekierowaniu do aplikacji gatekeeper. Tylko, że:
- tworzony formularz był podatny na XSS w adresie strony docelowej (na który token miał być wysłany),
- adres docelowy był częściowo kontrolowany przez atakującego - był to ten adres z aplikacji app3, do której odwołał się użytkownik,
Wystarczyło tak spreparować pierwszy payload XSS, by w adresie odwiedzanej strony w aplikacji app3 był osadzony kolejny payload XSS, tym razem na aplikację gatekeeper. Aplikacja app3 grzecznie przekazywała ten drugi payload do podatnej aplikacji. Jego zadaniem było odczytać wydany dla użytkownika token i przesłać go do mnie. Nie pozostało nic więcej, jak tylko go użyć do uwierzytelnienia się w aplikacji app3. Prawda, że proste?
Pora kończyć ten wpis, bo widzę, że mocno odjechałem w stosunku do pierwotnego tematu :)
"wykonaj jakąś informację "