NFsec Logo

esDNSGrep – szybkie wyszukiwanie dużych zestawów danych DNS

30/06/2020 w Pen Test Brak komentarzy.  (artykuł nr 742, ilość słów: 1782)

Zestaw danych projektu Sonar Rapid7 to niesamowite zasoby wiedzy. Są w nich zawarte wyniki skanowania internetu w skompresowanej i łatwej do pobrania formie. W tym wpisie skupimy się na dwóch rodzajach danych: Reverse DNS (RDNS) (czyli rekordy PTR) oraz Forward DNS (FDNS) (czyli rekordy: ANY, A, AAAA, TXT, MX oraz CNAME). Niestety praca z surowymi zestawami danych może być nieco wolniejsza, ponieważ pakiety rekordów RDNS oraz FDNS zawierają kilkanaście GB skompresowanego tekstu. Przeszukiwanie takich dużych plików może zajmować kilkanaście lub więcej minut dla pojedynczych kwerend. Możemy przyśpieszyć taką operację poprzez wykorzystanie wyszukiwania binarnego na odpowiednio posortowanych danych – lub wykorzystać do tego rodzaju operacji systemy wręcz stworzone do przechowywania i przeszukiwania tego rodzaju rekordów.

Mowa tutaj o Elasticsearch, z którym mogliśmy się już spotkać podczas serii (I i II) o skracarkach URL. Nie będziemy skupiać się tutaj na procesie instalacji oraz konfiguracji, ponieważ tego typu publikacji jest wiele w internecie – bardziej skupimy się na wymodelowaniu danych – tak, aby ich przeszukiwanie było bardzo proste i dawało nam duży zakres dowolności zapytań. Każdy z wyżej wymienionych zestawów danych posiada również schemat, który ułatwi nam wykonanie szablonu opisującego poszczególne pola do wyszukiwania. Dla RDNS oraz FDNS jest on taki sam:

{
 "$id": "https://opendata.rapid7.com/sonar.rdns_v2/",
 "type": "object",
 "definitions": {},
 "$schema": "http://json-schema.org/draft-07/schema#",
 "additionalProperties": false,
 "properties": {
  "timestamp": {
   "$id": "/properties/timestamp",
   "type": "string",
   "description": "The time when this response was received in seconds since the epoch"
  },
  "name": {
   "$id": "/properties/name",
   "type": "string",
   "description": "The record name"
  },
  "type": {
   "$id": "/properties/type",
   "type": "string",
   "description": "The record type"
  },
  "value": {
   "$id": "/properties/value",
   "type": "string",
   "description": "The response received for a record of the given name and type"
  }
 }
}

Jednak od strony Elasticsearch typy danych dla poszczególnych pól (name, value) będą odwrócone miejscami w ramach zbiorów RDNS i FDNS. Szablon dla przykładowego pliku 2020-05-27-15905381-rdns.json.gz będzie miał postać:

{
  "index_patterns": ["rdns*"],
  "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.tokenizer.by_dot.type": "pattern",
    "index.analysis.tokenizer.by_dot.pattern": "\\.",
    "index.number_of_shards": "1",
    "index.number_of_replicas": "0",
    "index.refresh_interval": "30s",
    "index.query.default_field": "value",
    "index.translog.durability": "async",
    "index.translog.flush_threshold_size": "1gb",
    "index.codec": "best_compression"
  },
  "mappings" : {
    "properties": {
      "timestamp": {
        "type": "date"
      },
      "name": {
        "type": "ip"
      },
      "value": {
        "type": "text",
        "analyzer": "dot_separator",
        "norms": false,
        "fields": {
          "keyword": {
            "type": "keyword"
          }
        }
      },
      "type": {
        "type": "keyword"
      }
    }
  }
}

Z kolei dla 2020-05-23-15902087-fdns_a.json.gz (tutaj value jest adresem IP, a name tekstem):

{
  "index_patterns": ["fdns*"],
  "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.tokenizer.by_dot.type": "pattern",
    "index.analysis.tokenizer.by_dot.pattern": "\\.",
    "index.number_of_shards": "1",
    "index.number_of_replicas": "0",
    "index.refresh_interval": "30s",
    "index.query.default_field": "value",
    "index.translog.durability": "async",
    "index.translog.flush_threshold_size": "1gb",
    "index.codec": "best_compression"
  },
  "mappings" : {
    "properties": {
      "timestamp": {
        "type": "date"
      },
      "value": {
        "type": "ip"
      },
      "name": {
        "type": "text",
        "analyzer": "dot_separator",
        "norms": false,
        "fields": {
          "keyword": {
            "type": "keyword"
          }
        }
      },
      "type": {
        "type": "keyword"
      }
    }
  }
}

Wrzucamy szablony indeksów do serwera Elasticsearch:

root@darkstar:~# curl -H 'Content-Type: application/json' \
-XPUT localhost:9200/_template/fdns -d @fdns_template
{"acknowledged":true}
root@darkstar:~# curl -H 'Content-Type: application/json' \
-XPUT localhost:9200/_template/rdns -d @rdns_template
{"acknowledged":true}

Teraz możemy nakarmić nasz silnik wyszukiwania strumieniami danych:

wget -O - "https://opendata.rapid7.com/sonar.rdns_v2/2020-05-27-15905381-rdns.json.gz" \
| gunzip -c | while IFS='$\n' read -r line; do curl -H "Content-Type: application/json" \
-X POST -d "$line" "http://localhost:9200/rdns_2020-05-27/_doc?pretty"; done

