Z okazji Dnia Admina przygotowaliśmy konkurs CTF, w którym uczestnicy mieli za zadanie zidentyfikować i wykorzystać celowo wprowadzone błędy oraz słabości w procedurach wdrażania serwera. Poniższy artykuł szczegółowo opisuje rozwiązania poszczególnych zadań, pokazując, w jaki sposób można było przełamać zabezpieczenia i zdobyć flagi.
Zadanie 1
W pierwszym zadaniu flaga chroniona była przez prosty skrypt PHP weryfikujący adres IP klienta. Skrypt wyświetlał flagę, jeżeli klient połączył się z adresu z sieci 10.20.25.0/24. Była to sieć przypisana do interface’u WireGuard.
Sama konfiguracja interfejsu WireGuard odbywała się z kolei dzięki pomocniczego skryptu napisanego w języku Python. Skrypt ten generował konfigurację dla kilkudziesięciu klientów. Podczas generowania konfiguracji generowane były pary kluczy, jednak klucze prywatne nie były nigdzie udostępniane.
Po bliższej analizie skryptu generującego klucza można było zauważyć, iż źródłem danych losowych – niezbędnych w kryptografii – był moduł random, oparty na algorytmie generatora Mersenne Twister, który nie nadaje się do zastosowań kryptograficznych. Dla ułatwienia zadania, generator był dodatkowo inicjowany wartością całkowitą, odczytaną z zegara systemowego:

Informacje te wystarczyły do napisania krótkiego skryptu, który w pętli odnajdywał seed użyty podczas generacji kluczy, co pozwalało na odtworzenie tych samych kluczy prywatnych i połączenie się z usługą WireGuard i pobranie flagi.

Zadanie 2
W tym zadaniu flagę należało pobrać dzięki protokołu SSH. Konto ze skryptem logowania chronione było kluczem RSA, który – podobnie jak w zadaniu pierwszym – generowany był autorskim skryptem. Tym razem nie popełniono jednak tego samego błędu i użyto bezpiecznego generatora liczb losowych z modułu secrets.

Błąd generowania klucza występował jednak w innej części kodu. Zasada działania algorytmu RSA opiera się na wygenerowaniu dwóch dużych liczb pierwszych p i q. Znajomość tych dwóch liczb pozwala wyliczyć klucz prywatny, natomiast ich iloczyn n = p * q jest elementem klucza publicznego. Bezpieczeństwo algorytmu RSA opiera się na założeniu, iż znając liczbę n, nie da się odwrócić operacji mnożenia i wyznaczać liczb p i q, które dały taki wynik. Tymczasem skrypt generujący klucz zawierał w sobie błąd logiczny.

Funkcja generująca dwie liczby pierwsze znajdowała dwie liczby znajdujące się bardzo blisko siebie. Oznaczało to, iż pierwiastek kwadratowy z publicznie znanej liczby n był bardzo bliski zarówno liczbie p, jak i q, a więc znalezienie ich było trywialnie proste.

Po znalezieniu liczb p i q wystarczało wygenerować klucz prywatny i zalogować się na konto ctf przez SSH.
Zadanie 3
W zadaniu trzecim flaga chroniona była przez skrypt PHP, który umożliwiał dostęp do flagi tylko ze wskazanych adresów IP. Adresy te, poza adresem 127.0.0.1, były adresami publicznych serwerów DNS i – w realiach tego zadania – raczej niemożliwe było podszycie się pod nie.

Adresy IP weryfikowane były dzięki kilku funkcji pomocniczych, które od samego początku mogły wyglądać na podejrzane. Po pierwsze – adresy nie były porównywanego bezpośrednio jako stringi, ale hashowane funkcją SHA-2 w wariancie 512-bitowym. Pomimo iż w PHP string jest typem wbudowanym i można go porównywać dzięki operatora ==, porównanie hashy odbywało w kolejnej funkcji, która porównywała podane wartości w pętli – znak po znaku.

Bliższe przyjrzenie się kodowi PHP weryfikującemu adres ujawniało pewną literówkę, która istotnie zmieniała sposób działania w pętli. Otóż w kroku sprawdzającym długość stringa, zamiast zmiennej $hash1 użyto stałej hash1 (brak symbolu dolara). Stała ta nie była nigdzie zdefiniowana, co zasadniczo powinno prowadzić do błędu, ale w języku PHP w wersjach starszych niż 8, każda niezdefiniowana stała automatycznie przyjmowała wartość tekstową odpowiadająca nazwie stałej. Oznaczało to, iż pętla porównywała tylko 5 pierwszych znaków hasha, a więc – dysponując odpowiednio dużą pulą adresów IP – mogliśmy znaleźć kolizję z jednym z adresów z białej listy.
W tym momencie należało wrócić do pierwszego zadania, w którym każde z połączeń Wireguard miało zdefiniowaną bardzo dużą pulę adresów IPv6, których można było użyć jako adres źródłowy.

Po znalezieniu odpowiedniej kolizji, wystarczyło skonfigurować dodatkowy adres źródłowy w połączeniu z zadania 1 i pobrać flagę.
Alternatywne rozwiązania
W zadaniu drugim zostawiliśmy pewną furtkę, którą można było wykorzystać do rozwiązania zadania pierwszego i trzeciego.
Serwer SSH pozwalał na przekierowywanie portów, dzięki czemu flagi z zadania pierwszego i trzeciego można było też pobrać wykorzystując adresy źródłowe samego serwera (10.20.25.1, 127.0.0.1)
Podsumowując, przygotowany CTF pokazał, iż choćby pozornie proste błędy w kodzie, takie jak użycie niewłaściwego generatora liczb losowych czy literówki w pętli porównującej hashe, mogą prowadzić do poważnych luk bezpieczeństwa. Zadania wymagały od uczestników nie tylko dogłębnej analizy dostarczonego kodu, ale także kreatywnego myślenia i łączenia informacji z różnych części zadania. Przykładem jest konieczność powrotu do zadania 1 w celu rozwiązania zadania 3. Dodatkowo, pozostawienie alternatywnych ścieżek, takich jak przekierowanie portów przez SSH, pokazało, iż do celu często prowadzi więcej niż jedna droga. Te ćwiczenia stanowią doskonałą lekcję na temat tego, jak istotna jest dbałość o szczegóły w kodzie, szczególnie w kontekście bezpieczeństwa.

Nadchodzące eventy
17
09
Wrocław
SysOps/DevOps Wrocław MeetUp #23
18
09
Poznań
SysOps/DevOps Poznań MeetUp #24
31
12
cała Polska