Przejdź do głównej zawartości

Analiza podatności CVE-2021-22119

W tym wpisie chciałbym przyjrzeć się jednej z podatności bezpieczeństwa, którą odkryłem w jednym z projektów wykorzystując plugin do mavena skanujący zależności - org.owasp:dependency-check-maven. Narzędzie to pomaga wychwycić znane podatności, które dotyczą naszej aplikacji tylko dlatego, że korzystamy z konkretnej wersji zależności (biblioteki, frameworka), która zawiera błąd bezpieczeństwa. Błędy te zaliczają się do kategorii A06 (przed 09.2021 - A09) z listy OWASP TOP 10 (https://owasp.org/www-project-top-ten/). Swoją drogą polecam każdemu przeskanowanie swoich projektów takim skanerem aby zobaczyć jak wiele niebezpieczeńsw pociąga za sobą beztroskie korzystanie z mnóstwa bibliotek. Względne bezpieczeństwo daje nam tylko przypadek spowodowany tym, że akurat nie korzystamy z jakiejś konkretnej funkcjonalności. Tak będzie i w moim przypadku :).

CVE-2021-22119

Dokładny opis błędu można znaleźć m.in. tu https://nvd.nist.gov/vuln/detail/CVE-2021-22119.

Dlaczego skupiłem się na tym zgłoszeniu? Bo w pewnym momencie pojawiło się jako nowa, świeża podatność w projekcie, który pod tym względem monitorowałem. Postanowiłem przyjrzeć się problemowi głębiej w celu upewnienia się, że projekt jest niezagrożony.

Jakiej zależności dotyczy problem? Spring Security w wersjach 5.5.x (<5.5.1), 5.4.x (<5.4.7), 5.3.x (<5.3.10) i 5.2.x (<5.2.11). Wersje te są podatne na atak DoS (Denial of Service). Może on wystąpić w przypadku gdy korzystamy z uwierzytelniania z użyciem protokołu OAuth 2.0, koniecznie w trybie Authorization Code. Czyli częsty sposób wykorzystania gdy chcemy pozwolić zalogować się użytkownikowi do naszego serwisu korzystając jednej z popularnych platform (jak Facebook, Github, Google, itd..). Możliwe jest całkowite wykorzystanie dostępmych zasobów aplikacji i w efekcie doprowadzenie do jej zatrzymania.

Zasadę działania OAuth 2.0 w trybie Authorization Code najlepiej przypomnieć za pomocą diagramu sekwencji:

Pierwszym krokiem w sekwencji pozyskania dostępu do zasobu chronionego jest wysłanie żądania Authorization Request, które na diagramie zaznaczono przez pogrubienie. Efektem tego żądania jest przekierowanie użytkownika na stronę pozwolającą nadać dostęp do zasobu. To właśnie to żądanie nas interesuje. Okazuje się, że implementacja Spring Security we wskazanych wyżej wersjach pozwala generować mnóstwo kolejnych żądań w ramach jednej sesji HTTP, zapisując je w pamięci aplikacji. Będą one trwały w pamięci dopóki cały proces autoryzacji nie zostanie ukończony. A wcale nie trzeba go przecież ukończyć.

Poprawka jaka powstała polega na zmianie domyślnego zachowania, które domyślnie nie pozwala generować wielu żądań w ramach jednej sesji, a zamiast tego zastępuje każde poprzednie żądanie kolejnym. Kod odpowiedzialny za całę to zachowanie można dojrzeć w następującej metodzie: org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository#saveAuthorizationRequest.

Tak wygląda kod przed poprawką:
public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) {
        Assert.notNull(request, "request cannot be null");
        Assert.notNull(response, "response cannot be null");
        if (authorizationRequest == null) {
            this.removeAuthorizationRequest(request, response);
        } else {
            String state = authorizationRequest.getState();
            Assert.hasText(state, "authorizationRequest.state cannot be empty");
            Map<String, OAuth2AuthorizationRequest> authorizationRequests = this.getAuthorizationRequests(request);
            authorizationRequests.put(state, authorizationRequest);
            request.getSession().setAttribute(this.sessionAttributeName, authorizationRequests);
        }
    }
A tak po:
public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) {
        Assert.notNull(request, "request cannot be null");
        Assert.notNull(response, "response cannot be null");
        if (authorizationRequest == null) {
            this.removeAuthorizationRequest(request, response);
        } else {
            String state = authorizationRequest.getState();
            Assert.hasText(state, "authorizationRequest.state cannot be empty");
            if (this.allowMultipleAuthorizationRequests) {
                Map<String, OAuth2AuthorizationRequest> authorizationRequests = this.getAuthorizationRequests(request);
                authorizationRequests.put(state, authorizationRequest);
                request.getSession().setAttribute(this.sessionAttributeName, authorizationRequests);
            } else {
                request.getSession().setAttribute(this.sessionAttributeName, authorizationRequest);
            }

        }
    }

Exploit

Żeby nikt nie musiał mi wierzyć na słowo, że problem istniał przygotowałem testową aplikację oraz kod, który wykorzystuje podatność. Całość udostępniłem na Githubie pod tym linkiem: https://github.com/mari6274/oauth-client-exploit.

W repozytorium znajdują sie 2 aplikacje. oauth-client to aplikacja Spring Boot, która udostępnia endpoint do uwierzytelniania się za pomocą OAuth 2.0. Ja test wykonywałem wykorzystując integrację z Github. W tym celu, do uruchomienia aplikacji konieczne jest dodanie 2 propertiesów:

