Статья Сканирование портов с захватом баннеров с помощью Python

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

full_c9rmCudI.jpg


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

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


Что понадобится?

Из посторонних библиотек – ничего. Из тех, что идут в комплекте с python, импортируем socket и threading. Давайте выполним импорт библиотек в скрипт.

Python:
import socket
import threading

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


Создание класса для сканирования портов

Давайте создадим файл portscan_v.py. Конечно же, вы можете назвать его так, как вам удобно. Как я писал уже выше, в него нужно будет импортировать библиотеки. После этого создадим функцию для инициализации класса: def __init__(self, target: str, port_counts: int). На вход она принимает цель для сканирования, то есть сайт или ip-адрес, а также количество портов для сканирования. Создадим сразу же при инициализации функции два словаря, в которые будем складывать найденные открытые порты с баннерами и без.

Python:
    def __init__(self, target: str, port_counts: int):
        """
        При инициализации класса требуется передать ему адрес для сканирования и кол-во портов,
        которые требуется просканировать.
        :ip - обработанный адрес для сканирования, в котором,
        в случае невозможности получения адреса содержится None.
        :banners_port - словарь, в котором содержаться все открытые ip-порты и баннеры
        полученные от сервисов запущенных на них.
        :open_port - словарь, содержащий все открытые порты без баннеров.
        :param target: Полученный от пользователя адрес для сканирования. Тип: строка.
        :param port_counts: Полученное от пользователя кол-во портов для сканирования. Тип: целое число.
        """
        self.target = target
        self.ip = self.check_target()
        self.port_counts = int(port_counts)
        self.banners_port = dict()
        self.open_port = dict()


Проверка и преобразование полученного адреса сайта, получение ip-адреса

Создадим функцию check_target(self). С ее помощью мы будем проверять введенные пользователем данные. То есть, адрес для сканирования. Обрезать лишнее и получать ip-адрес домена.

Для начала проверяем, начинается ли полученный адрес с http. Если да, делим адрес по «/», забираем 2 элемент. Именно здесь лежит домен. Затем, с помощью функции сокетов gethostbyname получаем ip-адрес. Если адрес получен успешно, возвращаем его из функции. Если нет, завершаем работу по исключению, соответственно, из функции вернется None.

Python:
    def check_target(self):
        """
        Производится обработка поступившего от пользователя адреса.
        Удаление http(https) для дальнейшего получения ip домена.
        Запрашивается ip-адрес, который возвращается из функции,
        если его удалось получить. Если нет, возвращается None.
        :return: Если не возникает исключения, возвращается ip-адрес,
        иначе возвращается None
        """
        if self.target.startswith("http"):
            self.target = self.target.split("/")[2]
        try:
            ip_domain = socket.gethostbyname(self.target)
            return ip_domain
        except socket.gaierror:
            return


Сканирование порта

Создадим функцию для сканирования порта, scan_port(self, port: int). На вход она принимает порт. Адрес цели у нас глобален в пределах класса. Создаем объект сокета с соединением по TCP.

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

Выставляем таймаут соединения в 5 секунд. Этого будет достаточно, чтобы получить ответ от большинства сервисов. Проверяем, если ip не получен, выходим из функции.

Python:
        s.settimeout(5)
        if self.ip is None:
            return

Создаем соединение на полученный ранее ip и полученный на входе порт. Получаем баннер. Обернем функцию получения баннера в блок try – except. Для начала проверяем, не является ли ответ полученный от сервиса пустым. Если да, обновляем словарь открытых портов без баннеров, пытаемся получить с помощью вспомогательной функции название сервиса работающего на данном порту.

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

Ну и если возникают ошибки соединения, просто выходим из функции.

Python:
    def scan_port(self, port: int):
        """
        В функции выполняется подключение к ip-адресу на полученный
        порт. Если подключение удалось, значит порт открыт. Производится
        попытка получить баннер. Если баннер не получен, обрабатывается
        исключение, в котором идет запрос службы на порту у функции за
        пределами класса. Если получить службу не удалось, возвращается
        unassigned. Полученные значения добавляются в словари для
        последующей обработки. В словарь с баннерами добавляются значения,
        если баннер получен. В остальных случаях, в словарь открытых портов.
        :param port: Номер порта для сканирования. Целое число
        :return: Возвращается None. Служит для выхода из функции
        """
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(5)
        if self.ip is None:
            return
        try:
            s.connect((self.ip, port))
            try:
                banner = s.recv(1024).decode().strip()
                if banner == '':
                    self.open_port.update({port: get_serv(port).upper()})
                else:
                    self.banners_port.update({port: banner})
            except OSError:
                self.open_port.update({port: get_serv(port).upper()})
            except UnicodeDecodeError:
                banner = s.recv(1024).strip()
                self.banners_port.update({port: banner})
        except (socket.timeout, ConnectionRefusedError, OSError):
            return


