Hlavní navigace

Zranitelnosti typu injekce: XSS aneb cross-site scripting

David Formánek

Představíme si zranitelnosti typu XSS, které umožňují pouštět cizí skripty v prohlížeči oběti. Ukážeme různé způsoby, jak k tomu může dojít, co tím může útočník získat, a že obrana je složitější, než se na první pohled zdá.

Doba čtení: 13 minut

Sdílet

minulých dílech seriálu jsme se zabývali SQL injekcí a zranitelnostmi typu command injection. Dnes se podíváme na chybu cross-site scripting, zkráceně XSS (CSS by se pletlo s kaskádovými styly). Jde o specifický problém, který se běžně vyčleňují mimo injekční zranitelnosti jako samostatná chyba. Prakticky se ale jedná o injekci pro jazyk HTML, případně JavaScript či další obsah interpretovaný prohlížečem.

Rozdíl oproti ostatním injekcím je v tom, že kód upravený útokem se nevykoná přímo v aplikaci na serveru, ale na straně uživatele, který si přece může pouštět kód jaký chce… Dopad chyb XSS byl tvůrci webových aplikací proto často bagatelizován, ve skutečnosti ale nepřímo umožňuje útočníkovi vydávat se za uživatele, kterému se načte zranitelná stránka. Průměrný dopad je nižší než u injekcí z předchozích dvou článků, hodně ale závisí na typu aplikace a uživatelské roli oběti. Může se jednat o kritický problém, záleží na tom, co vše lze zjistit nebo vykonat skrz webové rozhraní.

Organizace OWASP problém zařadila ve svém Top 10 nejnebezpečnějších webových chyb v roce 2013 na třetí místo, v roce 2017 jej přeřadila na sedmou pozici, což je však stále dost na to, aby si zasloužil velmi vysokou pozornost. K poklesu bohužel nedošlo proto, že by se chyba přestávala vyskytovat, mohou za to spíše nové položky, na které je třeba upřít pozornost.

Podle dostupných dat lze XSS nalézt ve dvou třetinách všech webových aplikací. Kdo používá webové systémy pro správu obsahu, určitě by měl sledovat nové zranitelnosti. Jen před pár dny bylo např. objeveno XSS v systému Drupal a velmi čerstvé jsou i chyby ve Wordpress a Joomla. Problém se ale netýká jen webových aplikací, minulý rok se našla zranitelnost v desktopové verzi komunikátoru Signal, který je přitom považován za jeden z nejbezpečnějších (dobrou vizitkou je, že opravená verze vyšla už 5 hodin po nahlášení).

Princip XSS

Chyba vzniká na místech, kde se webová stránka generuje dynamicky a vkládá nedůvěryhodný vstup. Uvažme například stránku s vyhledáváním, kde bude chtít autor aplikace vložit vyhledávaný výraz na začátek stránky. Pak bude potřeba vložit HTML kód jako

"<p>Výsledky vyhledávání pro <em>" + searchQuery + "</em></p>"

Útočník ale může za searchQuery vyplnit libovolný HTML kód, např. <script>alert("Baf!")</script> a pokud není vstup nijak ošetřen, stane se součástí zobrazené stránky. Aby vznikl validní HTML kód, správně by se měly doplnit uzavírací a otvírací tagy, ale prohlížeč si většinou poradí i bez nich. Záleží ale na kontextu, kam se vstup vkládá – možná aplikace raději zobrazí předvyplněný formulář, aby se dal hledaný výraz snadno upravit, a kód by se pak mohl generovat jako

"<input type='text' value='" + searchQuery + "'>"

V takovém případě bude potřeba vložit řetězec začínající na '><script>, aby se útočník dostal mimo obsah elementu input. Na rozdíl od ostatních chyb typu injekce ale není nutné formát odhadovat, stačí se podívat do zdrojového kódu. Pokud by vkládaný skript měl být příliš dlouhý, obsah skriptu se může také načíst z útočníkovi domény ( <script src='...'). Příkaz alert se často používá pro demonstraci existence zranitelnosti, co vše lze spuštěním vhodného skriptu docílit vysvětlíme později.

Reflected, DOM-based nebo stored?

Vložením kódu do pole ale dojde jen ke spuštění v útočníkově prohlížeči, jak se kód dostane k oběti? Z tohoto pohledu rozlišujeme několik typů XSS. V případě příkladu s formulářem se jedná o tzv. reflected XSS (někdy též XSS typu 1).V tomto případě je nutné oběť navést, aby klikla na odkaz, který bude obsahovat kód v parametru asi jako

zranitelny-web.cz/search?q=%3Cscript%3Ealert%28%22Baf%21%22%29%3C%2Fscript%3E

Požadavek se odešle na server zranitelného webu, parametr se dekóduje z URL, vloží do dynamické stránky a výsledek odešle jako HTTP odpověď zpět oběti. Prohlížeč výsledné HTML a JavaScipt interpretuje a zobrazí okno typu alert, kód se tak vlastně od serveru jen „odrazí“ (odtud ten název). Pokud by se formulář neodesílal HTTP metodou GET, ale jednalo se o POST požadavek, útočník bude muset připravit svoji stránku, která uživatele na zranitelnou stránku teprve přesměruje nebo načte ve skrytém rámu ( iframe). Je tedy vyžadováno určité sociální inženýrství, ale získat jedno kliknutí od běžného uživatele zpravidla není tak složité (řekněme, že na stránku umístí vtipné video a odkaz rozšíří po sociálních sítích).

Podobné to bude u moderních (mnohdy tzv. single-page) aplikací, kde se parametry předávají v URL fragmentu za znakem #. Hlavní rozdíl je v tom, že útočný kód nebude přímo staticky umístěný ve stránce (v HTTP odpovědi serveru), ale je teprve vytvořen v objektovém modelu stránky (DOM) při interakci klientských skriptů a škodlivé hodnoty parametru. Představme si například aplikaci, která upravuje dynamicky své rozhraní pomocí vlastnosti innerHTML a vypisuje do stránky části URL. Pokud do URL útočník vloží element script a aplikace jej neošetří, dojde k jeho vykonání po tom, co oběť odkaz navštíví. Na rozdíl od ostatních typů XSS útok typicky vůbec neprojde přes server a je tak těžší jej aplikací detekovat či přímo odfiltrovat. Tento typ XSS se nazývá DOM-based (též XSS typu 0). Útočný kód navíc nemusí být jen v URL, ale do objektového modelu může být vložen třeba přesměrováním z útočníkovi stránky a to do hodnoty document.referrer (URL útočníkovi stránky) nebo do window.name přes HTML atribut target u odkazu, který oběť navštíví.

Nejnebezpečnější (i když nejnápadnější) formou je ale asi tzv. stored XSS (neboli XSS typu 2, též persistent XSS), kdy se útočníkovi podaří uložit kód na server tak, že je automaticky vkládán do některých stránek zranitelné webové aplikace. Uvažme například diskusní fórum, které ukládá a vypisuje do stránky příspěvky uživatelů bez další úpravy (např. aby zkušenější uživatelé mohli používat formátování). Útočníkovi pak stačí vložit kód do svého příspěvku a každý návštěvník daného vlákna nevědomky spustí cizí skript. Jako obrana rozhodně nestačí blokovat elementy script, způsobů vložení kódu jazyka JavaScript je mnoho. Tvůrce aplikace by například mohl povolit pouze vkládání elementu img, aby šly v příspěvcích používat obrázky. Útočník ale může použít třeba následující kód

<img src="cokoliv" onerror="alert(document.cookie)">

Při načtení stránky se prohlížeč pokusí stáhnout neexistující obrázek a vyvolá událost onerror, která spustí kód v atributu. Protože ten běží v kontextu uživatele, má také přístup třeba k jeho cookies s potenciálně citlivými daty. Zároveň existuje velké množství způsobů, jak kód zamaskovat, pokud se někdo snaží útoku předejít nesystematickým způsobem. Lze-li například použít jen alfanumerické znaky, skript se dá zakódovat pomocí String.fromCharCode (vytvoří řetězec ze znaků odpovídajícím předaným ASCII kódům) a následně spustit funkcí eval (provede řetězec předaný v parametru jako kód).

