Статья Получение информации о домене с помощью Python #01

Иногда полезно узнать, какие данные находятся за адресом сайта, за его доменным именем. Конечно же, есть множество сервисов, которые с радостью предоставят вам эту информацию. Но, все это нужно будет делать вручную. Думаю, что вы понимаете, что информация о домене на Whois не заканчивается. Я, на основании некоторых примеров кода, сделал свою попытку разобраться, как же можно получить данные с помощью скриптов на Python. Конечно же, никто не даст вам стопроцентного результата поиска. Вот и у меня, не всегда, получается, найти полные данные по сайту. Впрочем, не мне судить, получилось у меня что-то или нет. Давайте начинать писать код.

000.jpg

Я также как и DeathDay полностью на стороне добра, а потому:

Дисклеймер: Все данные, предоставленные в данной статье, взяты из открытых источников, не призывают к действию и являются только лишь данными для ознакомления, и изучения механизмов используемых технологий.


В данном случае моей целью было попробовать, как работают те или иные технологии поиска информации в интернете. Давайте начнем с того, что понадобиться установить для работы скрипта, а уж после будем более детально рассматривать его код.


Что потребуется?

Для получения информации Whois о домене нужно установить модули работы с сервисами. Для установки пишем в терминале:

pip install python-whois PySocks

Для работы модуля, который будет детектировать Cloudflare, понадобиться установить библиотеку requests. Впрочем, впоследствии она понадобиться не только здесь. Также, для получения данных со страницы, то есть для её парсинга, потребуется установить библиотеку bs4 и модуль для парсинга xml – lxml:

pip install requests bs4 lxml

Для получения данных из сертификата, с сервиса crt.sh, нужно установить библиотеку для подключения к базе данных PostgreSQL - psycopg2. Также, для того, чтобы пинговать доступность ресурса установим библиотеку ping3, и библиотеку gevent для обработки исключений. Пишем в терминале:

pip install psycopg2 ping3 gevent

Для получения данных из SLL-сертификата домена нужно установить библиотеку pyOpenSSL. Пишем в терминале:

pip install pyOpenSSL

Ну и напоследок, чтобы вывод в терминале не был слишком скучным, установим библиотеку colorama.

pip install colorama

По библиотекам всё. Теперь хочу сказать, что в этот раз я не стал пихать весь код в один модуль. Конечно же, можно было бы так сделать, но тогда его код был бы на самом деле, очень большим и неудобным. Поэтому я разделил код по различным модулям, каждый из которых выполняет определенный функционал и ничего более, а результат возвращает в основной модуль для обработки. Это делает скрипт более гибким и расширяемым. То есть, я могу дописать какой-то функционал в еще один модуль и добавить в основной код его обработку. А теперь, давайте начнем писать код.

Конечно же, ничего сверхъестественного тут не происходит. Просто это моя попытка научиться писать определенные вещи, так как я всё ещё учусь это делать ).


Получение информации Whois о домене

Создадим модуль whois_info.py. Импортируем в него необходимые библиотеки для его работы:

import whois

Создадим функцию whois_domain(domain), которая на входе принимает имя домена для получения информации. Теперь создадим словарь, в котором определим, какие из ключей полученного json будем заменять на нужные нам значения. Можно было бы этого не делать, а просто вывести все как есть. Но приятнее, когда не нужно напрягаться и просто читать то, что пришло в ответе )).

Python:
list_field = {"domain_name": "Доменное имя", "registrar": "Регистратор", "updated_date": "Дата обновления",
                  "creation_date": "Дата создания", "expiration_date": "Дата истечения срока регистрации",
                  "name_servers": "Серверы NS", "dnssec": "dnssec", "name": "Владелец", "org": "Организация",
                  "address": "Адрес", "city": "Город", "state": "Штат", "zipcode": "Индекс", "country": "Страна"}

Теперь создадим словарь, в который будем складывать уже полученные и обработанные значения из ответа.

whois_dict = dict()

