/OD0DOPENTESTERA

Rozwiązania RADAR CTF oraz VOLGA CTF

Dzisiaj kilka ciekawostek powiązanych z językiem PHP na które natrafiłem podczas CTF-ów RADARCTF oraz VOLGA CTF.

1.

$salt = "secret";
$a = $_GET['a'];
$b = $_GET['b'];

if($a !== $a){ 
    if(hash('md5', $salt . $a) == hash('md5', $salt . $b)){ 
        echo "OK";
    } 
} 

Na początku mamy podać dwie wartości, które są od siebie różne, ale równocześnie ich hasz MD5 liczony razem z solą, która jest dla nas nieznana ma być identyczny. Wychodzi więc na to, że twórcy tego zadania, chcą abyśmy wygenerowali kolizję na funkcję skrótu MD5. I rzeczywiście - coś takiego mogło by być możliwe, tylko jest z tym jeden, malutki problem.

Nie znamy wartości soli, która została tutaj użyta. Nie możemy więc 1 do 1 przetestować tego kodu lokalnie i spróbować wygenerować kolizję używając naszej mocy obliczeniowej. A zasypywanie serwera milionami żądań to nie jest najlepszy pomysł. Trzeba do tego podejść inaczej. Do PHP możemy przekazywać parametry przy pomocy adresu URL. Zazwyczaj są to ciągi znaków czyli stringi. Ale nie tylko - możliwe jest też przekazanie tablicy1. W tym celu posługujemy się nawiasami kwadratowymi.

Tablice w HTML

Wtedy to, możemy się odnosić do tak przekazanych parametrów jak do zwykłych tablic w PHP. Tylko, ze tutaj funkcja hash - oczekuje, że otrzyma tekst, który zamieni na MD5 - a nie tablice. Nastąpi więc automatyczna konwersja tablicy na stringa. Wynik takiej konwersji nie jest oczywisty - w odpowiedzi otrzymamy bowiem ciąg Array.

Czyli hasz będzie liczony z połączenia soli oraz wyrazu Array. Tylko, że wcześniej porównujemy podawane przez nas parametry przy użyciu wykrzyknika i dwóch znaków równości. To oznacza, że PHP oprócz wartości parametrów, sprawdzi też ich typy. Nasza tablica - będzie miała typ Array - a równocześnie, możemy przekazać drugi parametr - będący zwykłym ciągiem znaków o wartości Array.

Porównanie tablic

Tutaj typy się nie zgodzą, pierwsza konstrukcja if zwróci zatem prawdę. Równocześnie hasz będzie liczony z tego samego ciągu - czyli w obu przypadkach ze słowa Array razem z solą - dzięki temu obeszliśmy oba zabezpieczenia.

2.

$number = "1000";
if (strlen($number) < 4) {
    if (is_numeric($number)) {
        if ($number > 10000) {
        	echo "OK";
        }
    }
}

Jak sprawić, aby 3 cyfrowa liczba była większa od 10 000 ? Teoretycznie niemożliwe zadanie. Przyjrzymy się mu bliżej. Najpierw sprawdzamy, czy podany przez nas parametr ma mniej niż 4 znaki przy pomocy funkcji strlen. Następnie sprawdzamy czy jest liczbą przy użyciu funkcji is_numeric. I to właśnie tutaj musimy przyjrzeć się manualowi PHP bliżej. Według manuala, funkcja ta sprawdza czy podany parametr jest liczbą. Ale jak to robi? Jak stwierdza, że dany tekst to liczba?

Funkcja is_numeric

Warto spojrzeć na przykładowe wartości. Bazując na nich widzimy, że rzeczywiście funkcja prawidłowo rozpoznaje różne typy danych. Zwykły tekst czy też tablica nie są rozpoznawane jako liczba. Ale dalej nie przybliża nas to bliżej rozwiązania. Spójrzmy nieco dalej, na przykłady od użytkowników. W jednym z komentarzy możemy przeczytać, że ciąg 4e4 jest również traktowany jako liczba. Dlaczego? Ponieważ jest to postać wykładnicza.

Postać wykładnicza

Jest to sposób przestawiania liczby rzeczywistej, szczególnie przydatny do bardzo dużych lub bardzo małych liczb. Patrząc na przykłady z Wikipedii2 widzimy, że 1e0 to tak naprawdę 1*10 do potęgi 0. W naszym więc wypadku możemy przy pomocy trzech znaków zapisać 9e9 - co będzie równoznaczne z 9 * 10 do potęgi 9. A to zdecydowanie jest większa liczba od 10 000. Kolejna zagadka rozwiązana.

3.

$d = $_GET['d'];
if (strlen($d) == 3 and !is_numeric($d)) {
    if (floatval($d) == md5($d)) {
        echo 'OK';
   }
}

Tym razem wprowadzony przez nas parametr musi mieć dokładnie 3 znaki. Co więcej, nie może być liczbą. Następnie z tej wartości wyliczany jest hash MD5 który to jest porównywany z wynikiem funkcji floatval. Już na pierwszy rzut oka wiemy, że w grę wchodzi tutaj pewnego rodzaju sztuczka. Do dyspozycji mamy bowiem 3 znaki, a hasz MD5 ma ich dokładnie 32.