Pro úplnost ještě můžeme zmínit pojem self-XSS, který lze použít, když útočník zkrátka přesvědčí uživatele, aby si škodlivý skript sám pustil v prohlížeči. K tomu je třeba využít sociální inženýrství a skript například rozšířit na sociálních sítích s tvrzením, že po spuštění bude odemčena nějaká speciální funkce. Technicky se o XSS přímo nejedná, ale dopad je podobný. Prohlížeče proto omezili spouštění JavaScriptu z adresního řádku (adresa začínající na javascript:) a zobrazují varování při spouštění skriptů ve vývojářské konzoli, pokud jsou vkládané přes schránku. Naivního uživatele ale žádné technické prostředky zcela neuchrání a ani jako autoři aplikace nemůžeme udělat moc (např. Facebook ale jednu dobu vypisoval do konzole vlastní varování a dokonce úplně znemožňoval konzoli použít).

Možnosti útoku

Bezpečný prohlížeč by měl spuštění libovolného JavaScriptu ustát, jaký je rozdíl, když bude spuštěn na webu, který oběť běžně používá? Pokud jsme přihlášení ve webovém rozhraní aplikace, jiná stránka otevřená ve stejném prohlížeči k němu nemá standardně přístup. Může za to tzv. same-origin policy – skript může číst či měnit pouze stránky, které pocházejí ze stejné domény (a musí se shodovat i port a protokol). Pokud je naopak aplikace náchylná na XSS, skript může odeslat útočníkovi obsah zranitelné stránky (s citlivými údaji zobrazenými po přihlášení), stránku pozměnit či provádět požadavky jménem uživatele. Pokud by se například jednalo o email, lze si přečíst citlivé zprávy, pozměnit obsah (třeba nahradit odkaz na stažení nějaké aplikace za její malware verzi) nebo změnit konfiguraci (např. nastavit přeposílání kopie všech zpráv na útočníkovu adresu, aby získal přístup i k budoucím emailům).

Jednoduchým útokem bývala krádež cookie s identifikátorem session, který útočník mohl použít pro přístup ke stránce pod identitou oběti. Dnes už se pro uchování session id často už ve výchozím nastavení používá cookie s příznakem HttpOnly, který znemožňuje její čtení přes JavaScript. Typický příklad s odesláním document.cookie v parametru požadavku na server útočníka tak u udržovaných webů pravděpodobně nebude funkční. Na druhou stranu moderní single-page aplikace často ukládají autentizační tokeny přes web storage API (do sessionStorage či hůře localStorage), odkud jdou jednoduše vyčíst, jakmile se podaří spustit útočníkův kód na stránce. Dostupné jsou třeba také tokeny přidávané do formulářů jako skrytá pole kvůli ochraně proti CSRF (což je typ zranitelnosti na samostatný článek), čímž umožní vyvolat akce s identitou přihlášeného uživatele.

Další možností je použití nástroje jako BeEF. Ten umožňuje v přehledném grafickém rozhraní sbírat nachytané uživatele (kteří mají otevřenou nakaženou stránku) a nabízí připravené možnosti zneužití pomocí modulů. Je možné například provádět sociální inženýrství, které pro běžné uživatele není snadné prokouknout. Jeden scénář zobrazí dialog vybízející k opětovnému přihlášení (údaje samozřejmě putují k útočníkovi), jiné zase nutí k aktualizaci software (čímž lze podstrčit malware). Vše se přitom odehrává na známé důvěryhodné doméně (klidně zabezpečené EV certifikátem).