Делаем запрос whois, в котором указываем доменное имя и флаг, который не обязателен. Но, как написано в документации, он позволяет подавить вывод предупреждений. И да, сейчас у библиотеки с этим более-менее все хорошо. Но было время, когда не помогали никакие ключи, и выскакивало неприятное предупреждение, хотя ответ приходил исправно.

wh = whois.whois(domain, flags=0)

Теперь в цикле пробежимся по полученному в ответе json и если значение ключа не является None, добавляем его в словарь, попутно заменяя ключи на нужные нам значения. Заключим данный код в блок try – except, чтобы обработать ошибку типа данных. Также заключим весь код в блок try – except, где будем обрабатывать ошибку получения данных и просто возвращать пустой словарь. А если все прошло отлично, возвращаем из функции словарь со значениями.

Python:
# pip install python-whois PySocks

import whois


def whois_domain(domain):
    list_field = {"domain_name": "Доменное имя", "registrar": "Регистратор", "updated_date": "Дата обновления",
                  "creation_date": "Дата создания", "expiration_date": "Дата истечения срока регистрации",
                  "name_servers": "Серверы NS", "dnssec": "dnssec", "name": "Владелец", "org": "Организация",
                  "address": "Адрес", "city": "Город", "state": "Штат", "zipcode": "Индекс", "country": "Страна"}

    whois_dict = dict()
    try:
        wh = whois.whois(domain, flags=0)
        for field in list_field:
            if wh.get(field) is not None:
                try:
                    if field == 'updated_date':
                        date = [str(dat) for dat in wh.get(field)]
                        if str(wh[field]) not in whois_dict:
                            whois_dict[list_field[field]] = date
                    elif field == 'creation_date':
                        date = [str(dat) for dat in wh.get(field)]
                        if str(wh[field]) not in whois_dict:
                            whois_dict[list_field[field]] = date
                    elif field == 'expiration_date':
                        date = [str(dat) for dat in wh.get(field)]
                        if str(wh[field]) not in whois_dict:
                            whois_dict[list_field[field]] = date
                    else:
                        if str(wh[field]) not in whois_dict:
                            whois_dict[list_field[field]] = wh[field]
                except TypeError:
                    if str(wh[field]) not in whois_dict:
                        whois_dict[list_field[field]] = str(wh[field])
    except whois.parser.PywhoisError:
        return whois_dict

    return whois_dict


Проверка ip-адреса на принадлежность к Cloudflare

Для того, чтобы проверить, скрыт ip-адрес за Cloudflare или нет, нужно его прогнать по всему диапазону адресов, которые за ним закреплены. И если данный адрес будет найден, возвратить значение True. В противном же случае, если адрес не найден, вернуть False.

Тут, на самом деле, можно пойти двумя путями. Либо, как у меня, проверять адрес на принадлежность к определенным диапазонам, либо получить данные ssl-сертификата и посмотреть, на кого он выдан. У тех доменов, что прячутся за Cloudflare, обычно он на него и выдан.

Создадим модуль cloudflare_detect.py. Импортируем в него необходимые для работы библиотеки:

Python:
import socket
from ipaddress import ip_network, ip_address

import requests

Создадим список, в который будем добавлять диапазоны адресов Cloudflare.

list_addr = ["104.16.0.0/12"]

Как видите, у меня список не пустой и в нем уже есть один из диапазонов. Дело в том, что на официальной странице диапазонов адресов, данного диапазона нет, но при проверке информации whios видно, что данный диапазон также принадлежит к Cloudflare. Поэтому, пришлось добавить его вручную.

Создадим функцию ip_in_range(ip, addr), которая на входе принимает ip-адрес для проверки, и диапазон адресов, в которых его нужно проверить. Воспользуемся модулем ipaddress, а точнее его функциями ip_network и ip_address. Первая функция создает из строки адрес, а вторая диапазон. И делается проверка на принадлежность к данному диапазону. Если принадлежит, возвращает True, нет – False.