wget -O - "https://opendata.rapid7.com/sonar.fdns_v2/2020-05-23-15902087-fdns_a.json.gz" \
| gunzip -c | while IFS='$\n' read -r line; do curl -H "Content-Type: application/json" \
-X POST -d "$line" "http://localhost:9200/fdns_2020-05-23/_doc?pretty"; done

Zaletą strumienia danych jest brak konieczności ściągania tymczasowych danych na dysk tylko od razu umieszczanie ich w docelowym systemie. Wadą powyższego przykładu jest indeksowanie rekord po rekordzie, które jest niewydajne i wolne od strony Elasticsearch. W celu przyśpieszenia tego procesu możemy zastąpić read oraz curl programem filebeat, który będzie pobierał i wysyłał paczki danych, a nie pojeczyne wpisy. Wystarczy, że przygotujemy prosty plik konfiguracyjny definiujący wejście (input) do programu przez stdin oraz wyjście (output) do serwera Elasticsearch:

root@darkstar:~# cat /etc/filebeat/filebeat_rdns.yml
queue.mem:
  events: 4096
  flush.min_events: 2048
  flush.timeout: 5s


filebeat.inputs:
- type: stdin
  json.keys_under_root: true

processors:
  - drop_fields:
      fields: ["agent","log","input","ecs","host"]

output.elasticsearch:
  hosts: ["http://localhost:9200"]
  index: "rdns_2020-05-27"
  bulk_max_size: 4096

setup.ilm.enabled: false
setup.template.enabled: false

Identyczną konfigurację wykonujemy dla danych FDNS tylko zmieniamy docelowy index. Strumienie karmiące po zmianach będą miały postać:

wget -O - "https://opendata.rapid7.com/sonar.rdns_v2/2020-05-27-15905381-rdns.json.gz" \
| gunzip -c | filebeat -c /etc/filebeat/filebeat_rdns.yml

wget -O - "https://opendata.rapid7.com/sonar.fdns_v2/2020-05-23-15902087-fdns_a.json.gz" \
| gunzip -c | filebeat -c /etc/filebeat/filebeat_fdns.yml

Po zakończeniu procesu wrzucania danych możemy wykonać testowe zapytania z poziomu API. Na początek spytamy się o jeden rekord PTR zawierający domenę totinternet.net (czyli frazę: totinternet + net, ponieważ dla pola value rozbijamy frazy do wyszukiwania po kropce):

root@darkstar:~# curl -X GET "localhost:9200/rdns*/_search?size=1&pretty" \
-H 'Content-Type: application/json' \
-d '{"query": {"terms": {"value": ["net", "totinternet"]}}}'

{
  "took" : 30,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 10000,
      "relation" : "gte"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "rdns_2020-05-27",
        "_type" : "_doc",
        "_id" : "cO7WBnMBncoF3IGFgp2e",
        "_score" : 1.0,
        "_source" : {
          "@timestamp" : "2020-06-30T20:06:26.850Z",
          "type" : "ptr",
          "timestamp" : "1590564032",
          "name" : "1.10.159.235",
          "value" : "node-6az.pool-1-10.dynamic.totinternet.net"
        }
      }
    ]
  }
}

Nasze zapytanie zajęło 30 ms, a sumaryczna liczba dopasowań wyniosła 10.000. Skoro wyszukiwanie po domenach działa sprawdźmy teraz wyniki po adresach IP:

root@darkstar:~# curl -X GET "localhost:9200/rdns*/_search?size=1&pretty" \
-H 'Content-Type: application/json' -d '{"query": {"term": {"name": "1.10.159.0/24" }}}'
{
  "took" : 4,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 256,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "rdns_2020-05-27",
        "_type" : "_doc",
        "_id" : "cO7WBnMBncoF3IGFgp2e",
        "_score" : 1.0,
        "_source" : {
          "@timestamp" : "2020-06-30T20:06:26.850Z",
          "type" : "ptr",
          "timestamp" : "1590564032",
          "name" : "1.10.159.235",
          "value" : "node-6az.pool-1-10.dynamic.totinternet.net"
        }
      }
    ]
  }
}

Zwrócenie pierwszego rekordu pasującego do maski CDIR /24 zajęło 4 ms, a sumaryczna liczba dopasowań wyniosła 256 (testy były przeprowadzone na 1 vCPU oraz 2 GB Heap). Jeśli wszystko działa zgodnie z naszymi oczekiwaniami – możemy teraz doinstalować oraz uruchomić GUI Elasticsearch w postaci Kibany. Da nam to możliwość wykonywania szybkich zapytań dla różnych indeksów oraz wygodne przeglądanie wyników. Ostatnim krokiem jest dopracowanie automatyzacji całego procesu: usuwania starych danych, ściągania nowych i karmienie nimi silnika wyszukiwania. Wówczas taką usługę możemy udostępnić wewnątrz naszego zespołu przeprowadzającego rekonesans w ramach testów penetracyjnych. Jest to znacznie wygodniejsze, szybsze i bardziej elastyczne rozwiązanie pod względem możliwości przeszukiwania takich zbiorów. Nawet myśląc o komercyjnym zastosowaniu – możemy się skalować do roznącej liczby klientów.

Więcej informacji: DNSGrep — Quickly Searching Large DNS Datasets

Kategorie K a t e g o r i e : Pen Test

Tagi T a g i : , , , , , , , , , , ,

Komentowanie tego wpisu jest zablokowane.