spring.security.oauth2.client.registration.github.clientId: <tu twój clientId>
spring.security.oauth2.client.registration.github.clientSecret: <tu twój clientSecret>

Jak można łatwo zauważyć (np w zakładce network swojej przeglądarki), wykonanie żądania na udostępniony endpoint /user generuje serię przekierowań. Nas będzie interesować żądanie GET http://localhost:8080/oauth2/authorization/github. I ten adres jest właśnie celem ataku drugiej aplikacji exploit. Prosty kod java, który za pomocą klienta Apache HttpClient wykonuje w pętli tysiące żądań http. Dodatkowo podzielone jest to na kilka wątków. Każdy z nich tworzy własnego klienta, który korzysta z własnego połączenia (i sesji) http.

W celach łatwiejszego unaocznienia problemu aplikację oauth-client uruchomiłem z ograniczonym zasobem pamięci: -Xmx256m. Poniżej screeny z wyników testu wersji Spring Security 5.4.1:

Spring Security 5.4.9:

Widać, że w wersji zawierającej błąd aplikacja w ciągu zaledwie jednej minuty została wysycona z zasobów pamięci, zaczęła generować błedy java.lang.OutOfMemoryError i zatrzymała się. Wykresy zużycia zasobów dla aplikacji korzystającej z wersji zależności bez błędu wskazuja natomiast na jej stabilne zachowanie - zarówno pod kątem użycia pamięci jak i CPU.

Podjęte akcje: powyższa analiza, wykluczenie wykorzystania OAuth 2.0 w trybie Authorization Code i ostatecznie uaktualnienie wersji Spring Security - choć to ostatnie można zrobić w zasadzie w ciemno po odczytaniu raportu ze skanu zależnosci :).

Komentarze

Popularne posty z tego bloga

Spring Data - save vs saveAndFlush

Cześć, dzisiaj będzie znowu trochę o warstwie persystencji. Czasami kodzie aplikacji korzystającej ze Spring Data można napotkać użycia metody repozytorium save , a czasami saveAndFlush , a z kolei innym razem brak jakiejkolwiek z nich podczas zapisu obiektu. Wszystkie trzy metody mają swoje zastosowanie choć nieco się różnią. Teoria W teorii, różnica jest prosta. Metody save oraz saveAndFlush dodają obiekt do kontekstu persystencji danej sesji i zwracają obiekt zarządzany. Ponadto druga z nich wymusza wymusza wykonanie nagranych przez ORM akcji na bazie danych przez co dane zostają przesłane do silnika bazy. Może się to okazać przydatne w przypadku gdy w ramach jednej transakcji chcemy jeszcze wykonać kolejne zapytania, które mają być świadome wprowadzonych wcześniej zmian. Natychmiastowa synchronizacja może się również przydać gdy wykorzystujemy poziom izolacji READ_UNCOMMITTED . Czasami jednak w kodzie nie ma żadnej z nich. Wtedy możliwe jest wykonywanie tylko zapytań UPDATE

Pułapki logowania w Javie

Czy spotkałeś kiedyś się z kodem takim jak poniższy? if (logger.isDebugEnabled()) { logger.debug("Request: " + requestObject); } Ja tak. Jakiś czas temu napotkałem na mnóstwo takich fragmentów w kodzie, który analizowałem. Zastanowiło mnie po co została wykorzystana tutaj instrukcja warunkowa - a jako, że if-y uwazam za zło konieczne postanowiłem to zbadać. Czy nie do tego właśnie służą biblioteki do logowania i udostępniane przez nie metody jak debug żeby właśnie sterowane konfiguracją decydowały co zalogować a co nie? Co więcej, okazało się, że instrukcję warunkową wprowadził ktoś w ramach większej akcji. I jak się okazuje, prawdopodobnie znacznie zwiększył dzieki temu wydajność aplikacji. W dzisiejszym wpisie postaram się wyjaśnić jak korzystać z loggera aby nie zaszkodziło to wydajności. Po co if-y? Na początek wyjaśnić trzeba czemu ten if tak na prawdę służy. Korzystamy z biblioteki do logowania i spodziewamy się, że wykorzystujac wybraną metodę, zostanie

Sneaky throws, czyli checked i unchecked exception w jednym!

Często podczas pisania kodu Java korzystam z wyrażeń lambda wprowadzonych wraz z Java 8. Często prowadzi to też do pewnych nowych problemów i zmusza do szukania rozwiązań. Przyjrzyjmy się jednemu z nich. Checked exception w wyrażeniu lambda Często zdarza się, że metoda wywoływana wewnątrz lambdy rzuca wyjątek. Nie ma sprawy gdy wyjątek jest obiektem klasy będącej podklasą RuntimeException . Wtedy po prostu się nim nie przejmujemy. Wyjątek jest przekazywany w górę stosu wywołań. Problem zaczyna się gdy kompilator zmusza nas do obsługi wyjątku. Jak zwykle mamy 2 wyjścia: obsłużyć wyjątek za pomocą bloku try-catch , bądź zadeklarować przekazanie wyjątku dalej za pomocą słowa kluczowego throws . O ile wiemy co zrobić po złapaniu wyjątku to wszystko gra. Co natomiast gdy chcemy wybrać drugą opcję? Większość interfejsów stosowanych jako typy parametrów, często przekazywanych jako lambdy jak np: Function , Consumer czy Supplier nie deklarują, że mogą rzucić wyjątek. W takim wypadku druga