Python:
def ip_in_range(ip, addr):
    if ip_address(ip) in ip_network(addr):
        return True
    return False

Создадим в этом же модуле еще одну функцию cloudf_detect(domain), которая на входе принимает домен. Теперь, для того, чтобы сделать проверку, нам нужно получить диапазоны адресов, которые принадлежат Cloudflare. Можно для этого сделать список, но, чтобы иметь актуальную информацию, получим данные со страницы с диапазонами.

Python:
    url = 'https://www.cloudflare.com/ips-v4'
    req = requests.get(url=url)

Теперь разделим полученный список по «\n» и пробежимся по нему циклом для заполнения нашего списка диапазонов.

Python:
    for adr in req.text.split("\n"):
        list_addr.append(adr)

Затем нужно получить ip-адрес домена, который мы будем проверять. Для этого воспользуемся socket. Также обработаем ошибку получения данных:

Python:
    try:
        ip = socket.gethostbyname(domain)
    except socket.gaierror:
        return

Создадим цикл в котором пробежимся по списку диапазонов адресов, в котором будем вызывать функцию проверки на принадлежность к диапазону, передавая ей ip и диапазон. Если функция возвращает True, то на этом работу функции можно завершить, так как ip-адрес принадлежит к диапазону. Если False, продолжаем итерации цикла. А если же ничего не найдено, то есть ip не принадлежит диапазону, соответственно возвращаем False.

Python:
    for addr in list_addr:
        detect = ip_in_range(ip, addr)
        if detect:
            return True
    return False

Python:
# pip install requests

import socket
from ipaddress import ip_network, ip_address

import requests

list_addr = ["104.16.0.0/12"]


# проверка адреса в диапазоне ip подсети
def ip_in_range(ip, addr):
    if ip_address(ip) in ip_network(addr):
        return True
    return False


# проверка на принадлежность адреса Cloudflair
def cloudf_detect(domain):
    url = 'https://www.cloudflare.com/ips-v4'
    req = requests.get(url=url)

    for adr in req.text.split("\n"):
        list_addr.append(adr)

    try:
        ip = socket.gethostbyname(domain)
    except socket.gaierror:
        return

    for addr in list_addr:
        detect = ip_in_range(ip, addr)
        if detect:
            return True
    return False


Поиск реального адреса домена за Cloudflare с помощью CrimeFlare

По поводу данного способа все не однозначно. Для начала, это уже «второе пришествие» данного проекта, так как первый проект был заблокирован и снесен с хостинга. Теперь он возродился в виде PHP скрипта на GitHub и приложения на Heroku. Во-вторых, насколько я понял, поиск адресов происходит по некой базе. И не совсем понятно, обновляется она или нет. Не ясно как наполняется данная база адресами. Так как разработчик не особо об этом распространяется. Но, что имеем, то имеем. Ну и в-третьих, приложение на Heroku может просто перестать работать.

Такое уже произошло примерно около месяца назад. Я тогда заинтересовался попытками определения адресов за Cloudflare и наткнулся на данное приложение. Когда мне перестали приходить ответы от приложения я перешел на его страницу и обнаружил, что оно просто не работает. Тогда я связался с автором и сообщил ему о проблеме, на что он ответил, что в курсе. Но ничего пока поделать не может, так как аккаунт платный, а денег для его оплаты пока нет. Но, похоже через какое-то время деньги все же нашлись, так как приложение снова заработало. Я попытался спросить, как работает данное приложение, но ответа до сих пор не получил. Ну да ладно. Это предыстория и попытка сказать, что использование данного модуля не даст стопроцентного результата и его работа может прерваться в любой момент.

Создадим модуль crimeflare_search.py. Импортируем в него библиотеки нужные для работы:

import requests

Создадим функцию crimf_search(domain), которая на входе принимает имя домена. А дальше идет банальный парсинг. Делаем запрос request к странице базы с параметрами и парсим из ответа адрес, если он есть. Если нет, обрабатываем ошибку поиска индекса и возвращаем значение False. Если же адрес найден, возвращаем адрес.

Полный код модуля crimeflare_search.py:

Python:
# pip install requests

import requests


def crimf_search(domain):
    url = f'https://crimeflare.herokuapp.com/?url={domain}'
    try:
        req = requests.get(url).text.split("</pre>")[0].split('Real IP:')[1].strip().split()[1]
        return req
    except IndexError:
        return False


Получение субдоменов из базы crt.sh

Создадим модуль subdomain_cert.py. Импортируем в него библиотеки и инициализируем модуль colorama:

Python:
import psycopg2
from colorama import Fore
from colorama import init
from gevent import exceptions
from ping3 import ping

init()

Создадим функцию subdomain_in_sert(domain), которая на входе принимает домен. Создадим множество для добавления в него субдоменов из сертификата. Множество нужно для того, чтобы отсеять повторяющиеся результаты, так как в выборке из базы данных возвращается много одинаковых значений, так как выборка идет по всем найденным сертификатам с доменным именем.

sub_list = set()

Теперь создадим подключение к базе данных, после чего сформируем запрос, выполним его и обработаем полученные значения. Надо признаться, что данный код я позаимствовал из какой-то библиотеки для поиска субдоменов. Потому как изначально я просто делал, зарос с помощью request и парсил результаты. Так тоже работало, но довольно нестабильно. Пока не наткнулся на данный код. Если честно, то даже и не знал, что под капотом крутиться PostgreSQL. С помощью данного кода получение данных сертификатов работает гораздо стабильнее.

Python:
    conn = psycopg2.connect(host="crt.sh", database="certwatch", user="guest", port="5432")
    conn.autocommit = True
    cur = conn.cursor()
    query = f"SELECT ci.NAME_VALUE NAME_VALUE FROM certificate_identity ci WHERE ci.NAME_TYPE = 'dNSName' AND " \
            f"reverse(lower(ci.NAME_VALUE)) LIKE reverse(lower('%.{domain}')) "
    cur.execute(query)
    result = cur.fetchall()
    cur.close()
    conn.close()

Ну, а дальше идет выборка нужных значений, из которых, для начала исключаем все субдомены со «*», затем проверяем, был ли уже такой субдомен или нет. Если был, то пропускаем итерацию. Пингуем полученный домен из выборки и если ответ не False добавляем в множество. После того, как обработка адресов завершиться, возвращаем множество из функции.

Python:
# pip install psycopg2 ping3 gevent

import psycopg2
from colorama import Fore
from colorama import init
from gevent import exceptions
from ping3 import ping

init()


def subdomain_in_sert(domain):
    sub_list = set()

    conn = psycopg2.connect(host="crt.sh", database="certwatch", user="guest", port="5432")
    conn.autocommit = True
    cur = conn.cursor()
    query = f"SELECT ci.NAME_VALUE NAME_VALUE FROM certificate_identity ci WHERE ci.NAME_TYPE = 'dNSName' AND " \
            f"reverse(lower(ci.NAME_VALUE)) LIKE reverse(lower('%.{domain}')) "
    cur.execute(query)
    result = cur.fetchall()
    cur.close()
    conn.close()

    old_url = ''
    if len(result) > 0:
        print(Fore.YELLOW + f'\r- Добавляю субдомены из сертификата')
        for item in result:
            if "*" not in item[0]:
                if item[0] == old_url:
                    continue
                old_url = item[0]
                try:
                    p = ping(item[0])
                    if p != False:
                        sub_list.add(item[0])
                        print(Fore.GREEN + f'\r[+] {item[0]}', end='')
                    else:
                        print(Fore.RED + f'\r[-] {item[0]}', end='')
                except exceptions.LoopExit:
                    return
    return sub_list


Что же, на этом первая часть закончена. Продолжение описания модулей в следующей части.

Спасибо за внимание. Надеюсь, что данная информация будет вам полезна
 
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!