Je také možné provést útok typu man in the browser, kdy po kliknutí na libovolný odkaz ve skutečnosti nedojde ke změně adresy a nová stránka se načte jen uvnitř zranitelné stránky, takže útočník má stálý přístup (dokud oběť nepřepíše adresní řádek prohlížeče). Pokud na zranitelné stránce není pro útočníka nic zajímavého a zároveň předpokládá uživatele odolného proti sociálnímu inženýrství, může jeho prohlížeč aspoň využít k průzkumu lokální sítě či přímo nalezení špatně zabezpečeného stroje (nepřístupného z venku), na útok by se hodil třeba firemní web zranitelný na stored XSS.

Obrana

Primární obranou před XSS je důsledná sanitizace dat vkládaných do stránky, např. převedení všech problematických znaků na HTML entity (jako < na &lt;), aby se neinterpretovala se speciálním významem. Velmi důležité je rozlišovat kontext, do kterého data vkládáme (dovnitř specifického tagu či atributu, přímo část skriptu apod.) Některým kontextům je třeba se úplně vyhnout, protože obecně neexistuje způsob, jak data přes všechna možná kódování ošetřit. Prevenci pro různé případy komplexně popisuje OWASP.

Ošetření je nejlepší provádět těsně před vložením do stránky, čímž snadno ověříme, že výstupní zakódování je provedeno právě jednou (resp. ve zvláštních případech právě n-krát). Už jsme také uvedli nějaké techniky, které jsou naopak nedostačující, obecně to jsou různé pokusy detekovat útok pomocí blacklistu. Je vhodné použít kvalitní knihovnu, která ponechá pouze vyjmenované bezpečné symboly a zbytek zakóduje podle kontextu. Také existují celé frameworky, co slibují, že chybu vyřeší za nás, to je ale vždy nějak podmíněno a je nutné je používat správným způsobem.

Specifickou obranu vyžaduje XSS typu DOM-based. Jakmile se do hry zapojí JavaScript, nemusí stačit sanitizace HTML, protože při manipulace s objektovým modelem automaticky dochází k dekódování. Jednoduché pravidlo je ve spojitosti s nedůvěryhodnými daty nepoužívat vlastnost innerHTML, ale pouze innerText nebo textContent. Pozor si také musíme dát na funkce jako document.write nebo ty, které někde uvnitř používají eval (jako setTimeout). Opět si musíme pohlídat kontext, např. použití funkce setAttribute je bezpečné jen pro vybrané HTML atributy. Samostatný návod od OWASP popisuje ještě řadu dalších opatření.

Ve spojitosti s XSS také musíme zmínit Content security policy (CSP). Jedná se o HTTP hlavičku, kterou lze specifikovat řadu direktiv pro úpravu chování prohlížeče především v souvislosti s načítáním obsahu z cizích domén. Jejím cílem je omezit dostupnost technik XSS a dalších útoků (tzv. clickjacking a zneužití nezabezpečených části stránky v případě tzv. mixed content). Snadno lze například nakonfigurovat, aby stránka povolila externí skripty jen z několika vyjmenovaných domén, obrázky či videa odkudkoliv a zbylý obsah (styly, fonty, vložené objekty apod.) pouze z vlastní domény.

Kromě toho ve výchozím nastavení zakazuje nebezpečné funkce jako eval a také použití tagů script a style tzv. inline, takže všechen JavaScript a CSS musí být v externích souborech. Protože inline skripty jsou ale stále dost využívané, existují dva mechanismy, jak vybranému kódu udělit výjimku. První způsob je k inline elementům script generovat při každém načtení stránky náhodnou hodnotu atributu nonce a tu zároveň posílat v hlavičce CSP. Druhý způsob je uvést v CSP kryptografické haše těchto skriptů. Direktiva strict-dynamic ještě umožní přenést důvěru i na skripty, které jsou volány těmi s explicitní důvěrou.

CSP je mocný aparát, bohužel ale v praxi není tak efektivní, jak by se mohlo zdát. V první řadě je evidentně nutné omezit domény pro načítání skriptů a zároveň i objektů (element object), které mohou rovněž obsahovat JavaScript. Nesmíme ale povolit přidat nový inline skript, dokonce i kdyby byly zakázány všechny zdroje z cizích domén. Sice by pak útočník nemohl jednoduše odeslat data na svoji doménu, proč je ale třeba nepřidat jako komentář ve veřejné části webu, kde si je pak vyzvedne?

