Zgodnie z zapowiedzią z poprzedniego wpisu wracam do tematu LastPass. Tym razem trochę na temat webowego klienta LastPass, tego co mogłoby się stać, gdyby był w nim (odpowiedni) XSS i wdrożenia Content Security Policy przez LastPass.
Olej z węża: LastPass i Content Security Policy
Co by było, gdyby...
Co chciałbym zdobyć atakując kogoś, kto korzysta z LastPass? Oczywiście najlepiej login i hasło, ale niech to będzie nieco bardziej skomplikowane. A chciałbym:
- uzyskać dostęp do konta użytkownika,
- być w stanie odczytywać/modyfikować przechowywane przez niego dane,
Czy byłbym to w stanie osiągnąć wykorzystując jakiegoś XSS, który aktywowałby się ofierze po uwierzytelnieniu się przez nią w aplikacji? Odpowiedź brzmi - kiedyś tak. Teraz zabawę popsuły dwa wredne znaki: //.
Nie, nie demonstruję w tym przypadku żadnych podatności związanych z LastPass i nie ujawniam wiedzy tajemnej. Pokazuję pewną ciekawostkę, która pozwoli lepiej uświadomić sobie potencjalne skutki wystąpienia podatności typu cross-site scripting we współczesnych aplikacjach internetowych. Takie podatności mogą spowodować zmianę sposobu funkcjonowania aplikacji, pozwalają również "dostać się do środka" stanu takiej aplikacji, w związku z czym warto czasem pomyśleć, czy nie zostawia się w nim pewnych rzeczy, które przestają być w pewnej chwili potrzebne.
Poniżej fragment funkcji obsługującej uwierzytelnienie w webowym kliencie LastPass:
(...) hash.value = make_lp_hash(u.value,p.value); //g_pw_hash = hash.value; g_local_key = make_lp_key(u.value,p.value); g_username = u.value; g_porig = p.value; p.value = ""; var eu = get('encrypted_username'); eu.value = enc(u.value); g_uvaltmp = u.value; (...)
W tym miejscu chciałem zwrócić uwagę na zmienne (globalne):
- g_local_key,
- g_username, g_uvaltmp,
- g_pw_hash (tu niestety pojawiają się te paskudne znaki //),
Zakładając, że mam możliwość uruchomienia swojego kodu JavaScript w uruchomionym kliencie, mam (prawie) wszystko, bo:
- nazwę użytkownika mogę odczytać ze zmiennych g_username i g_uvaltmp,
- hash wykorzystywany przy logowaniu mogłem (zwracam uwagę na czas przeszły) odczytać ze zmiennej g_pw_hash,
- klucz kryptograficzny jest dostępny w zmiennej g_local_key,
Tak, gdyby nie komentarz w jednej linii kodu jeden XSS pozwoliłby mi w prosty sposób osiągnąć założone cele, czyli:
- zalogować się do LastPass bez znajomości hasła użytkownika (g_username, g_pw_hash),
- przeglądać/edytować dane zapisane przez użytkownika (g_local_key),
W tej chwili nie jestem w stanie uzyskać możliwości uwierzytelnienia się do systemu, bo niestety, zarówno hasło jak i hash hasła są czyszczone po uwierzytelnieniu (przynajmniej tak wynika z szybkiego sprawdzenia/przeglądnięcia kodu. A tu obrazowe potwierdzenie obserwacji (tak, zdaję sobie sprawę z tego, co pokazuję na tym obrazu, zwłaszcza ze znaczenia g_local_key, zostawi mi ktoś notkę w LastPass?):

Jeszcze raz chcę podkreślić, że to, co pokazałem wyżej nie jest podatnością. Co więcej, tak musi być. Nazwa użytkownika jest potrzebna. Nawet jeśli nie byłaby przechowywana w zmiennej w JavaScript i tak byłaby do odczytania z DOM, no chyba, że użytkownik nie widziałby informacji na kogo jest aktualnie zalogowany (temat ewentualnego wstawiania loginu użytkownika w postaci obrazka chyba sobie odpuszczę bo jak znam życie to Krzysiek Kotowicz i tak by to wyciągnął korzystając z HTML5). Klucz kryptograficzny musi być dostępny, bo przecież dane trzeba (za|od)szyfrować (i w podobny sposób można przecież pozyskać klucz wykorzystywany przez TrueCrypt czy BitLocker), a hasha hasła już szczęśliwie nie ma.
W tym przykładzie o ewentualnym XSS można myśleć dokładnie tak samo, jak o malware na stacji. Po prostu system/aplikacja przestaje działać w sposób zgodny z intencją twórców, a wrogi kod może dowolnie modyfikować jej zachowanie. Dobrze napisany payload XSS jest właśnie takim malware. XSS to więcej niż tylko wyskakujące okienka.
I pora na snake-oil
W LastPass był już znaleziony XSS, więcej na ten temat można przeczytać tu: Cross Site Scripting vulnerability reported, fixed oraz tu: LastPass Vulnerability Exposes Account Details. Nie czepiam się tego, że XSS wystąpił. W końcu to jeden z najbardziej rozpowszechnionych błędów, patrz: CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting'). Można również pochwalić LastPass za szybką reakcję i próbę zrobienia czegoś więcej, niż tylko usunięcie wskazanej podatności. Podjęte zostały działania, które mają na celu zmniejszenie prawdopodobieństwa wystąpienia błędu oraz zmniejszenia skutków jego wystąpienia, jeśli jednak błąd wystąpi. Jeśli porównać to z sytuacją, w której błąd poprawiany jest w parametrze x1, ale linijkę dalej w x2 już nie, bo przecież nie było w raporcie, to różnica w podejściu do tematu jest uderzająca.
A teraz pora na czepianie się, cytat z Content Security Policy (CSP) implemented on LastPass.com -- the beginning of the end of bookmarklets?:
If you're using Firefox 4, you now gain the additional protection afforded by CSP (Content Security Policy) on LastPass.com.
I z końca tego samego posta:
We haven't fully locked down our CSP yet; today we're allowing every page from LastPass.com to talk to LastPass.com, but soon we'll lock this down further so that https://LastPass.com/?securitychallenge=1 can ONLY talk to https://LastPass.com/?securitychallenge=1, which will be another big step forward.
A teraz by była pełna jasność - jak wygląda CSP zaimplementowane w tej chwili przez LastPass (nagłówek X-Content-Security-Policy):
X-Content-Security-Policy: allow 'self'; img-src 'self' https://lastpass.com data: http://www.google-analytics.com https://ssl.google-analytics.com https://www.google-analytics.com; object-src 'self' http://*.youtube.com https://www.youtube.com http://*.ytimg.com https://*.ytimg.com http://www.google.com; options inline-script eval-script; report-uri /csp_report.php
Wyjaśnienie poszczególnych elementów można znaleźć tutaj: CSP policy directives.
Dla ułatwienia, CSP zdefiniowane dla LastPass zawiera następujące elementy:
- allow,
- img-src,
- object-src,
- options,
A w options znajdują się opcje:
- inline-script,
- eval-script,
To niestety znaczy, że skuteczność takiej polityki w przeciwdziałaniu XSS jest praktycznie żadna. No, może poza tym, że rzeczywiście nie uda się dołączyć payloadu z zewnętrznego serwera, co jednak nie jest problemem, jeśli nie ma ograniczenia na długość payloadu, który można wstrzyknąć. Być może również kilka sposobów na wyprowadzenie danych zostanie w pewnym stopniu ograniczonych, choć i tak zostanie całkiem sporo innych. Wstrzyknięty skrypt JavaScript zostanie jednak wykonany, bo pozwoli na to inline-script.
LastPass należą się brawa za wykorzystanie (no, przynajmniej próbę wykorzystania) wielu nowych mechanizmów oferowanych przez przeglądarki. Wydaje mi się jednak, że przed pochwaleniem się faktem "wykorzystania" Content Security Policy dobrze byłoby jednak nieco przebudować aplikację i wdrożyć taką politykę, która rzeczywiście przed czymś chroni. Bo teraz to trochę taki snake-oil wyszedł z tego... Co nie oznacza, że w przyszłości nie będzie lepiej. I tym optymistycznym akcentem kończę.