Preorder drugiego tomu książki sekuraka: Wprowadzenie do bezpieczeństwa IT. -15% z kodem: sekurak-book
Boston Key Party CTF 2015, czyli nie taki diabeł straszny
Wstęp
W dniach 27 lutego – 1 marca odbył się jeden z najbardziej znanych turniejów w kalendarzu CTF – Boston Key Party Capture The Flag 2015. Był to jednocześnie najbardziej jak do tej pory „oblężony” CTF, gdyż ogólnie sklasyfikowano 828 (!!!) teamów, które rozwiązały co najmniej jedno zadanie i zdobyły punkty. Można jedynie przypuszczać, że zarejestrowanych było jeszcze więcej zespołów, ale nie wszystkim udało się uporać chociaż z jednym wyzwaniem. W porównaniu z zeszłoroczną edycją, gdzie punktowało jedynie 137 teamów, tegoroczna frekwencja przeszła chyba oczekiwania wszystkich, z organizatorami włącznie.
A skoro mowa o organizatorach – według mnie odwalili kawał solidnej roboty. Infrastruktura serwerowa została zapewniona przez Amazon Web Services, cały czas aktywny był również kanał IRC. Co prawda przez pierwsze kilkadziesiąt minut (po godzinie 23 w piątek polskiego czasu) widać było, że serwery ledwo dają radę obsłużyć ogromny ruch, ale potem sytuacja unormowała się i w ciągu całego weekendu nie zanotowałem żadnych poważniejszych problemów – zadania były cały czas dostępne, a wysyłanie rozwiązań działało bez zarzutu.
Skoro mowa o rozwiązaniach, w tegorocznym BKP tradycyjnie dla tego rodzaju eventów pojawiły się zadania w kategoriach Reversing, Crypto, Pwning oraz specjalna kategoria dla początkujących, „School Bus”. Z racji doświadczenia i wiedzy (a może raczej ich braku ;) ), to właśnie zadania z tej kategorii skupiły moją uwagę. Poniżej zaprezentuję poszczególne wyzwania wraz z rozwiązaniami – jak się zaraz okaże, ten poziom trudności jest jak najbardziej osiągalny dla każdego z nawet niewielkim doświadczeniem programistycznym, ale niewielką dozą cierpliwości i „zacięcia” w szukaniu drogi do flagi.
Bezkonkurencyjny Plaid Parliament of Pwning
Pisząc o tegorocznym BKPCTF nie sposób nie wspomnieć o jednym z najbardziej utytułowanych i znanych zespołów CTF, amerykańskim Plaid Parliament of Pwning (PPP). Zespół ten, jako jedyny ze wszystkich 828 teamów, rozwiązał wszystkie zadania, a co ciekawe, dokonał tego na 8 godzin przed oficjalnym zakończeniem rozgrywki. To wydarzenie zostało odnotowane przez organizatorów, którzy nie omieszkali wspomnieć o nim na Twitterze:
PPP have solved *EVERY* challenge, with still 8 hours left. Congrats to this year’s champions! #bkpctf
— Boston Key Party (@BkPctf) marzec 1, 2015
„School bus”, czyli coś dla CTF-owych „żółtodziobów”
Zestaw zadań, który organizatorzy umieścili w kategorii o wymownej nazwie „autobus szkolny”, składał się z dziesięciu zróżnicowanych pod względem trudności, ale dość prostych wyzwań (prostych nie oznacza w tym wypadku, że nie trzeba było się przy nich trochę nagimnastykować…), z czego sześć polegało na pokonaniu formularza logowania, zaimplementowanego w PHP i zabezpieczonego różnego rodzaju mechanizmami. Ponieważ udało mi się przez nie przebrnąć, zaprezentuję przykładowe sposoby, którymi udało mi się dotrzeć do tzw. flag, czyli tego, co w terminologii CTF jest finalnym rozwiązaniem i które należy przesłać, by zdobyć punkty.
Wszystkie poniższe zadania wraz z rozwiązaniami oraz ich kodami źródłowymi zamieściłem na stronie https://github.com/bl4de/ctf/tree/master/BostonKeyPartyCTF_2015 , dlatego tutaj pozwolę sobie ograniczyć listingi tylko do istotnych z punktu widzenia rozwiązania fragmentów kodu.
Brigham Circle
W zadaniu Brigham Circle formularz logowania sprawdzał, czy w podanym haśle znajdują się dwa znaki myślnika – jeśli tak, wyświetlana była flaga. Utrudnieniem była walidacja przesyłanego hasła wyrażeniem regularnym, które dopuszczało jedynie duże i małe litery oraz cyfry:
if (isset ($_GET['password'])) { if (ereg ("^[a-zA-Z0-9]+$", $_GET['password']) === FALSE) echo '<p class="alert">You password must be alphanumeric</p>'; else if (strpos ($_GET['password'], '--') !== FALSE) die('Flag: ' . $flag); else echo '<p class="alert">Invalid password</p>'; }
Pozornie wyglądało to na dwa niemożliwe do spełnienia jednocześnie warunki. Ale tylko do momentu, w którym uświadomimy sobie, że wyrażenie regularne w funkcji ereg() nie zawiera żadnych flag, w tym flagi 'm’ (multiline), oznaczającej sprawdzanie ciągu zapisanego w wielu linijkach.
Podanie ciągu zawierającego odpowiednio zakodowany znak przejścia do nowej linii, a po nim wymaganego podwójnego myślnika załatwiło sprawę:
http://52.10.107.64:8006/?password=aaaa%0008--
Northeastern Univ.
Kolejne zadanie nie zawierało żadnej wskazówki co do zawartości hasła. Pierwszą myślą był klasyczny atak siłowy („brute force”) lub słownikowy – niestety, nie było żadnej pewności, jak długi miałby być ciąg zawierający hasło, co praktycznie przekreślało szanse powodzenia takiego ataku w rozsądnym czasie:
if (isset($_GET['password'])) { if (strcmp($_GET['password'], $flag) == 0) die('Flag: '.$flag); else print '<p class="alert">Invalid password.</p>'; }
Należało się więc skupić na „popsuciu” warunku tak, by zwrócił 'false’ (co w rezultacie zakończyłoby się sprawdzeniem, czy false == 0 – porównanie bez sprawdzenia typów sprawiłoby, że warunek byłby prawdziwy i flaga zostałaby wyświetlona.
Ponieważ funkcja strcmp() przyjmuje dwa argumenty będące stringami, próba przekazania jednego z nich jako np. tablicy wywoła oczekiwany przez nas skutek (strcmp() zwróci false). Mamy kontrolę nad pierwszym z argumentów ($_GET[’password’]), więc nie stanowi to żadnego problemu:
http://52.10.107.64:8003/?password[]=aaa
Prudential
Stosując podobne podejście, można było rozwiązać kolejne zadanie:
if (isset($_GET['name']) and isset($_GET['password'])) { if ($_GET['name'] == $_GET['password']) print 'Your password can not be your name.'; else if (sha1($_GET['name']) === sha1($_GET['password'])) die('Flag: '.$flag); else print '<p class="alert">Invalid password.</p>'; }
Z pozoru sytuacja wygląda na beznadziejną. Dwa różne ciągi, których skróty SHA1 mają być jednakowe? Pierwsza myśl – kolizja. Ale, ale… to nie MD5, gdzie potencjalnych kolizji można znaleźć przy pomocy Google’a pełno. Owszem, w teorii SHA1, jak każda funkcja skrótu, jest podatna na kolizje, ale szybkie przejrzenie wyników wyszukiwania dla frazy „SHA1 collision” ujawnia nam sedno problemu – taka kolizja jak na razie możliwa jest… czysto teoretycznie. Poza tym zadanie zostało wycenione na 25 punktów (gdzie zadania naprawdę trudne pozwalały zdobyć od 100 do 600 punktów). Stąd musiało istnieć inne, prostsze rozwiązanie – łamanie algorytmu SHA1 to raczej nie było to, co autorzy mieli na myśli.
No i oczywiście istniało, a polegało dokładnie na tej samej sztuczce, co w Northeastern Univ. Należało „unieszkodliwić” warunek $_GET[’name’] == $_GET[’password’] jako 'name’ i 'password’ przesyłając argumenty typu tablicowego o różnych wartościach, co w rezultacie powodowało przejście do kolejnego warunku:
http://52.10.107.64:8001/?name[]=aaaa&password[]=bbbb
Ponieważ sha1() przyjmuje argument typu String, a 'name’ i 'password’, były tablicami – po zrzutowaniu na tym String miały wartość 'Array’ (ponieważ nastąpiła niejawna konwersja typu z tablicy do ciągu). Porównanie sha1(’Array’) z sha1(’Array’) pozwoliło na wyświetlenie flagi.
Symphony
Pytanie: jaka liczba jest równocześnie większa od 999, a składa się z mniej niż trzech cyfr?
if (isset($_GET['password'])) { if (is_numeric($_GET['password'])){ if (strlen($_GET['password']) < 4){ if ($_GET['password'] > 999) die('Flag: '.$flag); else print '<p class="alert">Too little</p>'; } else print '<p class="alert">Too long</p>'; } else print '<p class="alert">Password is not numeric</p>'; }
Kluczem jest tutaj przypomnienie sobie, jak w PHP możemy zapisać dowolną liczbę: w systemie dziesiętnym (np. 1000), heksadecymalnym (np. 0xFFAA), oktalnym (ówsemkowym, np. 02222) oraz w postaci wykładniczej (np. 2e12). I właśnie ten ostatni system pozwala nam podać liczbę większą od 999, ale składającą się jedynie z trzech znaków:
http://52.10.107.64:8002/?password=4e3
Museum of Fine Arts
Ostatnie z prezentowanych zadań moim zdaniem było najciekawsze. Po wejściu na podany adres www naszym oczom ukazywał się formularz logowania z sekwencją trzech losowo wygenerowanych liczb:
Naszym zadaniem było podanie kolejnej liczby z sekwencji, która była poszukiwanym hasłem.
Spójrzmy na kod:
<html> <head> <title>level4</title> <link rel='stylesheet' href='style.css' type='text/css'> </head> <body> <?php session_start(); require 'flag.php'; if (isset ($_GET['password'])) { if ($_GET['password'] == $_SESSION['password']) die ('Flag: '.$flag); else print '<p class="alert">Wrong guess.</p>'; } // Unpredictable seed $seed = (microtime() ^ rand(1, 10000)) % rand(1, 10000) + rand(1, 10000); mt_srand($seed); ?> <section class="login"> <div class="title"> <a href="./index.txt">Level 4</a> </div> <ul class="list"> <?php for ($i=0; $i<3; $i++) print '<li>' . mt_rand (0, 0xffffff) . '</li>'; $_SESSION['password'] = mt_rand (0, 0xffffff); ?> </ul> <form method="get"> <input type="text" required name="password" placeholder="Next number" /><br/> <input type="submit"/> </form> </section> </body> </html>
Aby znaleźć czwartą liczbę, musimy wiedzieć, jakie ziarno (zmienna $seed) zostało użyte do zainicjowania generatora liczb pseudolosowych (argument funkcji mt_srand()). Po analizie wyrażenia obliczającego ziarno można zauważyć, że użyta została reszta z dzielenia przez liczbę z zakresu od 1 do 10000, która nie może „wychodzić” poza ten zakres.
Spójrzmy na prosty przykład ilustrujący koncepcję reszty z dzielenia: 7%3==1, 8%3==2, 9%3=0, 10%3==1 itd. Reszta z dzielenia przez daną liczbę zawsze będzie zawierać się w przedziale od 0 do tej liczby minus 1. Dlatego też ziarno obliczane w ten sposób:
(microtime() ^ rand(1, 10000)) % rand(1, 10000) + rand(1, 10000)
nigdy nie uzyska wartości spoza zakresu od 1 do 10000.
Mamy więc 10000 możliwych sekwencji czterech liczb. To niedużo, więc możemy posłużyć się prostym skryptem, który po podaniu jednej z trzech wyświetlonych w formularzu liczb znajdzie nam tę czwartą, będącą hasłem:
<?php $pass = $_GET['pass']; $found = false; for ($seed = 0; $seed <= 99999; $seed++) { mt_srand($seed); $found = false; for($i = 0; $i < 3; $i++) { $seq = mt_rand(0, 0xffffff); //found in sequence! if ($pass == $seq) { echo "Found!"; $found = true; } } if ($found) { echo "<br /><strong>next: " . mt_rand (0, 0xffffff) . "</strong><hr />"; } }
Wszystkie przedstawione zadania, choć pozornie mogą wydawać się skomplikowane, udało się rozwiązać dzięki znajomości podstawowych reguł rządzących językiem PHP – niejawne rzutowanie w operacjach porównań, operator reszty z dzielenia, argumenty przekazywane do funkcji, format zapisu zmiennych typu liczbowego czy sposób interpretacji wyrażeń regularnych.
Dlatego nawet proste wyzwania to okazja do odświeżenia wiedzy, która często, w codziennej pracy nam „umyka” i przypomnienia sobie o detalach, na które nie zwracamy uwagi (gubi nas rutyna, niestety) i popełniamy błędy, które mogą spowodować trudne do przewidzenia konsekwencje.
Podsumowanie
Jak już wspomniałem na początku, frekwencja na tegorocznym Boston Key Party dopisała. Cieszy niezmiernie fakt, że wzięło w nim również udział sporo teamów z Polski.
Tradycyjnie już w ścisłej czołówce (na 9. pozycji) zakończył grę Dragon Sector, a kolejne pozycje zajęły: Snatch The Root (62.), DebugTeam (110.), an empty fridge (142.), Church of 0x41414141 (153.), psuje (166.), Cheating Owls – najwyżej sklasyfikowany polski debiutant (169.), CS-WAT (188.) – drugi z debiutantów, zespół Wojskowej Akademii Technicznej w Warszawie, bl4de (285.), Flaming Trolls (261., również pierwszy rozegrany CTF), Steampunkers (kolejny zespół grający po raz pierwszy w tym roku, 366. pozycja), Hawks (468.), Sturdy Arch Devils (496.) oraz WithoutConcept – ostatni zespół, który od BKPCTF 2015 rozpoczął tegoroczne zmagania w CTF (761. pozycja).
Celowo wymieniłem wszystkie startujące polskie zespoły, gdyż w porównaniu do stycznia (http://sekurak.pl/ctf-podsumowanie-stycznia-2015/) nastąpił znaczny postęp w udziale rodzimych drużyn, co cieszy niezmiernie. Jak już pisałem jakiś czas temu, wbrew pozorom CTF-y nie są czymś nieosiągalnym – najważniejsze są chęci, zacięcie do nauki i szukania rozwiązań, a każde poprawnie zakończone zadanie to mnóstwo satysfakcji, no i nowe umiejętności, które zawsze mogą przydać się w codziennej pracy.
Pełny ranking znajduje się na stronie https://ctftime.org/event/163. Dodatkowo w chwili pisania artykułu dostępna była oficjalna strona BKPCTF 2015 (http://bostonkey.party/), na której nadal znajdują się wszystkie zadania.
Show must go on
BKPCTF 2015 to już historia, a co czeka nas w najbliższym czasie? Już w połowie miesiąca, w dniach 14-15 marca kolejny klasowy turniej – Codegate CTF Preliminary 2015, a zaraz po nim, w dniach 16-18 marca, B-Sides Vancouver 2015 (oba dostępne on-line). Po tygodniu oddechu czekają nas jeszcze BCTF 2015 i odbywający się jedynie na miejscu, w Genewie, Insomni’hack 2015. Marzec zapowiada się zatem niezwykle interesująco i na pewno nie zabraknie emocji, wypitych hektolitrów kawy i paru nieprzespanych nocy :)
Do zobaczenia na kolejnym CTF-ie.
Wszystkich zainteresowanych tematyką CTF-ów zapraszam do odwiedzenia strony ctftime.org, gdzie można znaleźć informacje o rozegranych i zbliżających się turniejach, punktacje, write-upy zadań i wiele innych przydatnych informacji.
Rafał 'bl4de’ Janicki
Może tak zawiązać Team? Sekurhack.
Parę zespołów szuka już graczy -> http://www.ctfteam.pl/
;)
A jakby w zadaniu „Museum of Fine Arts ” wysłać nieistniejącą sesję? Wtedy hasło zapisane w sesji jest równe NULL, wiec wystarczy wysłać puste pole z hasłem :p
//….
Swoją drogą jest jeszcze polski zespół aPONYmous_PL :)
Masz absolutną rację, zresztą jest nawet opis rozwiązania, które podajesz:
https://github.com/ctfs/write-ups-2015/tree/master/boston-key-party-2015/school-bus/museum-of-fine-arts
:)
Faktycznie, przeoczyłem zespół aPONYmous_PL , za co serdecznie przepraszam Grzegorza Uriasza (grzegorz225). 570 punktów i 130. miejsce to bardzo dobry wynik :)
https://ctftime.org/team/13721
Brawo, cieszę się, że Polaków widać od dobrej strony w takiej dziedzinie :)
„(…)Mamy więc 10000 możliwych sekwencji czterech liczb.”- Wlasciwie to mamy 20000 mozliwych sekwencji, bo na koncu jest jeszcze dodawanie. Innym sposobem rozwiazania zadania, ktory bedzie mozliwy takze, gdy nie ma ograniczenia na seed jest: http://www.openwall.com/php_mt_seed/ przy pomocy ktorego przeszukanie calej przestrzeni 32bitowej zajmuje kilkadziesiat sekund na porzadnym 4 rdzeniowym cpu, lub 7 sekund na Xeonie Phi.
Słuszna uwaga – 20000 kombinacji :)
Trafiłem akurat z seedem mniejszym niż 10000, stąd nie zwróciłem uwagi na to dodawanie.
Dodatkowo + zmienia dramatycznie rozklad prawdopodobienstwa, wiec efektywne przegladanie nalezalo by przeprowadzic inaczej niz for(i=1;i<=20000;i++) :)