Kilka słów o podatności XSS oraz polyglot XSS

17 sierpnia 2017, 10:50 | Bez kategorii | komentarze 4
: zin o bezpieczeństwie - pobierz w pdf/epub/mobi.

Cross-Site Scripting (XSS) to jedna z najczęściej występujących podatności w aplikacjach napisanych przy użyciu języka JavaScript i nie dotyczy to jedynie stron i aplikacji internetowych. Dotyka ona coraz częściej aplikacje desktopowe, rozszerzenia dostępne w przeglądarkach internetowych, interfejsy w urządzeniach IoT i nic nie wskazuje na to, by ten rodzaj zagrożenia w najbliższej przyszłości miał zniknąć. Wręcz przeciwnie, nieznajomość i bagatelizowanie zagrożenia, jakie niesie ze sobą XSS, niedostateczna wiedza programistów jak zapobiegać tego rodzaju atakom, a także powszechny w dzisiejszym świecie pośpiech w wdrażania nowych aplikacji i ich „ulepszonych” wersji sprawia, że natrafienie na podatność XSS nie jest niczym szczególnym. 

Na czym polega podatność XSS? W największym skrócie możemy ją opisać jako możliwość osadzenia dowolnego kodu JavaScript w kodzie aplikacji w taki sposób, aby został wykonany. Najczęściej taki kod zostaje „wstrzyknięty” bez ingerencji i wiedzy użytkownika (np. poprzez kliknięcie odpowiednio spreparowanego adresu URL ), zostaje pobrany z bazy danych, usługi sieciowej bądź bezpośrednio np. z formularza HTML.

Taki kod następnie zostaje „dodany” do oryginalnego kodu aplikacji i zostaje potraktowany przez przeglądarkę (bądź inny mechanizm wyświetlający kod HTML lub interpreter JavaScript) jako prawidłowy kod i wykonany.

Zobaczmy najprostszy przykład. Poniższy fragment źródła strony www wyświetla identyfikator użytkownika, który zostaje przesłany jako parametr w adresie url (Listing 1.)

Zgodnie z założeniem autora, link w postaci http://website.com?username=Adam powinien zostać przetworzony przez serwer i odesłany do przeglądarki w następującej postaci:

Co jednak wydarzy się gdy link będzie wyglądał np. tak?

W rezultacie wynikowy kod HTML zostanie zwrócony w postaci:

Gdy użytkownik otworzy tak spreparowany link, jego przeglądarka zamiast wyświetlić spodziewaną nazwę użytkownika wykona funkcję alert('Got XSS?'), a oczom zaskoczonego internauty ukaże się popup z komunikatem „Got XSS?”. Ciąg <script>alert('Got XSS?')</script> w terminologii związanej z bezpieczeństwem IT nazywany jest „payloadem”.

Ponieważ w serwisie Sekurak artykuły na temat ataków typu Cross-Site Scripting pojawiały się wielokrotnie, zachęcam do zapoznania się z ich treścią, a ja przejdę do tematu tego artykułu, którym są payloady wykonujące się w wielu tzw. kontekstach wykonania – najpierw zobaczmy, czym są owe konteksty.

Konteksty wykonania XSS

W zależności od tego, gdzie w kodzie aplikacji webowej znajdzie się nasz payload, będzie musiał spełniać odpowiednie reguły składniowe, by zostać wykonanym. Brzmi to bardzo enigmatycznie, ale za chwilę okaże się proste i zrozumiałe :).

W poniższych przykładach zapis {{payload}}  reprezentuje miejsce w kodzie, w którym serwer wygeneruje to, co przesłaliśmy do niego jako wartość zmiennej GET (jak w przykładzie w poprzednim rozdziale).

Kontekst 1 – znacznik HTML

Pierwszym kontekstem wykonania wstrzyknięcia XSS jest kod w postaci znacznika HTML. Rozważmy następujący fragment:

By wykonać kod JavaScript w tym miejscu, potrzebujemy otwierającego znacznika <script> , następnie fragmentu JavaScript do wykonania oraz zamykającego znacznika </script> , choć nie zawsze jest on konieczny, gdyż możemy w tym celu wykorzystać znacznik zamykający znajdujący się już gdzieś dalej w kodzie:

Finalnie, kod wygenerowany przez przeglądarkę przybierze następującą postać:

Kontekst 2 – atrybut znacznika HTML

Kolejnym kontekstem (i jednym z najpopularniejszych) jest wartość atrybutu znacznika HTML. Poniższy fragment kodu prezentuje podatność XSS w atrybucie  value  znacznika <input>  w polu wyszukiwarki w serwisie internetowym (jest to jedno z najczęściej spotykanych miejsc gdzie szuka się podatności XSS w aplikacji internetowej i jest to jednocześnie to miejsce, gdzie dużo aplikacji tę podatność posiada):

W takim miejscu nasz payload musi spełniać kilka warunków, by zostać wykonanym. Pierwszym z nich jest cudzysłów zamykający oryginalną wartość atrybutu  value, a następnie coś, co wywoła kod JavaScript, np. funkcja zwrotna obsługi jakiegoś zdarzenia DOM ( onmouseoveronclick itp.). Aby zachować prawidłową strukturę HTML, możemy dodać jakiś znacznik otwierający, by uzupełnić lukę, którą stworzyliśmy naszym payloadem. Mimo iż nie jest to konieczne, często zachodzi potrzeba „ukrycia” payloadu. Jeśli nie zadbamy o odpowiednią strukturę kodu, wygląd strony www może zostać zaburzony:

W rezultacie kod HTML przybierze następującą postać:

Gdy użytkownik kliknie w pole znacznika  input, np. w celu wprowadzenia hasła do wyszukania, wykona się kod zawarty w funkcji zwrotnej zdarzenia  onclick. To jednak nie jest gwarantowane (nie zawsze użytkownik skorzysta z wyszukiwarki), dlatego najczęściej stosuje się prosty trik, mający na celu uruchomić wstrzyknięty kod JavaScript automatycznie. Trik polega na dodaniu znacznika np. <img>  z nieprawidłową ścieżką w atrybucie  src oraz kodem obsługi zdarzenia  onerror  (który wykonywany jest, gdy obrazek nie może zostać załadowany np. z powodu błędu HTTP 404 Not Found ):

Nasz wynikowy kod przyjmie zatem następującą postać:

W omawianym kontekście ważną rzeczą jest rodzaj cudzysłowów, jakie zostały użyte (pojedyncze bądź podwójne) i w zależności od tego użycie odpowiedniego z nich jako pierwszy znak rozpoczynający nasz payload (pojedynczy cudzysłów nie „zamknie” atrybutu  value , w przykładzie powyżej – zostanie potraktowany jako pierwszy znak ciągu będącego jego wartością. Podobnie potraktowany zostanie podwójny cudzysłów, gdy programista użył pojedynczych).

Kontekst 3 – komentarz HTML

Następnym omawianym kontekstem będzie komentarz HTML:

W takim wypadku jedyne, co musimy zrobić, to zamknąć komentarz i dodać kod HTML wraz z JavaScript, który ma zostać wykonany:

W tym wypadku możemy pominąć „poprawianie” składni za naszym payloadem (zostanie tam oryginalne zamknięcie komentarza). Przeglądarka potraktuje to jako ciąg -->, który ma po prostu zostać wyświetlony w treści strony:

Warto w tym miejscu nadmienić, że w standardzie HTML5 sekwencja znaków --!> także może zostać  użyta do zamknięcia komentarza.

Zachowanie takie spowodowane jest faktem, że silniki renderujące przeglądarek internetowych działają w taki sposób, by wygenerować jak najwięcej kodu HTML z tego, co otrzymają od serwera i nie zgłaszają absolutnie żadnych błędów (w HTML nie istnieje coś takiego, jak błąd składniowy). To powoduje, że nie są one odporne na takie wstrzyknięcia, jak zaprezentowane powyżej. Ich jedyną obroną może być wbudowane zabezpieczenie przed atakami XSS (np. XSS Auditor w przeglądarkach opartych na silniku WebKit i jego forku Blink, czyli Safari, Chrome, Opera i Chromium), ale to zupełnie inny temat i nie będziemy się nim tutaj zajmować.

Kontekst 4 – definicje styli CSS

Dwoma kolejnymi kontekstami, których exploitacja wygląda analogicznie jak w przypadku wartości atrybutu znacznika HTML, są konteksty związane ze stylami CSS:

oraz

Tutaj w zależności od tego, który jest to kontekst, nasz payload musi albo „uciec” z atrybutu, np.:

co w rezultacie zaowocuje kodem:

lub (w drugim przypadku) zamknąć znacznik <style> , „wstrzyknąć” kod do wykonania i ponownie go otworzyć (opcjonalnie):

co prowadzi z kolei do takiego fragmentu HTML:

Kontekst 5 – atrybut „href” znacznika HTML <a>

Bardzo popularnym kontekstem jest atrybut href znacznika a zaskakująco często linki do dokumentów HTML budowane są w oparciu o to, co znajduje się w parametrach GET aktualnego adresu url (np. wartości przekazywane do paginacji) URL w postaci np.  http://strona.com/index.php?page=10&view=40&sort=desc  jest wyświetlany np. w paginacji na stronie jako: 

Jeśli jesteśmy w stanie dodać nasz payload do jednej z wartości przekazywanych jako parametr w GET (page, view lub sort), możemy zastosować podobną technikę, jak w przypadku regularnego atrybutu znacznika HTML:

Rezultat:

Innym popularnym sposobem wstrzyknięcia kodu w tym miejscu jest użycie pseudo protokołu  javascript:  jako wartość atrybutu href (innym pseudo protokołem jest np. mailto: , który otwiera domyślnego klienta poczty):

Jego działanie polega na wykonaniu kodu JavaScript, który znajduje się po dwukropku, po kliknięciu w Click me!  – dokładnie tak samo, jak ma to miejsce w przypadku kliknięcia w regularny url, co powoduje przejście do strony znajdującej się pod adresem podanym jako wartość atrybutu href .

Kontekst 6 – kod JavaScript

Ostatnim omawianym kontekstem jest ten, który znajduje się bezpośrednio w samym kodzie JavaScript:

W tym wypadku musimy przesłać prawidłowy kod JavaScript (tutaj w grę wchodzą już reguły składniowe i każdy błąd, jaki spowodujemy, przerwie działanie nie tylko kodu JavaScript w miejscu wstrzyknięcia, ale także całego kodu znajdującego się po nim, w danym znaczniku <script>  bądź pliku JavaScript:

Nasz payload spowoduje, że przedstawiony wcześniej fragment, po wygenerowaniu przez serwer będzie wyglądał następująco:

Zwróć uwagę na znak komentarza na końcu payloadu: //  – jego zadaniem jest, by silnik JavaScript przeglądarki zignorował wszystko, co znajduje się za payloadem. Czemu jest to ważne? Gdyby nasz payload nie posiadał znaku komentarza, kod wynikowy wyglądałby następująco:

Przeglądarka zgłosiłaby błąd składniowy ( Uncaught SyntaxError: Invalid or unexpected token ), a  kod nie wykonałby się:

W przypadku, gdy nie jesteśmy w stanie wstrzyknąć prawidłowo działającego payloadu np. z powodu filtrowania znaku "  (nasz payload "; alert('Got XSS?'); //  w tym wypadku nie zadziała), możemy spróbować „uciec” z tagu <script>  i wstawić nasz własny fragment.

I ponownie kod przed wstrzyknięciem (Listing 25):

Nasz payload:

I końcowy rezultat:

Polyglot XSS Payload – wprowadzenie

Po zapoznaniu się z kontekstami wykonania widać wyraźnie, że żaden z payloadów nie może zostać użyty w każdym z nich i zostać wykonany, gdyż jego reguły składniowe i sposób „ucieczki” z miejsca wstrzyknięcia (zamknięcie cudzysłowu, komentarza, znacznika itp.) jest charakterystyczny dla jednego, góra dwóch czy trzech kontekstów.

Czy istnieje zatem sposób, by przygotować jeden, uniwersalny payload, który wykona się prawidłowo w każdym kontekście i jednocześnie spełni reguły każdego z nich? Okazuje się, że jest to jak najbardziej możliwe. Tego rodzaju payloady nazywane są Polyglot XSS Payloads i służą do detekcji błędów XSS w nawet kilkunastu różnych kontekstach wykonania jednocześnie.

Zanim przejdziemy do omówienia działania takiego payloadu oraz sposobu jego budowy, przykład praktyczny:

Przykład pochodzi ze strony http://polyglot.innerht.ml, na której można zgłosić swój własny Polyglot XSS – do „złamania” jest szesnaście kontekstów jednocześnie (są to warianty kontekstów przedstawionych w poprzednim rozdziale), a wygrywa ten, który składa się z najmniejszej ilości znaków (powyższy payload działa we wszystkich 16 kontekstach i ma jedynie 87 znaków długości – gdy porównamy to z przykładami z poprzedniego rozdziału, stopień „zagęszczenia” i efektywność są imponujące).

Zobaczmy w praktyce jak wygląda jego działanie.

Poniżej znajduje się prosty dokument HTML wraz z zaznaczonymi miejscami, gdzie pojawi się wstrzyknięty kod (źródło: http://polyglot.innerht.ml). To lista kontekstów, w których uruchamiany jest payload:

Zobaczmy teraz, jak prezentuje się powyższy HTML gdy do każdego z kontekstów zostanie wstrzyknięty przykładowy Polyglot XSS Payload:

Jak możemy zauważyć, payload w każdym z kontekstów wywołał nieco „zamieszania” zaśmiecając go znakami //  lub nadmiarowymi znacznikami, ale po przeanalizowaniu każdej linijki możemy szybko odkryć, że jest to w 100% prawidłowy kod HTML (bądź JavaScript).

Polyglot XSS Payload – budujemy uniwersalny payload krok po kroku

Aby jak najlepiej zrozumieć w jaki sposób działa omawiana technika, korzystając z przykładowych kontekstów zbudujemy nasz własny, uniwersalny payload. Nie będziemy tutaj skupiać się na jak najmniejszej ilości znaków ani też szukać poprawnego rozwiązania dla wszystkich szesnastu kontekstów – skupimy się na stworzeniu payloadu, który będzie działał w kilku najważniejszych i najpopularniejszych (jako treść tagu HTML, wartość atrybutu z użyciem podwójnego i pojedynczego cudzysłowu, atrybutu href znacznika a, znacznika stylu, skryptu JavaScript i komentarza HTML).

Oto nasz przykładowy dokument HTML:

Jeśli chcecie wykonać poniższe ćwiczenie na swoim komputerze, jednym ze sposobów może być prosty skrypt PHP, który będzie wstawiać payload pobrany ze zmiennej GET z adresu url i uruchomienie go z wykorzystaniem PHP jako serwera (poniższy kod należy zapisać  jako plik PHP, np. index.php):

Aby uruchomić wbudowany w interpreter PHP serwer WWW w katalogu, w którym zapisaliście powyższy skrypt wykonajcie następujące polecenie (dla systemów Linux i macOS):

Jeśli korzystasz z systemu Windows, wykonajcie odpowiednie czynności właściwe dla instalacji serwera Apache i interpretera PHP, jaki znajduje się na waszym komputerze.

Zanim przystąpimy do testowania naszego payloadu, musimy upewnić się, że wbudowane w przeglądarki z silnikiem Blink i WebKit zabezpieczenia w postaci XSS Auditora są wyłączone (w przeciwnym wypadku nie będziemy w stanie zaobserwować efektów naszej pracy, gdyż będą one blokowane). Nie będziemy tutaj skupiać się na sposobach obejścia XSS Auditora (bo jest to temat zasługujący na odrębne opracowanie).

Najprostszym sposobem jest uruchomienie przeglądarki Chrome, Chromium, Safari bądź Opery z linii poleceń z argumentem --disable-xss-auditor:

Alternatywą może być także skorzystanie z przeglądarki Firefox, która jako jedyna (w dniu pisania tego artykułu) nie jest wyposażona w żadne zabezpieczenie przeciwko atakom XSS.

Po „zneutralizowaniu” XSS Auditora (bądź w Firefox) możemy już przejść do adresu:

W ten sposób przygotowaliśmy mały poligon, na którym będziemy sprawdzać nasze postępy.

Przystąpmy zatem do dzieła. Zaczniemy od najprostszego kontekstu, czyli wstrzyknięcia w postaci znacznika HTML:

Sprawdźmy, czy działa w pierwszym kontekście:

Super! XSS w pierwszym kontekście zadziałał zgodnie z oczekiwaniem. Pora na wartość atrybutu HTML. Gdy przyjrzymy się źródłu HTML, możemy zauważyć, że musimy „zamknąć” atrybut class znacznika div w linijce 5, aby nasz payload się wykonał. A zatem nasz payload będzie miał teraz następującą postać:

W rezultacie otrzymamy:

W linijce 5 widzimy spodziewany efekt, a nasz payload działa już w dwóch kontekstach. Podobnie musimy postąpić w linijce 6, ale tam zamykamy wartość atrybutu pojedynczym cudzysłowem. Zauważcie, że nie musimy ponownie użyć >, wystarczy, że uzupełnimy payload o znak '  (pojedynczy cudzysłów):

Kolejny etap osiągnięty:

Pora na znacznik <style>. Jak pisałem  wcześniej, musimy najpierw „uciec” z niego dodając znacznik zamykający. Nadal możemy pozostawić resztę payloadu:

Dzięki naturze HTML nie musimy przejmować się osieroconym znacznikiem </style>  w innych kontekstach – przeglądarka po prostu go zignoruje:

 

Na tym etapie nasz payload wyświetla już okienko z jedynką pięć razy (w linijkach 4,5,6,7 oraz 8).
Dodajmy teraz pseudo protokół  javascript jako wartość atrybutu href  znacznika a  w linijce 8:

Zwróćcie uwagę na to, jak znakami komentarza uniknęliśmy błędu składniowego po instrukcji javascript:alert(1)  jeśli jesteście ciekawi, jak zachowa się payload bez niego, usuńcie go a następnie kliknijcie w wygenerowany link.

Przedostatnim kontekstem jest komentarz HTML. Ponieważ kod wyświetlający popup mamy już gotowy, wystarczy jedynie w odpowiednim miejscu umieścić znacznik zamykający komentarz:

Efektem będzie oczywiście kolejny popup, wywołany w linijce 9:

Ostatnim z kontekstów, który pozostał nam do do wyeksploitowania, jest kod JavaScript wstawiany bezpośrednio do znacznika <script>...</script> w linijce 10.

Mamy tutaj jednak mały problem w postaci znaku komentarza przed kodem (symuluje to sytuację, gdy programista wyciął „niebezpieczny” fragment w produkcyjnym kodzie). Gdyby nie komentarz, zadziałałby payload dla atrybutu  href, czyli fragment  javascript:alert(1) .

Musimy więc znaleźć sposób, by „uciec” z komentarza.

Ważnym aspektem w tym miejscu jest to, że komentarz jest jednolinijkowy, w przeciwieństwie do wielolinijkowego, rozpoczynającego się sekwencją /* . W przypadku komentarza wielolinijkowego wystarczy użycie sekwencji zamykającej */  i zadziała ona nawet w tej samej linii, w której znajduje się sekwencja otwierająca:

// foo(); // alert(1) – to nie zadziała, wszystko po znaku //  traktowane jest jako komentarz

/* foo(); */ alert(1) – to zadziała; komentarz obejmuje jedynie wywołanie funkcji foo()

W przypadku komentarza jednolinijkowego nie ma takiej możliwości. Musimy jakoś sprawić, by payload znalazł się w następnej linijce. Osiągniemy to używając znaku przejścia do nowej linii (znak o kodzie ASCII 0A , czyli „line feed”) przed drugim wywołaniem funkcji alert(), a w celu zapobieżenia błędom składniowym w dalszej części skryptu zamkniemy go kolejnym znakiem komentarza jednolinijkowego:

Rezultatem będzie już osiem kolejno otwierających się alertów oraz dziewiąty, który zadziała gdy klikniemy na link text (wygenerowany w linijce 8 naszego HTML-a).

Zwróćcie uwagę jak po ostatniej modyfikacji zmienił się kod HTML. Przejścia do nowej linii nie popsuły działania naszego payloadu w żadnym z pozostałych kontekstów (przeglądarka nie dba zupełnie o wpływ znaków takich jak spacje, nowa linia, tabulator – chyba, że wypadną one w środku nazwy znacznika, np. <di v> ze spacją pomiędzy i i v jest nieistniejącym tagiem HTML).

Natomiast w kodzie JavaScript sztuczka ze znakiem %0a  zaowocowała oczekiwanym efektem ucieczki z komentarza jednolinijkowego – linia 16 pozostała komentarzem, ale nowa linia (17) zawiera już tylko fragment po znaku %0a  oraz dodany kolejny znacznik //  komentarza tak, aby to, co znajduje się dalej aż do znacznika zamykającego </script>  zostało zignorowane i nie spowodowało błędu składni JavaScript w tym miejscu:

Tak oto, krok po kroku, stworzyliśmy Polyglot XSS Payload, który zadziała w ośmiu kontekstach, nie generując przy tym żadnych efektów ubocznych ani błędów w konsoli przeglądarki:

Jako ćwiczenie proponuję próbę stworzenia payloadu, który zadziała we wszystkich szesnastu kontekstach użytych na stronie http://polyglot.innerht.ml

Podsumowanie

Skonstruowany powyżej payload to bardzo użyteczne i efektywne narzędzie do wykrywania potencjalnych błędów XSS, ale nie tylko w tej technologii (HTML) się ich używa. Na dokładnie takiej samej zasadzie działają np. Polyglot SQL Injection Payload, których zadaniem jest wykrywanie podatności typu SQL Injection w wielu kontekstach i systemach bazodanowych jednocześnie.

Należy pamiętać, że Polyglot XSS Payloads nie są sposobem na pokonanie zabezpieczeń przed atakami XSS. Jeśli na naszej drodze stanie WAF (Web Application Firewall) z odpowiednio przygotowanymi sygnaturami, XSS Auditor, prawidłowo przetworzony przez serwer kod HTML bądź inne zabezpieczenie – nasz payload nie zadziała.

Z drugiej strony w jego przygotowaniu ogranicza nas jedynie wyobraźnia (i reguły składniowe JavaScript). Zamiast znacznika <img>  można użyć np. <svg>  lub dość często niefiltrowanych przez WAF-y <video>  i <audio>  (właśnie z użyciem tych dwóch ostatnich udało mi się ominąć zabezpieczenia przed XSS w jednym z serwisów internetowych, który padł moim łupem w programie bug bounty firmy General Motors w serwisie HackerOne). Gdy dodamy do tego kilkanaście sposobów, którymi możemy zakodować nasz payload (url encoding, UTF-8, hex encoding, encje HTML itd.) – możliwości są nieograniczone.

Źródła

 

— Rafał ‚bl4de’ Janicki

Spodobał Ci się wpis? Podziel się nim ze znajomymi:



Komentarze

  1. t

    super!

    Odpowiedz
  2. adam

    Ekstra artykuł. Dzięki :)

    Odpowiedz
  3. Tony Hołk

    Czy tylko ja mam wrażenie, że punkty 9 i 11 z listy Polyglotów są identyczne?

    Odpowiedz
    • Michał Bentkowski

      Tak, są identyczne, ale – o ile pamięć mnie nie myli – różne sposoby enkodowania były stosowane w obu przypadkach. Z jednego dało się uciec przez zamknięcie tagu script, a w drugim było to zablokowane.

      Odpowiedz

Odpowiedz