Запуск потоков для сканирования портов

Создадим функцию для запуска потоков сканирования портов port_rotate(self). Создадим список, в который будем добавлять запущенные потоки. Запускаем цикл по диапазону портов от 1 до полученного от пользователя. Создаем экземпляр потока с целевой функцией и параметрами для передачи, в данном случае, передать нужно порт. Указываем, что поток запустится как демон, стартуем поток и добавляем в список.

Python:
    def port_rotate(self):
        """
        Служит для перебора в цикле полученных от пользователя
        портов и передачи их в функцию сканирования, где производится
        их обработка.
        :return: Ничего не возвращает.
        """
        threads = []
        for port in range(1, self.port_counts+1):
            t = threading.Thread(target=self.scan_port, kwargs={'port': port})
            t.daemon = True
            t.start()
            threads.append(t)
            time.sleep(0.02)
        for thread in threads:
            thread.join()


Вспомогательная функция для получения названия сервиса на порту

Создадим вспомогательную функцию get_serv(port). На вход она принимает порт, информацию о котором нужно получить. С помощью функции сокетов getservbyport выполняется попытка получения названия сервиса. Если она не удается, возвращается 'unassigned', если же название сервиса получено, возвращается название сервиса.

Python:
def get_serv(port):
    """
    Запрашивается название сервиса по порту. Если название получено,
    оно возвращается в функцию сканирования портов. Если нет, обрабатывается
    исключение и возвращается 'unassigned'.
    :param port: Порт для запроса имени сервиса. Целое число.
    :return: Возвращается сервис, если получен. В противном случае
    возвращается 'unassigned'.
    """
    try:
        return socket.getservbyport(port)
    except OSError:
        return 'unassigned'

Python:
import socket
import threading
import time


def get_serv(port):
    """
    Запрашивается название сервиса по порту. Если название получено,
    оно возвращается в функцию сканирования портов. Если нет, обрабатывается
    исключение и возвращается 'unassigned'.
    :param port: Порт для запроса имени сервиса. Целое число.
    :return: Возвращается сервис, если получен. В противном случае
    возвращается 'unassigned'.
    """
    try:
        return socket.getservbyport(port)
    except OSError:
        return 'unassigned'


class PortScan:
    """
    Класс для сканирования портов локального или удаленного компьютера.
    Из зависимостей требует библиотеку socket, а также наличие интернета,
    так как некоторые запросы обрабатываются с его помощью.
    """
    def __init__(self, target: str, port_counts: int):
        """
        При инициализации класса требуется передать ему адрес для сканирования и кол-во портов,
        которые требуется просканировать.
        :ip - обработанный адрес для сканирования, в котором,
        в случае невозможности получения адреса содержится None.
        :banners_port - словарь, в котором содержатся все открытые ip-порты и баннеры
        полученные от сервисов запущенных на них.
        :open_port - словарь, содержащий все открытые порты без баннеров.
        :param target: Полученный от пользователя адрес для сканирования. Тип: строка.
        :param port_counts: Полученное от пользователя кол-во портов для сканирования. Тип: целое число.
        """
        self.target = target
        self.ip = self.check_target()
        self.port_counts = int(port_counts)
        self.banners_port = dict()
        self.open_port = dict()

    def check_target(self):
        """
        Производится обработка поступившего от пользователя адреса.
        Удаление http(https) для дальнейшего получения ip домена.
        Запрашивается ip-адрес, который возвращается из функции,
        если его удалось получить. Если нет, возвращается None.
        :return: Если не возникает исключения, возвращается ip-адрес,
        иначе возвращается None
        """
        if self.target.startswith("http"):
            self.target = self.target.split("/")[2]
        try:
            ip_domain = socket.gethostbyname(self.target)
            return ip_domain
        except socket.gaierror:
            return

    def scan_port(self, port: int):
        """
        В функции выполняется подключение к ip-адресу на полученный
        порт. Если подключение удалось, значит порт открыт. Производится
        попытка получить баннер. Если баннер не получен, обрабатывается
        исключение, в котором идет запрос службы на порту у функции за
        пределами класса. Если получить службу не удалось, возвращается
        unassigned. Полученные значения добавляются в словари для
        последующей обработки. В словарь с баннерами добавляются значения,
        если баннер получен. В остальных случаях, в словарь открытых портов.
        :param port: Номер порта для сканирования. Целое число
        :return: Возвращается None. Служит для выхода из функции
        """
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(5)
        if self.ip is None:
            return
        try:
            s.connect((self.ip, port))
            try:
                banner = s.recv(1024).decode().strip()
                if banner == '':
                    self.open_port.update({port: get_serv(port).upper()})
                else:
                    self.banners_port.update({port: banner})
            except OSError:
                self.open_port.update({port: get_serv(port).upper()})
            except UnicodeDecodeError:
                banner = s.recv(1024).strip()
                self.banners_port.update({port: banner})
        except (socket.timeout, ConnectionRefusedError, OSError):
            return

    def port_rotate(self):
        """
        Служит для перебора в цикле полученных от пользователя
        портов и передачи их в функцию сканирования, где производится
        их обработка.
        :return: Ничего не возвращает.
        """
        threads = []
        for port in range(1, self.port_counts+1):
            t = threading.Thread(target=self.scan_port, kwargs={'port': port})
            t.daemon = True
            t.start()
            threads.append(t)
            time.sleep(0.02)
        for thread in threads:
            thread.join()


