Małe uzupełnienie do I co z tego, że jest SQLi w Płatniku? Napisałem, że błędy są już w architekturze programu, więc błędy w implementacji nie robią już tak dużego wrażenia. Warto przyjrzeć się temu tematowi, bo sposób działania programu Płatnik jest bardzo typowy, a co za tym idzie problemy z niego wynikające są bardzo powszechne. Należy pamiętać, że poruszane tu problemy są charakterystyczne dla całej klasy aplikacji.
Co jest nie tak z taką architekturą (Płatnik or compatible)
Ogólny schemat działania typowej aplikacji jest następujący:
- aplikacja uruchamia się i łączy z bazą danych,
- użytkownik loguje się do aplikacji,
- użytkownik działa w aplikacji w ramach przyznanych uprawnień,
Mówiąc o typowej aplikacji chodzi mi oczywiście o przypadek "grubego klienta", który korzysta z danych przechowywanych w lokalnej lub zdalnej bazie danych.
Już pierwszy punkt powinien wzbudzić czujność czytelnika. W jaki sposób aplikacja łączy się z bazą danych? W szczególności wtedy, gdy baza danych jest obsługiwana przez serwer typu MS SQL? Tak, aplikacja do połączenia potrzebuje nazwy użytkownika i jego hasła. Czasami nie potrzebuje, jeśli wykorzystane jest zintegrowane uwierzytelnianie Windows, ale taka konfiguracja nie rozwiązuje problemu.
Skoro wymagane są dane uwierzytelniające, trzeba je jakoś dostarczyć aplikacji. Użytkownik nie wpisuje tych danych, są one zapisane w rejestrze. Hasło co prawda jest obfuskowane, ale siłą rzeczy algorytm jest odwracalny, więc możliwe jest uzyskanie jego jawnej formy. Jak to zrobić? Pierwszy link w Google: http://platnik.fork.pl/. Nie jest to żadna wiedza tajemna, możliwość odzyskania zapomnianego hasła do bazy lub zapomnianego hasła administratora zapewne niejednokrotnie oszczędziła wielu nerwów. Przy okazji przypomnę mój stary wpis: Szyfrowanie czy "zaciemnianie" czyli hasła w konfiguracji, który tematu "szyfrowania" haseł w konfiguracji różnych programów dotyczy.
No dobrze, ale przecież użytkownik loguje się do aplikacji! Tak, loguje się, ale do aplikacji właśnie. Uwierzytelnienie jest realizowane przez aplikację, a nie przez serwer baz danych. W tym przypadku nie ważne, czy do Płatnika zaloguje się ktoś jako user1 czy user99, w kontekście bazy danych każdy użytkownik będzie działał z tymi samymi uprawnieniami (z dokładnością do przypadku wykorzystania uwierzytelniania zintegrowanego gdzie dodatkowo użytkownicy user1 i user99 korzystają z różnych kont w systemie Windows, ale znów - nie jest to istotne).
Jeśli się nie mylę, to w Płatniku można jedynie przypisać użytkownikowi uprawnienia do obsługi danego płatnika. Powiązania te przechowywane są w tabeli UPRAWNIENIA (tak, to ta tabela z przykładowego exploita), a konkretnie przechowywany jest tam identyfikator użytkownika oraz identyfikator płatnika. Jeśli istnieje stosowna para, dany użytkownik ma prawo działać na określonym płatniku. Użytkownik aplikacji może działać na określonym płatniku, którego kontekst wybierze, a wybrać może (teoretycznie) tylko kontekst płatnika, do którego ma prawa dostępu nadane przez administratora.
Warto udać się na stronę programu Płatnik i zapoznać się z dokumentem opisującym struktury danych. Nie jest tak, że dla różnych płatników zakładane są różne tabele. Dane wszystkich płatników przechowywane są w jednej tabeli. To pewne uproszczenie, bo dane w rzeczywistości rozproszone są między wiele tabel, jednak w każdej z tych tabel znajdują się dane wszystkich płatników i tak na przykład tabela PLATN_IDENT zawiera dane wszystkich płatników, a tabela UBEZP_IDENT dane wszystkich ubezpieczonych.
Z samej konstrukcji bazy danych wynika, że nie ma możliwości przypisania uprawnień do danych dotyczących konkretnego płatnika czy ubezpieczonego konkretnemu użytkownikowi bazy danych. Uprawnienia w bazach danych nakładane są na poziomie tabel lub kolumn, a nie rekordów (tak, wiem - istnieją implementacje, które dają taką możliwość, patrz Virtual Private Database).
Dla bazy danych zapytania wysyłane przez aplikację są praktycznie takie same, różnią się tylko identyfikatorem płatnika, który jest przekazywany w sekcji WHERE zapytania (a przynajmniej tak jest w części przypadków). Dlatego też działa zapytanie pokazujące dane wszystkich płatników (patrz: SQL injection w Płatniku, konkretnie to zapytanie z OR 1=1 --). Przy okazji ten przykład powinien rozwiać wszelkie wątpliwości czy rzeczywiście każdy użytkownik ma dostęp (na poziomie bazy danych) do danych wszystkich płatników, jeśli opis bazy danych jest dla kogoś niewystarczający i woli test empiryczny. Do danych wszystkich płatników musi mieć również dostęp użytkownik bazy danych, który jest wykorzystywany przez aplikację do połączenia z serwerem.
Czy opisany sposób działania wydaje się znajomy? Tak, właśnie tak działa znaczna część aplikacji internetowych. W ich przypadku jednak nie mówi się o fundamentalnych błędach w architekturze, dlaczego? Z prostego powodu - w typowym przypadku użytkownik aplikacji webowej nie może nawiązać bezpośredniego połączenia z serwerem baz danych (pomijam przypadki patologiczne, w których może). Co więcej - ponieważ aplikacja uruchomiona jest na serwerze, do którego użytkownik nie ma bezpośredniego dostępu, nie może on analizować aplikacji tak, jak może to zrobić na swoim własnym komputerze z aplikacją zainstalowaną lokalnie. Dlatego właśnie taka architektura w przypadku aplikacji webowych się sprawdza i jest akceptowalna. Oczywiście, pewne jej mankamenty są widoczne, choć w nieco inny sposób. Przykładem tych mankamentów mogą być skutki SQLi i możliwość uzyskania dostępu do cudzych danych, ich modyfikacji lub usunięcia. Gdyby dane poszczególnych użytkowników były od siebie odseparowane na poziomie bazy danych i poszczególni użytkownicy logowali się na "swoje" konta, skutki SQLi byłyby ograniczone. Z drugiej strony trzeba przyznać, że takie rozwiązanie miałoby również swoje wady, na przykład związane z jego złożonością czy wydajnością.
Inaczej sytuacja przedstawia się w przypadku "grubego klienta". Skoro takie połączenie może nawiązać aplikacja A uruchomiona na określonej stacji, to połączenie takie powinna również nawiązać aplikacja B. Potrzebne są jedynie dane serwera oraz dane uwierzytelniające, a te informacje zapisane są w rejestrze, w konfiguracji aplikacji A, w której roli występuje gościnnie program Płatnik. Innymi słowy - aplikacja Płatnik nie jest niezbędna do korzystania/przeglądania/modyfikowania/usuwania danych, wystarczy dowolne narzędzie pozwalające na dostęp do wykorzystywanej bazy danych. Atakujący nie musi obchodzić mechanizmów wbudowanych w aplikację lub wykorzystywać istniejących w aplikacji podatności, może po prostu użyć innej aplikacji (przepraszam, nie mogę się powstrzymać: Obchodzenie czytnika linii papilarnych). O podobnych błędach w architekturze, choć nieco w innym kontekście, pisałem już we wpisie Niespodzianka - wcale nie muszę korzystać z (waszego) klienta.
Dla równowagi na cały problem z Płatnikiem należy spojrzeć też z drugiej strony. Program ten jest wykorzystywany powszechnie (no nie da się ukryć, że z przymusu). Stworzenie bezpiecznego rozwiązania, które jednocześnie będzie używalne (nie chodzi mi tu o usability tylko raczej o: z niewielką pomocą nawet pani Jola sobie poradzi), jest... problematyczne.
Sie z kolegą nie zgodzę na początek co do rozwiązania jednego problemu jakim jest zapisane hasło usługi \ użytkownika. Jeżeli jest to faktycznie uwierzytelnienie zintegrowane i poprawnie zaimplementowane to problem rozwiązuje, ponieważ w konfiguracji nigdzie nie powinno być loginu czy hasła a kontekst połączenia się aplikacji powinien wynikać z kontekstu logowania użytkownika. Ale pewnie czegoś nie rozumiem ... jak to bywa ze mną i tym bezpieczeństwem całym. To czytam dalej ...
Czy coś w tu zmienia uwierzytelnienie zintegrowane? Nie. Skoro użytkownik ma dostęp, to oznacza, że jego konto w systemie ma dostęp do bazy danych. Dostęp ma użytkownik, a nie konkretny program, więc zamiast przykładowego Płatnika może uruchomić dowolne narzędzie do zarządzania bazą danych, które korzystając z uwierzytelnienia zintegrowanego również z bazą się połączy. Efekt dokładnie taki sam jak w przypadku odzyskania hasła, tylko, że hasła nie trzeba odzyskiwać (bo jest znane atakującemu).
Rzeczywiście nieco inaczej wygląda sytuacja w przypadku, gdy "atak" przeprowadza użytkownik, który do aplikacji dostępu nie posiada. Wówczas uwierzytelnienie zintegrowane ma tę zaletę, że hasło MOŻE nie być przechowywane w systemie (bo jeśli nie ma domeny, to jest lokalna baza SAM). Nawet jeśli atakujący uzyska dostęp do bazy SAM, to hasło musi jeszcze złamać, co nie zawsze jest proste.
Podsumowując - tak, uwierzytelnienie zintegrowane rozwiązuje problem z hasłami, które trzeba zapisać jakoś w konfiguracji. Nie rozwiązuje jednak problemu z architekturą.