Zgodnie z przypuszczeniami temat "global-id encoding" nie zrobił kariery takiej, jak wpisy o łamaniu haseł. Jednak ponieważ istnieje co najmniej jedna osoba zainteresowana tematem - kilka wyjaśnień. Mam nadzieję, że Kravietz nie obrazi się za poruszenie tu dość elementarnych kwesti :)
Jeszcze o identyfikatorach globalnych i ich kodowaniu
Pierwsza część pytania z komentarza:
Jeśli dobrze zrozumiałem to problem wynika z potrzeby zastosowania algorytmu mapowania identyfikatorów stron, które pokazujemy userowi na identyfikatory wewnętrzne.Sprawa jest trochę bardziej złożona. Tak naprawdę źródłem problemu jest złożenie trzech elementów:
- wykorzystywanie jako kluczy w bazie danych sekwencyjnych wartości liczbowych,
- wykorzystanie tych identyfikatorów w warstwie prezentacji,
- typowe usterki w kontroli dostępu,
Wykorzystanie sekwencyjnych wartości liczbowych jako kluczy (identyfikatorów) rekordów w bazie danych nie jest problemem samym w sobie. Podejrzewam, że osoby zajmujące się bazami danych, w szczególności kwestiami ich wydajności, mogłyby tu przedstawić sporo ciekawych informacji. Problemem jednak jest używanie tych identyfikatorów bezpośrednio na warstwie prezentacji, czyli na generowanej przez aplikację stronie.
Prosty przykład, niech będzie tabela z danymi dotyczącymi użytkowników pewnego serwisu. Istnieje duże prawdopodobieństwo, że w tabeli takiej znajdzie się sekwencyjny klucz, nazwa użytkownika i pozostałe jego dane. Zgodnie z praktykami normalizacji bazy danych niektóre dane mogą być wydzielone do oddzielnych tabel i w tabeli "głównej" na przykład zamiast pola "Miasto" znajdzie się identyfikator tylko identyfikator stosownego rekordu z tabeli "Miasta". Teraz wyobraźmy sobie funkcję edycji użytkowników. Użytkownik należący do określonej roli (logika biznesowa) otrzymuje listę użytkowników, których dane może edytować. Wybranie określonego użytkownika powoduje wysłanie żądania typu /editUser?id=1211, co w praktyce spowoduje załadowanie do formatki danych użytkownika o identyfikatorze 1211 w bazie danych, a następnie (po dokonaniu aktualizacji w formatce), aktualizację stosownego rekordu. W najbardziej trywialnym przypadku będzie to jakiś UPDATE ... WHERE id=1211. Modyfikując parametr przekazany w parametrze id można załadować dane innego użytkownika. Znalezienie innej poprawnej wartości identyfikatora w tego typu przypadku jest w zasadzie trywialne, można spróbować wartości 1210 czy 1212.
W tym momencie do głosu dochodzą typowe usterki w kontroli dostępu. Nie wiadomo dlaczego, ale część twórców aplikacji internetowych jest przekonana, że klient nie może przysłać innego żądania, niż to, które jest w stanie wygenerować w trakcie interakcji ze stroną. Czyli jeśli na wygenerowanej liście użytkowników są użytkownicy o identyfikatorach 1012, 1211, 1341 to niemożliwe jest przesłanie identyfikatora o wartości 1256. Jest to oczywisty błąd w założeniach, ponieważ taka modyfikacja może być trywialnie wykonana, choćby za pomocą jakiegoś local proxy, o prostej modyfikacji parametrów, które przekazywane są w GET, bezpośrednio w pasku adresu nie wspominając. Bazując jednak na tym założeniu, częstą praktyką jest umieszczenie "kontroli dostępu" wyłącznie w funkcji generującej listę użytkowników, natomiast wspomniane wywołanie /editUser akceptuje dowolną wartość identyfikatora id, bez jakiegokolwiek sprawdzenia, czy zalogowany użytkownik ma prawo edytować rekord o wskazanym identyfikatorze. Trzeba tu zwrócić uwagę, że niektóre frameworki dodają warstwę, która pozwala na sprawdzenie, czy wygenerowane zdarzenie było możliwe do wygenerowania na wyświetlonej stronie, ale to już jest temat oddzielny.
Istnieje grupa aplikacji, w których identyfikatory globalne wykorzystywane są "standardowo". Możliwa jest prawidłowa implementacja kontroli dostępu mimo wykorzystania identyfikatorów globalnych, prawdopodobieństwo błędu (jakiegoś przeoczenia) jednak istnieje i jest ono dość duże. Skuteczną metodą obrony jest wprowadzenie jakiejś warstwy pośredniej, która tłumaczyłaby identyfikatory globalne, na identyfikatory lokalne. W rozważanym przykładzie wywołanie funkcji edycji użytkowników mogłoby wyglądać następująco: /editUser?index=2, gdzie wartość parametru index jest na przykład indeksem wybranego rekordu na liście. Po otrzymaniu takiego żądania aplikacja sprawdza jaki rekord został wyświetlony na pozycji o indeksie 2 i ładuje stosowne dane. Swój PoC stworzyłem w zasadzie tylko po to, by zobaczyć, jaka jest możliwość (stosunkowo) prostego zastąpienia identyfikatorów globalnych "czymś innym". Postanowiłem spróbować "kodować" identyfikator globalny w taki sposób, by na jego tylko podstawie można było zweryfikować, czy mógł on być on rzeczywiście wyświetlony na stronie (czy otrzymane żądanie jest rezultatem "dozwolonej" akcji użytkownika, czy wynikiem jakiegoś tamperingu). Nie chodziło mi przy tym o całkowite uniemożliwienie tamperingu, tylko o jego poważne utrudnienie. Nadal możliwe jest "trafienie" z zakodowaną wartością identyfikatora, która po rozkodowaniu wskaże prawidłowy (pożądany przez atakującego) identyfikator globalny ORAZ przejdzie testy "integralności".
Dokładniejszy opis działania wraz z kodem dostępny jest tutaj. Teoretycznie proponowane przeze mnie rozwiązanie może być umieszczone całkiem poza aplikacją, jako dodatkowy moduł na serwerze HTTP przetwarzający output/input.
W każdym razie (wracając do pytania) chodzi nie tyle o samo "zamaskowanie" identyfikatora globalnego (np. rzeczywistego identyfikatora z bazy danych), ale również o powiązanie tych wartości z konkretną sesją, tak, by identyfikatory prawidłowe w jednej sesji (a podpatrzone w jakiś sposób przez intruza) nie mogły zostać wykorzystane w innej sesji (innego, lub nawet tego samego użytkownika). Dzięki temu można założyć, że jeśli aplikacja otrzymuje od użytkownika identyfikator i jest on poprawny, oznacza to (z dużym prawdopodobieństwem), że został on wcześniej "wygenerowany" przez aplikację. Nie można jednak założyć, że identyfikator został użyty w kontekście, w jakim został wygenerowany (o tym później).
Druga część pytania (albo raczej pytanie właściwe):
Czy nie można tego zrobić za pomocą statycznego mapowania?Jeśli chcemy "zamaskować" błędy kontroli dostępu, to statyczne mapowanie nie do końca jest skuteczne. W zasadzie nie będzie różnicy, czy przekaże się identyfikator globalny, czy identyfikator "statyczny" mapujący się na określony identyfikator globalny. Jeśli jednak identyfikator "statyczny" będzie długi i trudny do przewidzenia, wówczas rzeczywiście takie podejście może spełniać swoją rolę wystarczająco dobrze, ale tylko do czasu. Problem pojawi się wówczas, gdy atakujący w jakiś sposób będzie w stanie podpatrzeć/odgadnąć prawidłowe wartości identyfikatora "statycznego". Dlatego też w moim PoC wartości tego samego zakodowanego identyfikatora w dwóch różnych sesjach jest różna, nawet w obrębie jednej sesji przyjmuje 256 wartości.
Przy okazji warto zauważyć, że również mój PoC jest podatny na odgadnięcie/przewidzenie prawidłowej wartości identyfikatora. Wystarczy wyobrazić sobie, że w innym miejscu aplikacji można listować wszystkich użytkowników i dostępna jest akcja przeglądania ich szczegółów, która w parametrze przyjmuje identyfikator. Aplikacja na stronie umieści co prawda identyfikator w postaci zakodowanej, ale może on zostać z powodzeniem użyty w innym miejscu aplikacji, jako parametr funkcji edycji użytkowników. Po otrzymaniu prawidłowego identyfikatora, aplikacja nie jest w stanie stwierdzić, czy został on użyty "zgodnie z przeznaczeniem", czy został przeklejony do innego żądania. Oczywiście można przeciwdziałać tego typu atakowi w ten sposób, że klucze wykorzystywane w procesie kodowania/dekodowania identyfikatorów będą unikalne nie dla sesji, ale dla sesji i strony, ale to nie o to chodzi. Cel jest taki, by utrudnić wykorzystanie błędu dotyczącego kontroli dostępu, a nie zastąpić kontrolę dostępu. Patrząc w ten sposób, rozwiązanie ze "statycznym" mapowaniem identyfikatorów również utrudnia wykorzystanie błędów kontroli dostępu, pod warunkiem, że wartość identyfikatora statycznego jest trudna do odgadnięcia. Wydaje mi się jednak, że przedstawiony przeze mnie PoC w niektórych przypadkach stawia poprzeczkę nieco wyżej.
Przykład "wyższej" poprzeczkiKiedy rozwiązanie zaproponowane w moim PoC może okazać się skuteczniejsze? Jednym z ataków, które są (albo przynajmniej były) ostatnio "na topie" jest CSRF. Warunkiem powodzenia takiego ataku jest możliwość określenia jaki request powinien zostać wysłany przez klienta aplikacji w celu wykonania określonej akcji. Jednym z elementarnych mechanizmów obronnych jest dodawanie różnego rodzaju tokenów, które są losowe, przez co wygenerowanie odpowiedniego żądania staje się trudniejsze (ale nie niemożliwe, zwłaszcza jeśli połączy się atak typu CSRF ze "zwykłym" XSS). W przypadku ukrywania identyfikatorów globalnych za identyfikatorem "statycznym", nie ma wartości losowej, chyba, że została świadomie dodatkowo wprowadzona. W przypadku wykorzystania mojego PoC, wartość identyfikatora może jednocześnie (poniekąd: przy okazji) pełnić rolę tokenu. Nawet sprawdzając wartość identyfikatora (lub nawet wszystkie 256 wartości) w jednej sesji nic nie daje, bo ten sam identyfikator w innej sesji będzie wyglądał inaczej (przypominam, założenie jest takie, że klucze wykorzystywane w put/get są losowe i unikalne dla każdej sesji). To, czy jest metoda pozwalająca na ustalenie prawidłowej wartości identyfikatora w sposób inny niż brute force jest kwestią otwartą. Wciąż czekam na stosowny przykład (choć podejrzewam, że nikt tego nie próbował "łamać").
Udało mi się coś wyjaśnić, czy jeszcze bardziej zagmatwałem sprawę?
To drugie Tak na poważnie to taka dyskusja sama się gmatwa jeśli dyskutujemy na wysokim poziomie abstrakcji. Wychodzi z tego taki strumień świadomości "gdyby framework X miał funkcję Y to można by ją nazwać C, aczkolwiek w przypadku generalizacji..." itd.
Spróbujemy to zrobić na czymś konkretnym. Weźmy na ten przykład Dżango. Dżango posiada zarządzanie sesją (cookie) oraz ochronę przed CSRF (token w hidden).
No i teraz mamy np. bloga. W blogu parametrem jest numer wiadomości przechowywanej w SQL. User wskazuje (jakoś) numer wiadomości, którą chce zobaczyć.
Następnie przekazuje kolejne parametry w postaci treści swego komentarza (co i ja obecnie czynię
Pierwszy etap jest zrobiony w Dżango za pomocą publicznego identyfikatora wiadomości - np. "/view/1234". W Dżango jest to zrobione na wysokim poziomie abstrakcji i AFAIR programista może sobie stworzyć pole "id", ale ono nie musi być primary kejem.
Tutaj mi właśnie przychodzi do głowy, że taki statycznie istniejący identyfikator jest niezbędny np. po to żeby istniały permalinki...
W tym konkretnym przypadku jest dokumentacja/przykład (i to nawet z permalinkiem): http://docs.djangoproject.com/en/dev/ref/models/instances/
Czyli permalink jest wyliczany przez funkcję get_absolute_url.
Przy czym akurat do permalinków mój PoC się kompletnie nie nadaje, link byłby mało stały