Nebo může změnit některé nastavení, které mu umožní se k datům dostat, případně mu nemusí jít o data, ale o provedení privilegované akce (řekněme záměna čísla účtu při odeslání platby v bankovnictví) či jiné útoky, jak už jsme si vysvětlovali. A pokud by přece jen potřeboval svoji doménu, stačí pozměnit cíl formuláře (i když to se dá také v CSP zakázat) nebo přimět oběť kliknout na odkaz (např. překryje stránku reklamou a text „zavřít“ bude odkaz s uniklými daty v parametrech).

Zákaz externích i inline skriptů ovšem stále nestačí, lze totiž využít funkcionality použitých legitimních knihoven. Problémy působí například technika JSONP, která vrací data obalená ve funkci určené parametrem externího skriptu, což lze buď zneužít ke spuštění kódu přímo, nebo v případě kontroly znaků pro útok zvaný SOME. Některé legitimní knihovny zase mají své šablony, které interpretují jako JavaScript, takže útočníkovi stačí vložit kód v této podobě a o jeho vykonání se postará skript ze schválené domény.

Podle studie Googlu z roku 2016 lze téměř 95 % CSP triviálně obejít (nejčastěji kvůli povoleným inline skriptům. V případě, že počítáme jen na první pohled bezpečné „striktní“ CSP, stále se jich automaticky podařilo obejít více než polovinu (hlavně kvůli JSONP a dostupnosti knihovny AngularJS). Problémům se vyhneme použitím direktivy nonce v kombinaci se strict-dynamic pro snadné nasazení a zakázáním base-uri kvůli útoku přesměrováním přes HTML element base. Stejně ale existují případy, kdy se i takovou politiku podaří obejít, ale to už opravdu přesahuje tento článek, na CSP zkrátka nemůžeme spoléhat na 100 %.

Kromě CSP existuje ještě několik způsobů, jak útok ztížit, popř. zmírnit dopad. Už jsme zmiňovali cookies s atributem HttpOnly, které nejsou přístupné přes JavaScript. Aplikace, které používají tokeny neukládané v cookies, mohou do HttpOnly cookie uložit náhodnou hodnotu a do tokenu přidat její kryptografický haš. Při kontrole tokenu pak bude server vyžadovat ještě hodnotu náhodné cookie a jednoduchá krádež tokenu pak pro kompromitaci nestačí.

Některé prohlížeče také dokáží zablokovat zjevné XSS i ve zranitelné aplikaci, pokud se podezřelá část požadavku vyskytuje v odpovědi serveru, z principu ale nemohou vůbec bránit zneužití persistentního XSS (protože nemají podle čeho rozhodnout, zda je kód předaný serverem legitimní). Chování filtru může aplikace ovlivnit hlavičkou X-XSS-Protection. Nejen že se nachází způsoby, jak ochranu obejít, ale někdy může taková kontrola dokonce vyvolat zranitelnost. Na straně serveru lze nasadit webový aplikační firewall, také se ale obvykle najde způsob, jak ho ošálit, a proti DOM-based XSS je zcela bezmocný. Všechny tyto obrany mohou útokům zabránit, ale je vhodné je používat jen jako přídavnou vrstvu obrany, defense in depth.

Dnes jsme si ukázali, jak vzniká chyba XSS a jak ji lze zneužít i přes některé obrany. Stejně jako v předchozích článcích se jedná o tradiční typ zranitelnosti, který se stále nedaří eliminovat. Na jedné straně máme frameworky s automatickým ošetřením parametrů a stále se rozvíjející CSP, na druhé to jsou aplikace s čím dál komplexnějším skriptováním na straně klienta, kde je obtížné ohlídat bezpečnost veškerých interakcí. V příštím článku trochu odlehčíme a podíváme se na třídu zranitelností, které jsou méně nebezpečné, ale i méně známé, a mohou nás překvapit.