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ę, iż 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ć dzięki 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ę, iż 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, iż 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ę dzięki 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 dzięki 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ć, iż 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 :).