Chyba najwyższa pora napisać trochę więcej na temat wielu ścieżek wykonania. Pierwszy przykład (http://bootcamp.threats.pl/lesson18/) kilka osób prawidłowo rozwiązało, w przypadku drugiego (http://bootcamp.threats.pl/lesson18a/) nikt się prawidłowym rozwiązaniem nie pochwalił, nawet mimo tego, że udostępniłem fragment kodu. Pewnie nie chodzi o to, że przykłady są wyjątkowo trudne, raczej nie są specjalnie ciekawe. Mimo wszystko ten umiarkowanie ciekawy temat doprowadzę do końca.
Bootcamp XVIII i XVIIIa: tak to działa
Stosowne fragmenty kodu
Na początku dwa fragmenty kodu z przykładów dostępnych pod adresami http://bootcamp.threats.pl/lesson18/ oraz http://bootcamp.threats.pl/lesson18a/.
Fragment kodu z przykładu XVIII
if ($b_id) { $query = 'SELECT * FROM news WHERE id=:id'; $params['id'] = $id; } else if ($b_text && $b_type){ $query = 'SELECT * FROM news WHERE title LIKE :text AND public=\''.$type.'\''; $params['text'] = $text; } else if ($b_type) { $query = 'SELECT * FROM news WHERE public=:type'; $params['type'] = $type; }
Fragment kodu z przykładu XVIIIa
if ($b_id) { $query = 'SELECT * FROM news WHERE id=:id'; $params['id'] = $id; } else if ($b_text && $b_type){ $query = 'SELECT * FROM news WHERE title LIKE :text AND public=:type'; $params['text'] = $text; $params['type'] = $type; } else if ($b_type) { $query = 'SELECT * FROM news WHERE public=:type'; $params['type'] = $type; } else if ($b_text) { $query = 'SELECT * FROM news WHERE title LIKE \''.$text.'\''; }
Gdzie jest problem
Znalezienie potencjalnie podatnego fragmentu kodu nie jest trudna. W obu przypadkach problemem jest budowanie zapytania SQL przy pomocy sklejania stringów:
$query = 'SELECT * FROM news WHERE title LIKE :text AND public=\''.$type.'\''; $query = 'SELECT * FROM news WHERE title LIKE \''.$text.'\'';
We fragmentach kodu nie jest pokazane co dzieje się z przychodzącymi od użytkownika danymi, czy nie są one w jakiś sposób oczyszczane bądź kodowane (przy okazji warto przeczytać: mysql_real_escape_string() versus Prepared Statements). Samo wykorzystanie sklejania stringów do budowania zapytania SQL nie jest podatnością, sygnalizuje jedynie, że z dużym prawdopodobieństwem podatność taka może wystąpić.
Tu pozwolę sobie na małą dygresję. Kiedyś miałem okazję przeglądać kod źródłowy pewnej aplikacji. Dostęp do bazy danych był realizowany co do zasady poprzez prepared statements, ale ponieważ wykonawca miał pewne problemy ze zmieszczeniem się w uzgodnionym czasie, niektóre poprawki i rozszerzenia były wykonywane "na skróty". Czyli tak, jak widać to w pierwszym zapytaniu, do którego jeden parametr przekazywany jest prawidłowo, drugi natomiast jest wstawiany poprzez sklejanie stringów. I oczywiście w tym właśnie parametrze był SQLi. Zresztą temat podatności wprowadzanych "w ostatniej chwili" można poprzeć większą ilością przykładów.
Jak wykonać podatne zapytanie
W przypadku obu fragmentów kodu zapytań SQL jest kilka, ale tylko jedno z nich jest podatne na injection. Jak wykonać właśnie to zapytanie? Trzeba po prostu wejść do właściwego elementu konstrukcji if/else, co jest uzależnione od parametrów typu $b_text, $b_type, (...). Tu z kolei pomocny może być ten fragment kodu:
if (isset($_POST['id']) && $_POST['id'] <> '') { $id = (int) $_POST['id']; $b_id = true; }
Na podstawie powyższego fragmentu można przypuszczać, że wartość parametru typu $b_ zależy od tego, czy powiązany z nim parametr (np. text i $b_text) znajduje się w przesłanym żądaniu. Tak, zachowanie aplikacji niejednokrotnie zależy nie tylko od tego, jaką wartość ma parametr, ale też od tego czy został on przesłany do serwera.
Przykład pierwszy
Pierwsze podatne zapytanie wykona się wtedy, gdy ustawione będą $b_text oraz $b_type, natomiast $b_id będzie miało wartość false. Akurat w przypadku parametru type nie zostało zastosowane rzutowanie, w związku z czym możliwe jest przekazanie do zapytania dowolnej wartości. Przyznam, że specjalnie wybrałem właśnie parametr type, który jest powiązany z polem typu dropdown, bo programiści często zapominają, że wartości takich kontrolek również mogą być dowolnie modyfikowane, patrz: Lekcja 1: absolutne podstawy.
Przykład drugi
W drugim przykładzie chciałem zasymulować legacy-code. Ten fragment kodu nigdy nie wykona się, jeśli użytkownicy będą korzystać z formatki do wygenerowania żądania. Po prostu być może kiedyś był używany, później na formatce wprowadzona została możliwość określenia typu wiadomości, który ustawiony jest zawsze, różni się co najwyżej wartością. By dostać się do ścieżki kodu zawierającej podatny kod trzeba usunąć z wysyłanego żądania parametr type. No i oczywiście nie można ustawiać parametru id. Najłatwiej sprawdzić to obserwując rezultaty wyszukiwania tekstu Wiado'||'mość z oraz bez parametru type.
Co mówią te przykłady
Użyję tu magicznego słowa: pokrycie. Chcę po prostu pokazać, że w trakcie testów black-box ciężko jest osiągnąć pełne pokrycie testami. Nawet testując każdą funkcję aplikacji parametr po parametrze nie ma gwarancji, że sprawdzona zostanie każda możliwa ścieżka wykonania kodu. A jak pokazują oba przykłady, parametr bezpieczny w jednej ścieżce, może sprawiać problemy w innej...
Warto zastanowić się również jak w takich przypadkach sprawdzają się narzędzia automatyczne (fuzzery, skanery). Mają niewątpliwą przewagę nad człowiekiem, mogą sprawdzić zdecydowanie więcej możliwości, niż człowiek. Tylko czy strzelanie w ciemno daje lepsze efekty niż chwila zastanowienia człowieka? Z drugiej strony takie strzelanie w ciemno też jest potrzebne...