Odpowiedź na rozwiązanie tej zagadki czai się w funkcji floatval. Ona to zamienia wartość podaną przez użytkownika na liczbę zmienno-przecinkową. Jak tego dokonuje? Rozpatruje ona podany ciąg od lewej do prawej strony - sprawdzając, czy ma do czynienia z cyframi lub kropkami - oznaczającymi liczbę zmienno-przecinkową. Jeżeli napotka pierwszy, inny znak - kończy swoje działanie. Tak więc 2a będzie równe 2 a 34b będzie zamienione na 34. Chociaż nie do końca, ponieważ zwracana wartość to nie liczba całkowita, ale zmienno-przecinkowa. A to ma znaczenie. Wtedy bowiem podczas użycia podwójnego znaku równości podczas porównywania różnych typów w PHP dochodzi do automatycznej ich konwersji.

Szczegóły tej konwersji są opisane w manualu3. Mówiąc w skrócie ciąg znaków zostanie zamieniony na liczbę przy użyciu dokładnie takich samych zasad jak opisałem wcześniej. No dobrze, ale co nam to daje? Przy użyciu funkcji floatval możemy zwrócić dowolną cyfrę od 0 do 9.

Wystarczy bowiem, że nasza wartość będzie się zaczynała od danej cyfry a dalej podamy dowolną literę. Funkcja po prostu ją zignoruje - zwracając pierwszą cyfrę. To samo stanie się z haszem MD5. Jeżeli zaczyna się on od 1 a następnie pojawi się jakakolwiek litera - PHP podczas zmiany typu zwróci jeden jako wynik. Szukamy wiec takiego ciągu trzech znaków, który zaczyna się od 1 a następnie posiada 2 litery. Ten to ciąg zamieniony na hash MD5 - ponownie będzie się zaczynał od 1 a na drugim miejscu będzie posiadał dowolną literę. Napiszemy do tego prosty skrypt, który sprawdzi nam wszystkie takie możliwe kombinacje.

// Kod ze strony https://stackoverflow.com/questions/19067556/php-algorithm-to-generate-all-combinations-of-a-specific-size-from-a-single-set
function sampling($chars, $size, $combinations = array()) {
    if (empty($combinations)) {
        $combinations = $chars;
    }
    if ($size == 1) {
        return $combinations;
    }
    $new_combinations = array();
    foreach ($combinations as $combination) {
        foreach ($chars as $char) {
            $new_combinations[] = $combination . $char;
        }
    }
    return sampling($chars, $size - 1, $new_combinations);
}

$chars = array('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'w', 'x', 'y', 'z');
$output = sampling($chars, 2);

foreach ($output as $a) {
	$test = '1'.$a;
	
	if (md5($test) == 1) {
		die($test);
	}
}

I rzeczywiście już po chwili otrzymujemy interesujący nas wynik.

MD5

Oczywiście liczba ta nie musi się zaczynać od 1, równie dobrze może to być 2, 3 czy tez 9.

4.

<?php

$e = $_GET['e'];
if (substr(strtolower($e), 0, 4) != "http") {
	if (file_get_contents($e) === "0") {
		echo "OK";
	}
}

Tym razem funkcja file_get_contents, która zwraca treść pliku lub adresu URL podanego jako parametr. Treść zwracana przez tą funkcję musi być równa 0. Tylko, że tutaj nie możemy użyć żadnego adresu URL. Dodatkowo, nie wiemy czy w danym systemie operacyjnym istnieje plik z taką treścią. Jak zatem obejść ten mechanizm?

Funkcję operujące na plikach - oprócz obsługiwania plików i protokołu http - mogą także korzystać z innych wrapperów4. Jednym z nich jest data://. Pozwala on na zakodowanie treści przy użyciu algorytmu base64. Ta to treść zostanie odkodowana przez podaną funkcje i zwrócona jako jej wynik. Aby więc ominąć ta blokadę należy użyć odpowiedniego wrappera data - gdzie ciąg 0 zapisujemy w kodowaniu base64.

5.

class Secrets { 
    var $temp; 
    var $flag; 
} 
    
$f = $_GET['f']; 
$r = unserialize($f); 
$r->flag = "SEKRET";

if ($r->flag === $r->temp) {
  	echo 'OK';
}

Tym razem mamy do czynienia z deserializacją obiektu, pochodzącego od użytkownika. Ten to parametr jest przekazywany do funkcji unserialize. Potem porównujemy wartość temp z wartością flag. Problem w tym, że wartość flag jest tajna i jej nie znamy. Wychodzi więc na to, że wymaga się od nas zastania jasnowidzem i przewidzenia jaka wartość została tam użyta. Nie do końca. Poszukując rozwiązania do tego zadania, poszukiwałem informacji na temat tego jak zserializowane dane są przetrzymywane wewnętrznie przez sam język. Na jednym blogu5 zauważyłem rozpisze typów, które mogą być przechowywane wraz z ich identyfikatorami.

Serializacja danych

Zaciekawiła mnie literka R oznaczająca referencje. Poszukiwania dalej zaprowadziły mnie do forum Stackoverflow6.

Użycie referencji

Tam to jeden z programistów pokazuje, jak zserializować obiekt używając referencji.

$b = new Secrets();
$b->temp =& $b->flag;
echo serialize($b);

Tak też zrobiłem - to znaczy podczas serializacji ustawiłem wartość temp na referencję do flag a następnie użyłem funkcji serialize.