17-12-2019 / Od 0 do pentestera

Skutki błędów SQL Injection - wykonanie kodu

Jedną z ważniejszych zasad w branży bezpieczeństwa jest zasada najmniejszego uprzywilejowania.

Chodzi w niej o to aby każdy z elementów systemu informatycznego miał dostęp tylko do tych zasobów, które są mu niezbędne do pracy.

Dzisiaj pokaże dlaczego jest to takie istotne na przykładzie bazy danych.

A dokładnie opowiem o tym dlaczego baza MariaDB nie powinna działać z uprawnieniami roota albo administratora jeżeli nie jest to konieczne.

 MySQL jako root

Zademonstruje atak typu privilege escalation - czyli podniesienia uprawnień.

Mamy z nim do czynienia wtedy, kiedy z poziomu nieuprzywilejowanego konta użytkownika jesteśmy w stanie wykonać funkcję dostępne jedynie dla administratora.

Chociażby stworzyć nowe konto administratora na danym komputerze.

Posiadając takie uprawnienia atakujący ma praktycznie nieograniczoną władzę nad daną maszyną - a to jest bardzo niebezpieczne.

W dzisiejszym odcinku zakładam, że atakujący posiada nieuprawniony dostęp do naszej bazy danych.

W większości scenariuszy będzie to zatem wynikało ze znalezienia błędu typu SQL Injection, który pozwala na wykonanie dowolnego zapytania do naszej bazy.

Tutaj w celu uproszczenia komendy będę wykonywał przy pomocy narzędzia phpMyAdmin1, które służy do zarządzania bazą danych.

Ale pamiętaj, że równie dobrze te samy komendy - odpowiednio zmodyfikowane - możesz przesłać przy pomocy błędu typu SQL Injection, który pozwala na wstrzyknięcie dodatkowych danych do zapytania przygotowanego wcześniej przez programistę.

 Wersja bazy danych

Jeżeli mamy dostęp do bazy danych warto upewnić się co to za rodzaj bazy danych, jaka konkretnie jest to wersja oraz pod jaką wersję systemu operacyjnego została skompilowana.

SHOW GLOBAL VARIABLES LIKE '%version%';
 MariaDB pod Windowsem

Tutaj widzimy, że korzystam z bazy MariaDB pod Windowsem i skompilowana jest ona pod systemy 32 bitowe.

No dobrze ale po co nam ta informacja?

MariaDB oprócz pobierania danych z tabel ma dodatkową funkcjonalność służącą do zapisywania i ładowania plików z dysku twardego.

Dzięki temu można pobrać jakąś treść z pliku i zapisać ją do stworzonej przez nas tabeli.

Popatrzmy na przykład.

Tworze nową tabele, która posiada jedną kolumnę - typu text.

CREATE TABLE files(content text);
INSERT INTO files VALUES(load_file('c:\\maria\\plik.txt'));
SELECT content FROM files INTO DUMPFILE 'c:\\maria\\nowy_plik.txt';

Ten to typ pozwala nam na przechowywanie danych binarnych.

Jest to istotne - w plikach bowiem mogą znajdować się tak zwane bajty zerowe.

Jest to bajt o kodzie hex 0x00.

Problem w tym, że sporo języków gdy napotyka taki bajt - stwierdza, że użytkownik skończył podawać ciąg tekstowy i kończy go przetwarzać.

Standardowo bowiem - każdy ciąg jest zakończony właśnie takim bajtem.

To mogło by doprowadzić do tego, że nasz plik nie został by zapisany do tabeli w całości - a tylko do pierwszego wystąpienia bajtu zerowego.

Teraz mogę załadować treść dowolnego pliku, do którego posiada dostęp użytkownik, który uruchomił serwer przy pomocy konstrukcja INSERT INTO z wykorzystaniem funkcji LOAD_FILE.

INSERT INTO files VALUES(load_file('c:\\Users\\root\\Downloads\\lib_mysqludf_sys.dll'));
 Treść pliku w bazie

Jak widzisz treść pliku została umieszczona w danej bazie danych.

SELECT * FROM files

Bardzo podobnie wygląda zapisywanie pliku na dysku twardym.

Tym razem wykorzystujemy składnie INTO DUMPFILE - gdzie podajemy nazwę pliku wynikowego.

Dane tam zapisywane są pobierane z kolumny, którą definiujemy w zapytaniu SELECT.

SELECT content FROM files INTO DUMPFILE 'c:\\xampp\\mysql\\bin\\lib_mysqludf_sys_32.dll';

I rzeczywiście, po wykonaniu tego zapytania na dysku twardym pojawił się nowy plik.

Ale dlaczego o tym mówię?

Wszak mieliśmy dzisiaj mówić o podnoszeniu uprawnień?

Maria ma sporą liczbę wbudowanych funkcji, ale okazuje się - że dla niektórych użytkowników to wciąż za mało.

Dlatego też deweloperzy udostępnili możliwość tworzenia własnych funkcji, które mogą być następnie używane z poziomu składni SQL.

Tutaj są one nazywane UDF czyli User Definied Function2 i tworzy się je w języku C.

 Funkcje UDF

No dobrze ale skąd wziąć takie dodatkowe funkcjonalności?

W Internecie można znaleźć kilka źródeł3, gdzie zewnętrzni twórcy przygotowali i skompilowali już potrzebne pliki.

Nas jednak interesuje jedna, specyficzna opcja, której brakuje w standardowej składnie SQL.

Chcemy bowiem przy pomocy bazy zaatakować serwer - a najlepiej atakuje się, jeżeli możemy wykonać dowolny kod na danej maszynie.

Dlatego też dodamy do naszej bazy funkcję,która będzie odpowiedzialna za wykonywanie komend systemowych przekazanych przez użytkownika w treści zapytania.

Żeby móc używać tych funkcji należy je najpierw zarejestrować.

Najpierw musimy sprawdzić gdzie baza będzie poszukiwała plików dll, w których zaimplementowana jest cała funkcjonalność.

Informacje o katalogu, w którym powinny się znaleźć pliki UDF znajdują się w zmiennej plugin_dir.

SHOW VARIABLES LIKE 'plugin_dir';

Skompilowany plik dostępny jest w repozytorium projektu SQLMap4, który służy do wykonywania ataku SQL Injection.

Uwaga - tutaj wykorzystujemy starsza wersję tego pliku a to dlatego, że nowsze wersje zostały zaszyfrowane - tak aby nie były wykrywane przez programy antywirusowe.

Musimy wybrać odpowiednia wersję pliku - stąd tez na samym początku sprawdzaliśmy czy nasz system jest 32 czy tez 64 bitowy.

Jeżeli wybierzemy zły typ - plik po prostu się nie uruchomi.

No dobrze - mamy już plik z naszą funkcją a także wiemy gdzie powinien się od znaleźć.

Ale jak przesłać ten plik?

Gdybyśmy posiadali dostęp do systemu plików serwera moglibyśmy załadować taki plik przy pomocy poznanej wcześniej składni LOAD_FILE a następnie zapisać go w nowym miejscu - używając INTO OUTFILE.

Załóżmy, jednak że atakujący znalazł tylko błąd - SQL Injection i może jedynie wykonywać zapytania SQL.

Jak zatem przesłać zawartość pliku binarnego do serwera SQL?

Maria posiada funkcje CHAR, która zamienia kod ASCI danego znaku na ten znak.

W ten sposób możemy przesłać do zapytania również te znaki - które nie sa printowalne - chociażby bajt 00, który występuje w plikach binarnych.

Posłużymy się zatem prostym skryptem w języku Python, który otworzy nasz plik binarny i zamieni wszystkie znaki na ich odpowiedniki w kodzie ASCII.

a = open("lib_mysqludf_sys.dll2", "rb").read()
out = "INSERT INTO files VALUES(CHAR("
for b in a:
	out += str(ord(b))+", "
out = out[0:-2]
out += "))"
print out

Dzięki temu mamy przygotowane odpowiednie zapytanie - gdzie każda litera została zamieniona na odpowiedni kod ASCII.

W taki oto sposób możemy umieścić treść pliku w tabeli w bazie danych.

INSERT INTO files VALUES(CHAR(1,2,3,4));
 Zapis pliku do bazy

Teraz możemy przesłać ten plik do katalogu plugins - tak aby baza mogła go załadować.

Tutaj warto zaznaczyć, że ten katalog musi istnieć na dysku.

Jeżeli więc mamy pecha i katalog nie został wcześniej stworzony - atak się nie powiedzie.

SELECT content FROM files INTO DUMPFILE 'C:\\xampp\\mysql\\lib\\plugin\\lib_mysqludf_sys.dll';

Gdy już plik zostanie prawidłowo przesłany musimy jeszcze zarejestrować daną funkcję.

CREATE FUNCTION sys_eval RETURNS string SONAME 'lib_mysqludf_sys.dll';

Tutaj rejestrujemy funkcję sys_eval korzystając z pliku 'lib_mysqludf_sys.dll'.

Definiujemy także co ona zwraca - w naszym wypadku ciąg znaków.

Nasz plik dll posiada bowiem obsługę kilku funkcji - nas interesuje ta konkretna, ponieważ oprócz wykonania komendy zwraca również jej wynik.

 Eksporty w pliku DLL

Nareszcie możemy wykonać dowolna komendę w danym systemie.

Chociażby whoami - aby sprawdzić konto bieżącego użytkownika.

SELECT sys_eval("whoami");

Właśnie udało się nam wykonać dowolny kod na zdalnym serwerze wykorzystując jedynie zapytania SQL.

A jeżeli serwer uruchomiony jest z uprawnieniami administratora - oznacza to, że również kod wykonywa Nany przy użyciu funkcji sys_eval będzie uruchamiał się z tymi uprawnieniami.

To sprawia, że mamy pełny dostęp do serwera.

 Wynik funkcji sys_eval

Możemy dla przykładu stworzyć nowe konto administratora, stworzyć nową bazę danych czy tez usunąć jakiś plik.

A to wszystko z powodu jednego błędu SQL Injection oraz błędnej konfiguracja serwera.

Jak zatem ochronić się przed tym atakiem?

Po pierwsze - warto korzystać z Prepared Statements aby uchronić się przed błędami typu SQL Injection.

Po drugie - nigdy nie uruchamiać bazy z uprawnieniami administratora.

Po trzecie - jeżeli nie jest to konieczne warto stworzyć konto użytkownika, który nie posiada uprawnień typu FILE.

Wtedy to komendy operujące na plikach po prostu się nie powiodą - co zmniejszy wektor ataku potencjalnego intruza.

 Podniesienie uprawnień

Alternatywną opcją jest uruchomienie serwera z dodatkowym parametrem secure_file_priv5.

Określa się w nim do jakich katalogów ma dostęp serwer z poziomu funkcji FILE.

Wtedy to atakujący nie będzie w stanie wykorzystać w/w scenariusza ponieważ nie będzie w stanie zapisać danych do katalogu z rozszerzeniami.