Na początku 2017 roku opublikowano informacje o groźnej podatności w bardzo popularnym pakiecie Apache Struts, pozwalającej na zdalne wykonanie kodu na serwerze, na którym działa aplikacja (RCE). O wystąpieniu podatności (CVE-2017-5638) poinformowano 6 marca, i tego samego dnia udostępniono zaktualizowaną wersję biblioteki, która eliminuje błąd. Ważne jest, aby 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[2], która w wyniku wykorzystania tej podatności padła ofiarą ataku, w którym cyberprzestępcy pozyskali dane ponad 143 milionów użytkowników. Wybrałem ten błąd bezpieczeństwa do analizy w tym wpisie 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, które działają w przestrzeni internetu a które opierają się na tym często wykorzystywanym frameworku, ale mówiąc o dziesiątkach milionów nie będę przesadzał.

A jaki jest problem? Na stronie nvd.nst.gov.com[3] możemy przeczytać, że:

A jaki jest problem? Na stronie nvd.nist.gov możemy przeczytać, że parser Jakarta Multipart w bibliotece Apache Struts 2, w wersjach 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, który jest generowany podczas próby przesyłania pliku. To umożliwia atakującemu wstrzyknięcie komend poprzez 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').(#[email protected]@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ł on, 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), która 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 czymś złym ani niebezpiecznym, 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, ale 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ć taki framework, jest możliwość przesyłania plików przez użytkowników na serwer, na przykład aby umożliwić im 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 jest jednak pamię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 po to aby można było go później obsłużyć. W pole związane z domyślną treścią wyjątku trafiła informacja o tym, że nagłówek Content-Type ma niewłaściwy format i jego wartość to %{(#_='multipart/form-data').(#[email protected]@DEFAULT_MEMBER_ACCESS).(#_memberAccess?.....

W momencie posiadania sparsowanego żądania HTTP, które zostało opakowane w klase MultiPartRequestWrapper(co zostało wykonane 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, który został umieszczony przez wrappera, znajduje się ładunek OGNL przygotowany przez użytkownika.

W tym momencie wykonywane są operacje mające na celu przygotowanie komunikatu błędu w taki sposób, aby jasno informowały o zaistniałej sytuacji. Pamiętaj, ż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, aby unikać powtarzania konkretnych instrukcji. W tym przypadku zaimplementowano weryfikacje dotyczące sposobu przygotowania treści komunikatu dla wielu różnych typów wyjątków, jednak zabrakło obsługi dla wyjątku typu struts.messages.upload.error.InvalidContentTypeException, który to błąd został zasygnalizowany przez wrappera. W scenariuszu, gdy wszystkie weryfikacje zawiodą, komunikat błędu generowany jest przez mechanizm domyślny, który, aby 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 a i poprawka, która uniemożliwiała wykorzystanie błędu była 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. Co robią te zmiany? W zasadzie odchodzą od wykorzystania LocalizedTextUtil.findText na rzecz innych metod, które nie wykonują zapytań OGNL.

Czy rozwiązanie to działa? Tak. 

Czy jest to dobre rozwiązanie? Moim zdaniem, nie. Problem zasadniczy, polegający na tym, że niezaufane dane trafiają do aplikacji, są wczytywane do jednego z obiektów, a następnie na tym obiekcie, który zawiera niezaufane dane, wykonywane są operacje, nie został rozwiązany. 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 tak – tego typu weryfikacja powinna być realizowana wszędzie i jak najczęściej, zgodnie z podejściem Zero Trust. Jednak według mnie niekoniecznie. Jeśli skoncentrujemy się na cyklu życia obiektów w naszej aplikacji i poświęcimy wystarczająco dużo uwagi, aby nigdy nie dopuścić do sytuacji, w której 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ść i 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 a to dlatego, że jeśli wczytać się w treść funkcji i klas, które są w nim wykorzystywane widzimy interesujące rzeczy. Czy jeśli chciałbym skrócić ten payload (przecież ten z początku artykułu jest strasznie długi!) do czegoś takiego:

%{(#_='multipart/form-data').(#[email protected]@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 znajduję 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:

(#[email protected]@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 dotarłeś tutaj i jesteś zainteresowany jeszcze bardziej szczegółową analizą tego przypadku, zachęcam do zapoznania się z materiałami dostępnymi pod linkiem [1]– duża część tego artykułu opiera się na analizie wykonanej przez AON Cyber Labs.


Referencje

[1] https://www.aon.com/cyber-solutions/aon_cyber_labs/an-analysis-of-cve-2017-5638/
[2]https://avatao.com/blog-deep-dive-into-the-equifax-breach-and-the-apache-struts-vulnerability/
[3] https://nvd.nist.gov/vuln/detail/cve-2017-5638

Tags: