tom@home.htb:~$

Blog o HTB

29 November 2020

File read a include varianty mimo klasické LFI

Úvod a kontext

Klasické LFI si většina lidí vybaví jako ../../../../etc/passwd. To je dobrý základ, ale v praxi se file read a include chyby často schovávají v méně nápadných variantách. Aplikace sice zablokuje obyčejný traversal, ale stále přijme php://filter, UNC cestu na SMB share nebo vstup, který se po unicode normalizaci změní na zakázanou sekvenci až příliš pozdě.

Ten rozdíl je důležitý i metodicky. Nejde jen o to, jestli “mám traversal”. Důležité je, jak aplikace interpretuje zadanou cestu, jakou vrstvu abstrahuje jazyk nebo framework a v jakém pořadí probíhá validace, normalizace a samotné otevření souboru. Pokud vás zajímá klasický základ, navazuje tento text na starší článek o Local File Inclusion.

Co mají tyto chyby společné

Na první pohled jde o různé techniky, ale všechny stojí na stejném omylu: aplikace si myslí, že kontroluje souborovou cestu, zatímco runtime ve skutečnosti pracuje s jiným významem vstupu.

V praxi bývá problém v jedné z těchto vrstev:

Proto dává smysl mluvit spíš o rodině file read a include chyb než jen o jedné LFI zkratce.

Varianta 1: stream wrapper místo klasické cesty

V PHP není vstup do include, file_get_contents() nebo podobné funkce vždy jen “cesta k souboru”. Může jít i o wrapper, tedy speciální schéma, které říká, jak má být zdroj otevřený a zpracovaný.

Typický příklad:

$path = $_POST['url'];
echo file_get_contents($path);

Pokud aplikace očekává cestu k obrázku nebo konfiguračnímu souboru, ale nepoužije pevný allowlist, může vstup typu:

php://filter/convert.base64-encode/resource=/etc/passwd

změnit význam celé operace. Už nejde o běžné načtení lokálního souboru, ale o wrapper, který obsah nejdřív přečte a pak ho před vrácením zakóduje.

Praktický dopad je dvojí:

To je přesně důvod, proč wrappery patří do stejné mentální kategorie jako LFI, i když na payloadu není ani jedno ../.

Varianta 2: síťová cesta, která vypadá jako soubor

Na Windows je další zrádná vlastnost: mnoho funkcí pracujících se soubory akceptuje i UNC cesty ve tvaru \\\\server\\share\\soubor.

Pokud aplikace dělá něco jako:

include($_GET['lang'] . '.php');

vývojář často myslí na:

Nemusí ale myslet na to, že operační systém a runtime mohou považovat síťový share za “normální souborový zdroj”. Na Windows tak může parametr pro jazykový soubor skončit u vzdáleného SMB share a aplikace pak includuje obsah, který neleží na lokálním disku, ale na stroji útočníka.

Tohle už není jen file read. Pokud daný runtime include zároveň vykonává, mění se chyba velmi rychle v RCE bez klasického uploadu.

Varianta 3: validace před jinou normalizací než používá aplikace

Další častý vzorec je, že filtr kontroluje surový vstup, ale otevření souboru proběhne až po normalizaci, canonicalizaci nebo jiném převodu znaků.

To je dobře vidět u unicode traversal variant. Aplikace zakáže ../, ale pracuje s řetězcem, který se později normalizuje na stejný význam. Znak, který původně neobsahuje doslova dvě tečky nebo lomítko, se po zpracování změní právě na ně.

Praktický problém pak není v tom, že by filtr “zapomněl” na traversal. Problém je v pořadí operací:

  1. aplikace zkontroluje původní tvar vstupu,
  2. jiná vrstva ho normalizuje,
  3. teprve normalizovaný tvar rozhodne, jaký soubor se otevře.

Výsledek je stejný jako u klasického LFI, ale root cause leží v nesouladu mezi tím, co validuje filtr, a tím, co čte runtime.

Proč tyto varianty unikají pozornosti

Na podobných chybách je zrádné hlavně to, že ochrana může na první pohled vypadat rozumně:

Jenže útok jde o úroveň níž. Neútočí na přesný text, který si představoval vývojář, ale na interpretaci cesty v jazyce, frameworku nebo operačním systému.

To je důvod, proč nestačí testovat jen několik obvyklých traversal payloadů. U file read chyb je často důležitější pochopit, jaký typ vstupu daná funkce ve skutečnosti akceptuje.

Jak o tom přemýšlet při analýze

Při review nebo testu je užitečné si položit několik konkrétních otázek.

Co přesně aplikace očekává

Kdo interpretuje význam vstupu

Kdy probíhá validace

Co se stane po úspěšném čtení

Kde se z file read stává větší problém

Samotné čtení souboru ještě nemusí znamenat shell. Prakticky ale velmi často vede k jedné z těchto dalších vrstev:

To je přesně důvod, proč je file read tak cenné primitivum. Často nejde o finální exploit, ale o nejspolehlivější způsob, jak z aplikace dostat tajemství a pochopit její vnitřní vazby.

Obrana a bezpečnější návrh

1. Nepřijímat obecnou cestu tam, kde stačí identifikátor

Pokud aplikace přepíná jazyk, šablonu nebo dokument, bezpečnější je přijmout pevný identifikátor:

$allowed = ['cz' => 'lang/cz.php', 'en' => 'lang/en.php'];
include($allowed[$_GET['lang']] ?? 'lang/cz.php');

Ne obecnou cestu od uživatele.

2. Validovat až po stejné normalizaci, jakou používá runtime

Kontrola nad surovým vstupem nestačí, pokud se jeho význam později změní. Bezpečnostní rozhodnutí se musí dělat nad canonicalizovaným tvarem cesty, ne nad tím, co uživatel poslal doslova.

3. Myslet na wrappery a síťové cesty jako na jiné typy vstupu

php://, file://, UNC share nebo jiný síťový path nejsou “detail syntaxe”. Jsou to jiné režimy práce se zdrojem. Pokud je aplikace neumí bezpečně potřebovat, nemá je akceptovat.

4. Oddělit file read od execute kontextu

Funkce typu include nebo templating helper jsou rizikovější než prosté čtení souboru, protože kombinují dva problémy:

Tam, kde je to možné, je bezpečnější pracovat s datovým obsahem, ne s dynamickým include mechanismem.

5. Chránit konfigurační tajemství i proti “pouhému” čtení

Jakmile aplikace umí číst své vlastní configy, je často pozdě uvažovat o dopadu jen jako o informačním úniku. V praxi to bývá přímá cesta k dalšímu přihlášení nebo k obejití jiné vrstvy systému.

Shrnutí klíčových poznatků

Co si odnést do praxe

tags: lfi - web - file-read - include