V závěru svého minulého článku pojednávajícího o
PHP Include jsem se zmínil o tom, že díky využívání obyčejných PHP funkcí neobyčejným způsobem lze mj. odkrývat stromovou strukturu webového serveru. V tomto článku bych se chtěl této problematice věnovat podrobněji. Pokusím se zde popsat metody vedoucí ke kompromitaci jak systémových složek, tak root adresářů jiných uživatelů webhostingu. V druhé části článku si poté ukážeme, jakým způsobem může útočník odhalené soubory přečíst.
Důvodů, proč je útočník schopen mapovat strukturu webového serveru, je hned několik. Od chyb samotných správců webů, přes nedokonalou bezpečnostní politiku majitelů webhostingových společností, až po nedostatky na straně samotného PHP. My se budeme zabývat druhou a třetí jmenovanou variantou. Již teď je tedy jasné, že k nápravě bude nutný přinejmenším zásah ze strany provozovatelů hostingu, vy danému útoku nijak zabránit nemůžete.
Určitě jste se již někdy setkali se sekcí, která byla zabezpečena pomocí JavaScriptu tím způsobem, že se za vložené heslo dosadila přípona a načetla se příslušná tajná stránka. To však platilo v případě vložení správného hesla, v opačném případě nebyl soubor na serveru nalezen a pokus skončil chybovým hlášením
# 404. I když se JavaScript na zabezpečování veřejně nepřístupných sekcí nikdy nedoporučoval, tato technika byla donedávna považována za poměrně bezpečnou. O to děsivější je fakt, že útočník takový "bezpečnostní prvek" překoná pomocí níže uvedené techniky během několika málo vteřin.
Listing adresářů
Množství PHP funkcí, které nějakým způsobem pracují se soubory, lze zneužít přinejmenším k detekování, zda se požadovaný soubor nebo adresář na serveru nachází. Postup je přitom vždy totožný - chybová hlášení. PHP v nich uvádí až příliš mnoho informací, které jsou ke všemu za určitých podmínek odlišné, i když se vždy jedná o tu jednu a samou chybu.
K vypsání obsahu adresářů na serveru pomocí PHP slouží mj. funkce
glob(). Zajímavé na ní je především její chování v případě, že se pomocí ní pokusíme o listing adresáře, kam nemáme díky direktivě
open_basedir přístup. Vypsání obsahu takové složky nám sice funkce díky zmíněné direktivě nepovolí, z nepochopitelného důvodu ale společně s chybovým hlášením vrátí i název prvního nalezeného souboru v daném adresáři. Tato informace je sice zajímavá, přesto sama o sobě pro útočníka ve většině případů nepoužitelná. Funkce
glob() má však oproti jí podobným funkcím jednu nespornou výhodu, lze v ní používat tzv. metaznaky, tedy "*" pro libovolný počet znaků (i žádný) a "?" pro právě jeden znak. Tato vlastnost dává útočníkovi možnost napsat si automatický script, kde pomocí metaznaku "*" vypíše postupně celý obsah zadaného adresáře, ke kterému nemá díky direktivě
open_basedir regulérní přístup.
My si ukážeme dva takové scripty.
První z nich má oproti druhému jednu nespornou výhodu, je velice rychlý, avšak za cenu důkladnosti. Nevypíše totiž vždy opravdu všechny soubory, ale většinou ca 80 % skutečného obsahu.
Běh
druhého scriptu trvá podstatně déle, průměrně 20 vteřin. Odměnou je ale opravdu kompletní obsah adresáře. Script je však poměrně náročný na systémové požadavky, a sice na paměť (
memory_limit) a maximální dobu běhu (
max_execution_time). Je proto vhodné zároveň s uploadem druhého scriptu na server nahrát i konfigurační soubor
.htaccess, který tyto limity zvýší:
php_value memory_limit "50M"
php_value max_execution_time 900
případně požádat přímo správce serveru o jejich navýšení.
Pokud je script (jakýkoliv z výše jmenovaných) spouštěn bez parametrů, automaticky se vypíše obsah root adresáře serveru, na kterém běží, tedy /. Listing jiných adresářů je možný buď pohybem pomocí odkazů (každá nalezená složka je automaticky odkazem na svůj obsah), nebo pomocí GET proměnné
path v url, tedy například:
http://www.hosting.tld/script.php?path=/home/user/
Ani jeden ze scriptů vizuálně nerozlišuje adresáře od souborů. O co se ve skutečnosti jedná zjistíte buď podle přípony, nebo až poté, co na daný název kliknete. V případě adresáře se vypíše nový obsah (respektive . a .. pro prázdný adresář), zatímco v případě souboru se doplní první řádek s absolutní adresou a nevypíší se žádná další jména (odkazy), tedy ani tečky symbolizující aktuální a nadřazený adresář.
Pomocí žádného ze scriptů se mi však nepodařilo vypsat opravdu obsáhlé adresáře s několika tisíci soubory. Takovým případem může být např. adresář
tmp, kde je tedy nutné výstup omezit pomocí metaznaků na menší počet souborů a adresářů.
Obrana
Jediným řešením výše zmíněného problému je přidání funkce
glob() do direktivy
disable_functions, čímž se ale zabrání i jejímu regulérnímu využití. Z deseti náhodně vybraných freehostingů a webhostingů v České republice měly tuto funkci zakázanou pouze dva! U zbylých se kdokoliv může procházet v libovolných adresářích, tedy jak v těch systémových, tak v těch patřících jiným uživatelům webhostingu.
Samotné zakázání funkce
glob() je sice nejlepším možným řešením, mapování struktury webového serveru to ale nezabrání. Jak jsem již zmínil v úvodu, k mapování lze využít i jiné PHP funkce, které nějakým způsobem pracují se soubory, například funkce
stat() atp. Také tyto funkce vracejí odlišný chybový výstup v případě, že se soubor nebo adresář na serveru nachází a jiný v případě, že ne. Narozdíl od
glob() však neobsahují podporu metaznaků. Automatický script by tedy na detekci obsahu adresářů používal brute force útok, což by bylo zdlouhavé a, co se systémových požadavků týče, velice náročné.
Čtení nalezených souborů
Víme již tedy, jakým způsobem může útočník odhalit obsah našich i systémových adresářů, má právo ale nalezené scripty a textbased soubory číst? Ano i ne. Kromě těch, které jsou na vašem serveru umístěné ve veřejně přístupném adresáři a vy jste doufali v to, že je díky žádným přímým odkazům na ně nikdo nenajde (zálohy, soubory s hesly, logy), může útočník číst díky zapnutému
safe_modu pouze soubory, které sám vytvořil, přesněji řečeno, které mají stejné UID, jako on sám (uživatel). To znamená, že má tedy možnost číst i soubory, které sám vytvořil, což by opravdu asi nikdo nečekal =)
Útočník tuto situaci vyřeší tak, že nejprve vytvoří script, díky němuž bude číst obsah jiných souborů, například pomocí funkce
readfile(), které se bude jako parametr (cesta k souboru) předávat obsah GET proměnné, aby bylo jeho ovládání co možná nejpohodlnější a nebylo nutné jej pro každý soubor přepisovat. Pomocí takového scriptu však může stále číst obsah pouze vlastních souborů, vytvoří si tedy ještě druhý, který ten první zkopíruje pod jiným jménem. Tuto kopii však již nevytvořil přímo útočník, ale samotný script, respektive server, druhý soubor má tedy UID serveru. Tím pádem je pomocí této kopie možné číst veškeré soubory, které server vytvořil, tedy session proměnné (
sess_*) a dočasné soubory v
tmp adresáři. Jakkoliv by se mohlo zdát, že je schopnost útočníka číst veškeré dočasné soubory k ničemu, opak je pravdou. V
tmp adresářích se totiž nacházejí soubory, které uživatelé nahráli pomoci upload formulářů na server a nesmazali je, soubory vytvořené pomocí speciálních PHP funkcí
tmpfile() a
tempnam(), v některých případech i logy a na jednom nejmenovaném webserveru dokonce kopie veškeré e-mailové komunikace všech uživatelů.
Soubory jsou zde z větší části uloženy pod náhodnými alfanumerickými řetězci, v mnoha případech bez přípon. Typ souboru lze ale snadno vyčíst z jeho hlavičky, doplnění suffixu a otevření v příslušném softwaru je pak otázkou okamžiku. Útočník má pomocí zmíněné metody možnost číst veškeré soubory vytvořené serverem, které se nacházejí v adresářích uvedených v direktivě
open_basedir (viz
phpinfo()).
Appendix
Výše popsaným technikám není možné nijak zabránit. V případě funkce
glob() je sice možné její zakázání, to ale zcela problém neřeší. Útok není možné ke všemu žádným způsobem detekovat nebo logovat, stává se tak tichou zbrání v rukou webhackerů ...
Emkei