https://ctfd.rozdzka.securing.pl/ https://vallheru.rozdzka.securing.pl/
Jak na CTFa, to wydawał się dość prosty - szczególnie dla mnie, laika bez doświadczenia. Wszystkie zadania były "jednowarstwowo" trudne, do wielu trudniejszych można znaleźć write-upy podobnych zadań w internecie. Strona jest forkiem open-sourcowego silnika a autorzy CTFa udostępnili większość źródeł .php, więc można było wręcz po prostu diffować oryginalne pliki z plikami CTFa żeby zobaczyć gdzie dodali luki.
Witamy w Dębinie (10): Login w HTMLu strony głównej, hasło automatycznie wypełnione.
Nie jesteś władcą (10): Wejdź na /admin.php i tyle.
Zdrój wiedzy (10): Wejdź w Bibliotekę, kliknij "źródło strony" i tyle.
Promocja (50):
Strona raz używa $_GET['promo']
, a raz $_REQUEST['promo']
. Jedno ustawiam z URLa ?buy=1&promo=
, drugie ustawiam z ciastka na 'DNIDEBINY2021' co omija sprawdzanie ważności promocji.
Subskrypcja (50): https://www.programmersought.com/article/2633234394/
Prohibicja (50):
Parametry do wywnioskowania z czytania .php: ?read=1&one=1
i tyle.
Kukła (50):
Trzeba przekazać ?action=dummy&fight=LICZBA
. Liczba jest generowana w PHP przez rand()a, ale seed jest znacznie mniej losowy niż się wydaje, bo kod błędnie używa +
do dodania stringów, przez co PHP próbuje konwertować stringi na liczby. Można więc po prostu ten sam kod odpalić kilka razy na np sandbox.onlinephpfunctions.com i przekopiować najczęściej powtarzający się wynik do urla.
Jubiler (100):
Tutaj trzeba poczytać o "feature"ach PHP $$zmienna
oraz extract($_GET)
. Drugie pozwala efektywnie nadpisywać zmienne lokalne. URL: ?craft=asdf&asdf[inscription]=asdf&asdf[material]=asdf&buy=1&ringPrice=0
.
List do sołtysa (200):
Patrz https://palletsprojects.com/blog/jinja-2-8-1-released/
Template nie dostaje żadnych zmiennych, a natywne metody nie mają __global__
więc jedyna wartość która pozwala na wyrwanie się z sandboxa przez __init__.__global__
to conveniently zredefiniowana przez Jinję funkcja range
:) Stamtąd tylko przeskoczyć do klasy zdefiniowanej w kontekście mającym dostęp do modułu sys
, a stamtąd to już prosta droga do flagi.
Moja ścieżka do flagi:
{{ "{0.__globals__[Environment].__init__.__globals__[sys].modules[__main__].FLAG}".format(range) }}
(do obu tych zadań mamy dostępny ten sam kod .cpp)
Stary Hazardzista (100):
Wejście do seeda RNG pochodzi od inputa usera (gets
) oraz stałego tekstu. Tak się składa że oba przechodzą przez małe bufory zaalokowane przez malloc() i te bufory są oddalone o dokładnie 32 bajty, więc wystarczy wpisać za długi tekst na wejście - w ten sposób mamy deterministyczny seed, który można odpalić lokalnie a wynik przepisać do usługi.
Stary Hazardzista kontratakuje (200): "Stały tekst" z pierwszej części to tak naprawdę flaga. Odpalając program dla każdej długości wejścia, dostajemy w odpowiedzi wartości losowe wygenerowane na podstawie częściowo nadpisanej flagi. Mając tą informację (i robiąc poprawkę na null terminator), można na lokalnej binarce bajt po bajcie (od końca) odkrywać kolejne bajty flagi.
Szafa grająca (50): To jest dosłownie "baby's first stack overflow exploit" :) Program sam tekstowo podaje adres funkcji z flagą, po czym pyta do jakiego indeksu tablicy (na stosie) wpisać podaną przez usera liczbę. Wystarczy jako indeks wpisać "-9" (indeks na return address funkcji), jako liczbę adres tej funkcji i to wszystko.
Pozytywka (150):
Bardzo podobne do pierwszego - trzeba na stosie nadpisać return address na adres funkcji wyciągającej flagę. Tym razem stack overflow robi się trochę bardziej ręcznie, przez wejście tekstowe (gets
). Przeszkodą jest stack canary, ale program "pomocnie" prosi o wejście dwukrotnie - raz by wyciągnąć stack canary wypisany na konsolę, za drugim razem by nadpisać return address.
(Myślałem że przeszkodą będzie że null terminator z pierwszego gets
nadpisze pierwszy bajt stack canary, ale wygląda na to że jego pierwszy bajt to i tak 0 ? )
Las (150): Podręcznikowe zadanie z advent of code :) Masz tekstowy labirynt, trzeba przekazując kierunki dostać się do wyjścia w wybranej krawędzi labiryntu. Do pathfindingu zrobiłem prosty BFS. Największą trudnością były nie lubiące mnie sockety :)
Zabytkowy zegar (100): Widać wzrokowo, że pierwszy wiersz pikseli odstaje od obrazka. Wyciągając bajty wiersza i traktując je jak ASCII dostałem flagę.
Miś (10):
strings teddy.jpg | grep WOC
i tyle. Wyglądało jakby w tym JPG był schowany inny format pliku, ale nie wnikałem.
Różdżka (50): Zaklęcie (100):
Na koniec zadania Las socket wypluł 6MB plik .apk zakodowany w base64. Nie mam telefonu z Androidem i miałem problemy z odpaleniem apk w emulatorze, więc zajrzałem w zdekompilowane źródła i... obie flagi były widoczne w kodzie. Różnica wycen obu zadań sugeruje, że autorzy nie przewidzieli że ktoś po prostu wyciągnie obie flagi z binarki.
public static final String BABAJAGA_FLAG_1 =
(...)
new String(new byte[]{(byte) (-1412234857 >>> 23), (byte) (1793390757 >>> 12), ...etc...});
public static final String BABAJAGA_FLAG_2 = (...);
ROTMistrz (10): Prosty ROT, bodajże 9.
Rozmowa (50):
Prosty szyfr podstawieniowy. Zgadywałem litery po kolei pół-ręcznie, zaczynając od Vsdss
-> s=i, a potem z pomocą słownika i wyszukiwania słów pasujących do wzorców typu
cat scrabble-polish-words.txt | grep -P "^(.).i.\1.\1..i$"
Pęk kluczy (50):
Próbowałem po omacku kilka na raz, aż zauważyłem podobieństwo ostatnich 4 znaków w kluczu 4 (PGP). Dopisałem -----BEGIN PGP MESSAGE-----
na początku i końcu wiadomości, po czym gpg
poprawnie odczytał wiadomość.
Złodziejska Spelunka (100):
Kod bierze flagę, koduje do liczby i dla każdego obrotu bitowego wypisuje jej pow(obrocona_liczba, 65537, BIG_EVEN_RANDOM)
. Wystarczy zaboserwować że bit jedności nie zmienia się w wyniku potęgowania ani modulo, więc dla każdej linii wyniku jej bit jedności odpowiada kolejnemu bitowi flagi, co pozwala odkodować flagę w kilku liniach Pythona.
Tutaj główną trudnością było zrozumieć jak to wszystko w ogóle działa :) Nauczenie się używania IDE, rozszerzenia do portfela etc.
Kości ze starcem (100): Kilka niemal identycznych zadań było już na CTFach i mają dobre write-upy, np: https://medium.com/blockchain-security/solutions-of-ethcc-2019-ctf-part-2-189627fefff0
Osypisko (200):
Bardzo podobne do poprzedniego, z tym haczykiem że tutaj transakcja w pierwszym bloku musi przewidzieć sha(block_data)%10
transakcji późniejszego bloku. Fakt że to jest %10
trywializuje sprawę - można wręcz próbować ręcznie do skutku*. Sprytniejszym sposobem jest wykonać wszystkie 10 przewidzeń na raz, po czym w późniejszym bloku w nowej transakcji policzyć sha()%10
i wybrać wcześniej wykonane pasujące przewidzenie.
* Choć miałem tu dziwne przeszkody, jak na początku bardziej spamowałem transakcjami to transakcje które by zgadły poprawnie (i tylko te) umierały przez "out of gas", np 0x903b54e87ddea534003803c999ac650655dc4f490318dcfd630325f76478483d.
(BTW, nie rozumiem czemu nie wrzucili kodu kontraktu drugiej części jako załącznik tak jak w pierwszej, jeśli kod i tak był dostępny na etherscan.io.) (BTW2, czy aby natura blockchaina nie sprawia że późniejsi uczestnicy mogą trywialnie zajrzeć w kod (albo po prostu użyć) kontraktów innych uczestników którzy już zaliczyli challenge?)
List z frontu (50):
Sprawdziłem których znaków ascii nie ma w zbiorze liter i zauważyłem że brakuje I, O, l - dopiero 2 dni później zorientowałem się że dokładnie tak wygląda base58. Potem CyberChef magicznie sam wydedukował resztę drogi: zlib inflate* -> gunzip -> fromhex -> flaga.
* też przez 2 dni uparcie błędnie używałem deflate :/
Chcemy sobie być wadzi? (100): ???