Использование созданного класса
Функция для сканирования портов


Теперь давайте используем созданный класс в деле. Создадим новый файл main.py. В него импортируем из модуля с созданным нами классом, собственно класс.

from portscan_v import PortScan

Создадим функцию main(). Запросим у пользователя домен или ip-адрес для сканирования. Запросим количество портов, которые нужно просканировать. По умолчанию передастся 1000 портов. Затем выведем сообщение, что весь в работе.
Создадим экземпляр класса PortScan, в него передадим домен или ip-адрес, количество портов. После чего запустим функцию сканирования. По окончании выведем сообщение о том, что все выполнено.

Python:
    target = input('\n[*] Введите домен или IP-адрес для сканирования >>> ')
    port_count = int(input('[*] Введите количество портов для сканирования (1000 по умолчанию) >>> ') or 1000)
    print('\n- Сканирую...', end='')
    scan = PortScan(target, port_count)
    scan.port_rotate()
    print('\r- Выполнено', end='')

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

Python:
    if len(banners) == 0 and len(o_port) == 0:
        print('Открытых портов не найдено.')
        return
    else:
        if target.startswith("http"):
            target_print = target.split("/")[2]
            print(f'\n\nСВОДНАЯ ИНФОРМАЦИЯ ПО ДОМЕНУ (IP): {target_print}\n{"*"*50}')
        else:
            print(f'\n\nСВОДНАЯ ИНФОРМАЦИЯ ПО ДОМЕНУ (IP): {target}\n{"*"*50}')
        for bann in banners:
            print(f'  Порт: {bann:5}  Баннер: {banners[bann]}')
        for o in o_port:
            print(f'  Порт: {o:5}  Сервис: {o_port[o]}')

Python:
from portscan_v import PortScan


def main():
    target = input('\n[*] Введите домен или IP-адрес для сканирования >>> ')
    port_count = int(input('[*] Введите количество портов для сканирования (1000 по умолчанию) >>> ') or 1000)
    print('\n- Сканирую...', end='')
    scan = PortScan(target, port_count)
    scan.port_rotate()
    print('\r- Выполнено', end='')

    banners = scan.banners_port
    o_port = scan.open_port

    if len(banners) == 0 and len(o_port) == 0:
        print('Открытых портов не найдено.')
        return
    else:
        if target.startswith("http"):
            target_print = target.split("/")[2]
            print(f'\n\nСВОДНАЯ ИНФОРМАЦИЯ ПО ДОМЕНУ (IP): {target_print}\n{"*"*50}')
        else:
            print(f'\n\nСВОДНАЯ ИНФОРМАЦИЯ ПО ДОМЕНУ (IP): {target}\n{"*"*50}')
        for bann in banners:
            print(f'  Порт: {bann:5}  Баннер: {banners[bann]}')
        for o in o_port:
            print(f'  Порт: {o:5}  Сервис: {o_port[o]}')


if __name__ == "__main__":
    main()

Для примера, ниже показан результат работы функции сканирования портов на тестовой машине Metasploitable2.

screenshot12.png

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

А на этом, пожалуй, все.

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

Вложения

  • portscan.zip
    2,7 КБ · Просмотры: 15
Последнее редактирование модератором:
25.08.2021
11
0
Надеюсь, что данная информация будет вам полезна
Спасибо, весьма познавательно!
В поисках ответа на свои вопросы забрел сюда.
Подскажи пожалуйста, как может выглядеть скрипт для проверки списка урлов на ответ сервера?
Чтобы выводил что-то типа
domain.com - 200
domain2.com - 301
domain3.com - 403

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

Johan Van

Green Team
13.06.2020
156
295
Спасибо, весьма познавательно!
В поисках ответа на свои вопросы забрел сюда.
Подскажи пожалуйста, как может выглядеть скрипт для проверки списка урлов на ответ сервера?
Чтобы выводил что-то типа
domain.com - 200
domain2.com - 301
domain3.com - 403

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

Привет. Ну, для начала отвечу на последний вопрос. Тут да, все достаточно просто. Гугл, помоги )) Просто все не запомнить. Со временем запоминаешь то, чем чаще всего пользуешься. Иногда где-то видишь похожий код, но с чуть другим решением, читаешь. Особых ресурсов порекомендовать трудно. Тебя все равно будет чаще всего заносить на Стек Оверфлоу. Ну и документация, если какая-то конкретно библиотека нужна. Иногда просто смотришь примеры, как используется библиотека. В общем, всегда по разному. Да и на самом деле, запоминать все особо не нужно. Нужно просто иметь представление, в каком направлении производить поиск )

Теперь, касаемо второго вопроса.

Вот небольшой код:

Python:
import requests

headers = {
    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
                  'Chrome/102.0.5005.148 Safari/537.36',
    'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,'
              'application/signed-exchange;v=b3;q=0.9 '
}

url = 'https://google.com'

req = requests.head(url=url, headers=headers)
print(req.status_code)

А вот результат его выполнения:

screenshot1.png


То есть, в данном случае я запросил заголовки домена. Не полный код страницы. Если не нужно содержимое, то метод get можно не использовать. И получил в ответ код 301, что означает переадресацию. За отображение ответа отвечает req.status_code. Теперь попросим распечатать заголовки данного запроса. То есть, добавим в код еще одну строчку:

print(req.headers)

Получим следующую картину:

screenshot2.png


Как видно, в заголовках есть тег Location, в котором указано, куда именно перенаправление. То есть, 301 означает, окончательно перемещен. А в этом теге указано куда. Теперь попробуем сделать запрос на тот адрес, куда идет перенаправление, то есть, заменим ссылку на: url = 'https://www.google.com'. И вот что получим:

screenshot3.png


Как видно, здесь уже статус код 200 и куча полезной или не очень информации, среди которой уже нет Location. Слегка изменим код приведенный выше:

Python:
import requests

headers = {
    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
                  'Chrome/102.0.5005.148 Safari/537.36',
    'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,'
              'application/signed-exchange;v=b3;q=0.9 '
}

url = 'https://google.com'

print(f'Начальный адрес: {url}')

req = requests.head(url=url, headers=headers)

redirect = None
if req.status_code == 301:
    print(f'Ответ сервера: {req.status_code}')
    redirect = req.headers['Location']
    print(f'Перенаправление: {redirect}')
    req = requests.get(url=redirect, headers=headers)
    print(f'Ответ сервера: {req.status_code}')

И увидим следующее:

screenshot4.png


То есть, изначально я запросил заголовки, проверил статус код. Так как он 301, то можно предположить, что есть тег Location. Следовательно, его содержимое можно забрать из заголовков и уже перейти туда, куда ведет перенаправление. В итоге получаем правильную страницу ресурса, и статус-код 200.

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

Python:
import requests

headers = {
    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
                  'Chrome/102.0.5005.148 Safari/537.36',
    'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,'
              'application/signed-exchange;v=b3;q=0.9 '
}

url = 'https://www.google.com'

print(f'Начальный адрес: {url}')

req = requests.head(url=url, headers=headers)

redirect = None
if req.status_code == 301:
    print(f'Ответ сервера: {req.status_code}')
    try:
        redirect = req.headers['Location']
        print(redirect)
        print(f'Перенаправление: {redirect}')
        req = requests.get(url=redirect, headers=headers)
        print(f'Ответ сервера: {req.status_code}')
    except KeyError:
        print('Отсутствует адрес перенаправления')

print(f'Ответ сервера: {req.status_code}')

И в ответе получим:

screenshot6.png


Вот, примерно так. Надеюсь, что я смог ответить на ваш вопрос. ))
 
Последнее редактирование:
Мы в соцсетях: