Skracarki i wklejarki URL cz.II – Legere librum Necronomicon
Napisał: Patryk Krawaczyński
12/01/2019 w Bezpieczeństwo, Pen Test Brak komentarzy. (artykuł nr 675, ilość słów: 3411)
W
poprzedniej części zebraliśmy i przygotowaliśmy do wstępnej obróbki zbiór danych pochodzący z skracarek URL. W tej części zajmiemy się jego umieszczeniem w silniku wyszukiwania, który umożliwi nam bardzo szybkie pozyskiwanie interesujących nas informacji. Jak wcześniej wspomnieliśmy na naszej maszynie wirtualnej uruchomiony jest system Ubuntu 18.04 LTS. Sama maszyna ma 2 CPU / 4 GB RAM oraz 100 GB dysku z czego 57 GB jest już zajęte przez ściągnięte, skompresowane dane. W tej części artykułu interesuje nas stworzenie następnego przepływu danych:
[/data/archive/*/*/*.txt] --> [logstash] --> [elasticsearch] --> [kibana]
Źródłem danych będą rozpakowane pliki .txt – za ich pozyskiwanie, modyfikację i dalszą wysyłkę będzie odpowiadał logstash; sercem naszego OSINT będzie elasticsearch. Jego zadaniem jest indeksowanie danych w określony przez nas sposób. Po tej czynności będziemy w stanie je przeszukiwać za pomocą prostych, jak i tych bardziej zaawansowanych zapytań. Wszystko to zostanie przykryte GUI, za które będzie odpowiedzialna kibana. Na potrzeby demonstracji będziemy używać wersji 6.1.4, ale prezentowane rzeczy i konfiguracje bez problemu powinny działać na każdej wersji z gałęzi 6.x. Instalację zaczynamy od Java SE Runtime Environment 8:
sudo add-apt-repository ppa:webupd8team/java sudo apt update sudo apt install oracle-java8-set-default
Następnie zainstalujemy wszystkie trzy komponenty stosu ELK, ale jeszcze nie będziemy uruchamiać jako serwisu żadnego z nich:
wget https://artifacts.elastic.co/downloads/logstash/logstash-6.1.4.deb wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.1.4.deb wget 'https://artifacts.elastic.co/downloads/kibana/kibana-6.1.4-amd64.deb' dpkg -i logstash-6.1.4.deb dpkg -i elasticsearch-6.1.4.deb dpkg -i kibana-6.1.4-amd64.deb
Modelowanie danych:
Przed umieszczeniem danych w E)elastic S)earch musimy je odpowiednio przygotować. Określić jaką mają mieć strukturę, schemat, format itd. Typowy, reprezentatywny i najczęściej pojawiający się przykład z naszego zbioru danych to:
100AQo|http://subdomena.domena.tld/ścieżka/index.cfm?parametry&zapytania
W celu ułatwienia i przyśpieszenia przeszukiwania informacji po konkretnych obszarach rozbijemy powyższą linię na następujące komponenty:
- uri_short –
100AQo
– hash pochodzący z oryginalnego serwisu do skracania. Na podstawie ścieżki pliku, która również będzie przesyłana do naszego systemu w polu source będziemy w stanie określić pochodzenie oryginalnego URL, który został złamany np./data/archive/urlteam_2017-02-01-19-17-07/bitly_6
=http://bit.ly/100AQo
. - uri_full –
http://subdomena.domena.tld/ścieżka/index.cfm?parametry&zapytania
, czyli cała reszta, która znajduje się za znakiem potoku"|"
. W tej części znajduje się najwięcej interesujących nas informacji. Dlatego ten komponent rozbijemy sobie jeszcze na kilka mniejszych. - uri_protocol –
http
– protokół, jaki został użyty w komunikacji (znaki od potoku “|” do “://”). - uri_domain –
subdomena.domena.tld
– czyli serwis / aplikacja internetowa, do której odwołuje się adres URL. Nie będziemy tutaj rozbijać go na wszystkie czynniki pierwsze: subdomena, domena, tld (zrobi to za nas elasticsearch, ale o tym później) tylko wyciągniemy sobie z niego domenę najwyższego poziomu (pole: uri_topdomain). Pozwoli nam to filtrować konkretne organizację, kraje, komercyjne serwisy itd. - uri_path –
/ścieżka/index.cfm
– tutaj wyodrębnimy sobie ścieżki dostępu do zasobów. - uri_params –
?parametry&zapytania
– ciąg zapytania, który przekazuje dodatkowe dane do strony docelowej. Zapytania mają format nazwa=wartość, a pierwszy ciąg zapytania w adresie URL poprzedzony jest znakiem “?” (znakiem zapytania). Przed kolejnymi ciągami zapytania umieszczone są znaki “&” (ampersand). Ciągi zapytania są często nazywane parametrami, bo służą do umieszczania parametrów w kodzie uruchamianym na stronie docelowej. To ta część najczęściej jest testowana pod względem różnych ataków.
Skoro posiadamy już plan rozłożenia naszych danych na różne pola i ich wartości – czas skonfigurować program logstash, aby wykonał wszystkie nasze w/w założenia. Pierwszym krokiem będzie stworzenie pliku z wzorami dla filtra grok. Grok jest świetnym filtrem do parsowania niestrukturalnych danych w coś uporządkowanego i możliwego do uruchamiania kwerend.
mkdir /etc/logstash/patterns chmod 755 /etc/logstash/patterns touch /etc/logstash/patterns/nfpatterns chmod 644 /etc/logstash/patterns/nfpatterns
Plik nfpatterns powinien posiadać następującą zawartość:
NFPROTO [A-Za-z]([A-Za-z0-9+\-.]+)+ NFDOMAIN \b(?:[0-9A-Za-z][0-9A-Za-z-]{0,62})(?:\.(?:[0-9A-Za-z][0-9A-Za-z-]{0,62}))*(\.?|\b) NFPATH (?:/[A-Za-z0-9$.+!*'(){},~:;=@#%&_\-]*)+ NFPARAMS \?[A-Z a-z0-9$.+!*'|(){},~@#%&/=:;_?\-\[\]<>]* NFALL %{NFPROTO:uri_protocol}://%{NFDOMAIN:uri_domain}(?:%{NFPATH:uri_path})?(?:%{NFPARAMS:uri_params})?
Mimo, że logstash posiada już około 120 gotowych wzorów do obsługi różnych formatów logów to my na ich podstawie stworzyliśmy własne i dostosowaliśmy je do swoich potrzeb. Na przykład NFPARAMS
jest poprawioną wersją URIPARAM
, która uwzględnia możliwość występowania “czystej spacji” (nie jako kod URI: %20
) w parametrach. Posiadając już niezbędne wzory do permutacji danych musimy teraz zdefiniować plik konfiguracyjny, który je wykorzysta:
# Dane wejściowe będą pobierane z plików .txt. input { file { # Ścieżka do wszystkich plików. path => "/data/archive/*/*/*.txt" # Zawsze czytaj od początku plików. start_position => "beginning" # Zapisuj metadane przetwarzania plików. sincedb_path => "/var/lib/logstash/sincedb" # Wg mnie najlepszy kodek dla tych plików. codec => plain { charset => "US-ASCII" } } } # Każda odczytana linia z pliku będzie filtrowana. filter { # Nie przesyłaj linii zaczynających się od komentarzy. if [message] =~ /^#/ { drop {} } # Nie przesyłaj pustych linii. if [message] == "" { drop {} } # Rozdziel linię na część przed i po potoku. # Bedą nazywać się "uri_short" oraz "uri_full". grok { match => { "message" => "%{DATA:uri_short}\|%{GREEDYDATA:uri_full}" } } # Dopasuj wcześniej zdefiniowane wzory do pola "uri_full". grok { patterns_dir => ["/etc/logstash/patterns"] match => { "uri_full" => "%{NFALL}" } } # Z pola "uri_domain" - stwórz nowe pole "uri_topdomain" z tld. grok { match => { "uri_domain" => "(?<uri_topdomain>([^.]*)$)" } } # Usuń zbędne metadane, aby zaoszczędzić miejsce w ES. mutate { remove_field => [ "@version", "host", "message" ] rename => [ "path", "source" ] } # Z pola "source" wydobądź datę w postaci YYYY-MM # i zapisz ją jako nowe pole "archive_date". grok { match => { "source" => "(?<archive_date>(?>\d\d){1,2}-(?:0?[1-9]|1[0-2]))" } } } # Wszystkie przetworzone dane wyślij do elasticsearch. output { # Jeśli nie było błędów dopasowania wzorów: if ("_grokparsefailure" not in [tags]) { # to wysyłaj dane na localhost port 9200. elasticsearch { hosts => ["localhost:9200"] # Nazwa indeksu = shortcodes-YYYY-MM. index => "shortcodes-%{archive_date}" # Nie zarządzaj szablonem bo sami go zdefiniujemy. manage_template => false } } }
Plik w takiej postaci powinniśmy zapisać w ścieżce: /etc/logstash/conf.d/harvester.conf. Reszty ustawień nie musimy dotykać – fabryczne są na tyle dobre, że nie powinniśmy mieć problemu z uruchomieniem programu. Ewentualnie, jeśli chcemy jak najszybciej przetwarzać dane (bo np. posiadamy serwer(y) o dużej mocy) – wówczas należy zapoznać się z sekcją dokumentacji pt. “Tuning and Profiling Logstash Performance“.
Gromadzenie danych:
Czytanie danych, modyfikacja i ich wysyłanie to jedna sprawa, a ich gromadzenie i możliwość przeszukiwania to druga. Z samym elasticsearch spotkaliśmy się już wcześniej. Nie będziemy omawiać tutaj jego podstaw, ponieważ w internecie istnieje wiele bardzo dobrych opracowań – choćby “Elasticsearch: The Definitive Guide“. Ze względów dyskowych przed uruchomieniem elasticsearch zmienimy tylko jego ścieżkę zapisu danych oraz wyprofilujemy ilość RAM, jaką może sobie zarezerwować w systemie (zazwyczaj jest to 50% posiadanej pamięci):
mkdir /data/elasticsearch chown elasticsearch:elasticsearch /data/elasticsearch
W pliku /etc/elasticsearch/elasticsearch.yml
możemy teraz ustawić:
network.host: 0.0.0.0 path.data: /data/elasticsearch
Wybraliśmy nasłuch na wszystkich interfejsach, ponieważ będziemy się z nim za pomocą eshell oraz curl
z poziomu maszyny gospodarza. Limit pamięci RAM ustawiamy w pliku /etc/elasticsearch/jvm.options
( -Xms2g / -Xmx2g ). Po wystartowaniu usługi (service elasticsearch start
) curl na port 9200 powinien otrzymać odpowiedź:
curl -XGET localhost:9200/_cluster/health?pretty { "cluster_name" : "elasticsearch", "status" : "green", "timed_out" : false, "number_of_nodes" : 1, "number_of_data_nodes" : 1, "active_primary_shards" : 0, "active_shards" : 0, "relocating_shards" : 0, "initializing_shards" : 0, "unassigned_shards" : 0, "delayed_unassigned_shards" : 0, "number_of_pending_tasks" : 0, "number_of_in_flight_fetch" : 0, "task_max_waiting_in_queue_millis" : 0, "active_shards_percent_as_number" : 100.0 }
W tym momencie jesteśmy gotowi do wgrania szablonu, który powie elasticsearch w jaki sposób ma zapisywać / indeksować dane byśmy mogli je później przeszukiwać za pomocą odpowiednich zapytań. Poniższy szablon możemy zapisać jako plik o nazwie template:
{ "index_patterns": "shortcodes*", "order": 1, "aliases": {}, "settings": { "index.analysis.analyzer.dot_separator.type": "custom", "index.analysis.analyzer.dot_separator.tokenizer": "by_dot", "index.analysis.analyzer.dot_separator.filter": ["lowercase"], "index.analysis.analyzer.slash_separator.type": "custom", "index.analysis.analyzer.slash_separator.tokenizer": "by_slash", "index.analysis.analyzer.slash_separator.filter": ["lowercase"], "index.analysis.tokenizer.by_dot.type": "pattern", "index.analysis.tokenizer.by_dot.pattern": "\\.", "index.analysis.tokenizer.by_slash.type": "pattern", "index.analysis.tokenizer.by_slash.pattern": "\\/", "index.number_of_shards": "1", "index.number_of_replicas": "0", "index.refresh_interval": "30s", "index.query.default_field": "uri_full", "index.translog.durability": "async", "index.translog.flush_threshold_size": "1gb", "index.codec": "best_compression" }, "mappings": { "doc": { "_source": { "excludes": [ "archive_date" ] }, "properties": { "@timestamp": { "type": "date" }, "archive_date": { "type": "date", "index": false }, "uri_full": { "type": "text" }, "uri_path": { "type": "keyword" }, "uri_params": { "type": "keyword" }, "uri_topdomain": { "type": "keyword" }, "uri_protocol": { "type": "keyword" }, "uri_short": { "type": "keyword" }, "uri_domain": { "type": "text", "analyzer": "dot_separator", "fields": { "keyword": { "type": "keyword" } } }, "source": { "type": "text", "analyzer": "slash_separator", "fields": { "keyword": { "type": "keyword" } } } } } } }
Jak widzimy niektóre pola chcemy mieć jako text (przepuszczane przez analizatory), a niektóre jako keyword (zapisywany bez żadnej zmiany). Wszystko po to, aby mieć większą elastyczność przy wyszukiwaniu. W dodatku dla pola uri_domain oraz source stworzyliśmy własne analizatory, które będą dzielić nam tekst na frazy używając do tego jako separatora odpowiednio: kropkę ("."
– dot_separator) oraz slash ("/"
– slash_separator). Szablon do elasticsearch wgrywamy poleceniem:
curl -H 'Content-Type: application/json' -XPUT \ localhost:9200/_template/shortcodes -d @template
Po tak przygotowanym silniku wyszukiwania możemy uruchomić w końcu program logstash (service logstash start
), który powinien zacząć zasilać danymi elasticsearch. Potwierdzeniem tego powinno być pojawienie się indeksów o nazwie shortcodes-YYYY-MM:
curl -XGET localhost:9200/_cat/indices?v health status index pri rep docs.count store.size pri.store.size green open shortcodes-2018-02 1 0 49555694 16.6gb 16.6gb green open shortcodes-2017-02 1 0 88967792 22.1gb 22.1gb green open shortcodes-2017-01 1 0 45410076 15gb 15gb green open shortcodes-2018-01 1 0 59046383 19.5gb 19.5gb
Na potrzeby demonstracji tego artykułu indeksacji zostały poddane pierwsze dwa miesiące z roku 2017 i 2018, co sumarycznie dało 72GB danych. Ze względu na ograniczone miejsce – każdy miesiąc był rozpakowywany, indeksowany i usuwany z plików .txt. Po zaindeksowaniu każdy index dodatkowo został skompaktowany do jednego segmentu, aby zaoszczędzić miejsce i przyśpieszyć wyszukiwanie:
curl -X POST "localhost:9200/shortcodes-2017-01/_forcemerge?max_num_segments=1"
Prezentacja danych:
Ostatnim krokiem jest uruchomienie interfejsu graficznego dla naszej wyszukiwarki skaranych URL, czyli kibany. W pliku konfiguracyjnym /etc/kibana/kibana.yaml
ustawiamy następujące parametry:
server.port: 8080 server.host: "0.0.0.0" elasticsearch.url: "http://localhost:9200" kibana.index: ".kibana" elasticsearch.requestTimeout: 60000 logging.dest: /tmp/kibana.log
Pierwsze uruchomienie może trwać chwilę czasu, ponieważ nodejs musi skompilować parę rzeczy na podstawie dostarczonej konfiguracji. Moment wystartowania interfejsu najlepiej obserwować w pliku logu (tail -f /tmp/kibana.log
). Powinna pojawić się w nim wiadomość:
{"type":"log","@timestamp":"2019-01-09T22:00:12Z", "tags":["listening","info"], "pid":10054, "message":"Server running at http://0.0.0.0:8080"}
Po uruchomieniu kibany możemy wywołać ją w naszej przeglądarce (np. http://192.168.56.101:8080/
). Kibana narazie nie jest świadoma, jakie dane ma wyświetlać oraz przeszukiwać – dlatego musimy zdefiniować jej źródło danych pod postacią nazw indeksów. W tym celu przechodzimy do zakładki: Management i następnie klikamy w: Index Patterns. W pierwszym kroku kreatora: Step 1 of 2: Define index pattern w polu Index pattern wpisujemy: shortcodes-* i klikamy Next step. W drugim kroku: Step 2 of 2: Configure settings przez rozwijane menu Time Filter field name wybieramy: I don’t want to use Time Filter i potwierdzamy operację przyciskiem: Create index pattern. Po zakończeniu tej operacji sukcesem możemy przejść do zakładki Discover, gdzie będziemy wykonywać nasze zapytania.
Wyszukiwanie danych:
Rzućmy okiem na kilka wrażliwych i interesujących informacji, które możemy znaleźć w danych dotyczących skracania URL. Poniżej znajduje się kilka przykładów rzeczy, które można znaleźć wzorując się na Google Dorks. Wyniki wyszukiwania nie zostały specjalnie opublikowane, aby chronić firmy podatne na ataki. Pamiętajmy, że jeśli znajdziemy jeszcze aktualne podatności – zgłośmy je w duchu odpowiedzialnego ujawnienia zainteresowanym osobom / firmom. Wszystkie zapytania zostały wpisywane w Query Bar – zakładki Discovery.
Wykonane ataki:
Pierwszą kategorią, są przykłady prób ataków. Możemy zgadywać, że skryptowe dzieciaki, cyberprzestępcy, a być może także inni używają skracarek, by dzielić się swoją pracą z “kolegami po fachu”. Niektóre z zidentyfikowanych adresów URL wyglądają jak proste dowody koncepcji (ang. Proof of Concept) tutoriali, podczas gdy inne są przeznaczone do eksfiltracji danych. Poniżej znajdują się przykłady ataków próbujących wykorzystać LFI (ang. Local File Inclusion) oraz Directory Traversal wraz z techniką osadzania NULL bajtów, które nie zawsze są poprawnie obsługiwane przez różnego rodzaju aplikacje. No i oczywiście wstrzykiwanie SQL:
Pytanie: | Rodzaj: | Ilość trafień: |
uri_full: \/proc\/self\/envirion | lucene w query bar | 134,466 |
uri_full: \/etc\/passwd | lucene w query bar | 178,203 |
uri_params: *%00 | lucene w query bar | 2,325 |
uri_params: *%0a | lucene w query bar | 411 |
uri_params: *%0d | lucene w query bar | 12 |
uri_full: union all select | lucene w query bar | 1,063,635 |
uri_full: “or 1=1” | lucene w query bar | 479 |
uri_full: select AND where AND limit NOT union | lucene w query bar | 1,292 |
Głębokie ukrycie:
Kolejnym przykładem są kopie zapasowe danych. Wielu programistów i administratorów udostępnia tymczasowo kopie zapasowe online np. aby je ściągnąć do innego systemu. Udostępniając je widocznie niektórzy z nich używali skracarek URL, aby uczynić sobie ten proces bardziej wygodnym. Luki te są klasyfikowane jako wyciek informacji.
Pytanie: | Rodzaj: | Ilość trafień: |
uri_full: *\.sql | lucene w query bar | 340 |
uri_path: *\.bak | lucene w query bar | 256 |
uri_path: *\.tar\.gz | lucene w query bar | 909 |
uri_path: *\.sql.gz | lucene w query bar | 29 |
Prywatne pliki:
Innym powszechnym odkryciem jest udostępnianie prywatnych plików bez ochrony hasłem. Użytkownicy zakładają, że losowe identyfikatory skracarek (te magiczne algorytmy, które można prosto złamać) zapewnią bezpieczeństwo ich dokumentom. W poniższym przypadku można samemu ocenić ile poufnych dokumentów krąży po różnych usługach udostępniania plików:
Pytanie: | Rodzaj: | Ilość trafień: |
uri_domain: docs.google.com | lucene w query bar | 167,245,267 |
uri_domain: dropbox.com | lucene w query bar | 167,016,085 |
uri_domain: mega.nz | lucene w query bar | 303,465 |
uri_domain: 1drv.ms | lucene w query bar | 21,287 |
uri_domain: icloud.com AND uri_full: photostream | lucene w query bar | 8,328 |
Rezerwacje hoteli, lotów:
Serwisy do przechowywania plików w chmurze to nie jedyne linki bez haseł, które są udostępniane online. Systemy rezerwacji hotelowych i biura podróży często wysyłają e-maile z linkami bez haseł, co zapewnia łatwy dostęp do szczegółów takich rezerwacji:
Pytanie: | Rodzaj: | Ilość trafień: |
uri_domain: click.mail.hotels.com | lucene w query bar | 167,135,288 |
Loginy, hasła, tokeny:
Tokeny sesji w adresach URL od lat są uważane za złą praktykę, głównie dlatego, że serwery proxy i ataki przez ramię mogą zagrozić obecnej sesji użytkownika. W poniższym zestawieniu trochę przykładów adresów URL zawierających poufne informacje:
Pytanie: | Rodzaj: | Ilość trafień: |
uri_params: *password\=* | lucene w query bar | 24,462 |
uri_params: *email\=* | lucene w query bar | 1,430,704 |
uri_params: *sessionid\=* | lucene w query bar | 6,101 |
Środowiska developerskie:
Istnieje również wiele systemów, które są wdrażane na różne środowiska przed produkcją – a wyniki z ich błędów, eksperymentów oraz testów często są współdzielone pomiędzy programistami. Wyszukiwanie środowisk developerskich oraz testowych bardzo często również ujawnia interesujące wyniki:
Pytanie: | Rodzaj: | Ilość trafień: |
uri_domain.keyword: dev\.* | lucene w query bar | 79,054 |
uri_domain.keyword: test\.* | lucene w query bar | 118,830 |
uri_domain.keyword: staging\.* | lucene w query bar | 20,185 |
uri_domain.keyword: beta\.* | lucene w query bar | 128,461 |
Podsumowanie:
Patrząc na dane pozyskane przez archiveteam.org tylko z czterech miesięcy wyraźnie widać, że użytkownicy bez skrupułów skracają wrażliwe dane w tego typu serwisach. Usługi te są używane bez głębszego zastanowienia się nad faktem, że osoby trzecie mogą uzyskać dostęp do tych samych adresów URL, co jest klastycznym przykładem zabezpieczenia przez zaciemnienie. Dane te mogą być wykorzystywane przez ludzi o złych intencjach, pragnących nadużywać ich na swoją korzyść. Lecz analogicznie działy bezpieczeństwa w organizacjach i firmach mogą wykorzystać je w dobrej wierze np. poprzez monitoring, czy nie doszło do wycieku ich własnych danych. Co więcej – pentesterzy oraz konsultanci bezpieczeństwa mogą korzystać z tego rodzaju technik w celu wyszukiwania danych swoich klientów dostarczając im cenne informacje o próbach potencjalnych ataków i zagrożeniach, nad którymi mogliby wspólnie popracować.
Wersja z terminalem:
Wyniki z elasticsearch zwracane są w formacie JSON. Ułatwia to pisanie narzędzi do interakcji z danymi, które przechowuje. Możemy przetwarzać je także w terminalu Linuksa. Powiedzmy, że chcemy uzyskać wszystkie domeny, związane z zapytaniem: uri_domain.keyword: dev\.*
. Aby to zrobić, możemy po prostu przesłać zapytanie do serwera elasticsearch za pomocą curl i przekierować dane wyjściowe do pliku. Na przykład:
curl -s -X POST -H "Content-Type: application/json" \ 'http://localhost:9200/shortcodes-*/_search?scroll=1m&size=5000' \ -d '{ "query": { "wildcard": { "uri_domain.keyword": "dev.*" } } }' > results
Na wyjściu otrzymamy ogromny blok tekstu zapakowany w JSON, więc możemy przesłać go do jq i wyciągnąć tylko te informacje, których potrzebujemy:
cat results | jq .hits.hits[]._source.uri_domain > results.domains
Na koniec przywołujemy moc standardowych narzędzi Linuksa do przetwarzania danych:
cat results.domains | cut -d"\"" -f2 | sort | uniq | wc -l 991
Oczywiście mogliśmy to zrobić za pomocą jednego polecenia, bez przechowywania żadnych plików. Jednak jest to prosty przykład mający na celu ukazanie, że możemy łatwo przetwarzać te dane w dowolny sposób, jaki chcemy. Teraz, gdy mamy domeny możemy poszukać w nich te, które biorą udział w programach Bug Bounty!.
Więcej informacji: The Secrets in URL Shortening Services