Интернет уже видел множество сканеров портов разного функционала, скорости и информативности. Не обошел и я данную тему стороной. Но, попытался дополнить ее небольшим, хоть и не секретным, но, почему-то очень редко встречающимися функционалом, а именно, захватом баннеров. Конечно же, делать так не нужно, и вся информация в статье только лишь для ознакомления. А поможет нам в создании сканера портов, конечно же, Python.
В широком смысле, захват баннеров, это метод, который нередко используется для того, чтобы получить информацию о запущенных службах на целевой системе. В данном контексте, баннер, это сообщение, которое размещается на хосте и как правило выполняет роль приветствия или предоставления минимальной информации о версиях используемых служб и программных продуктов.
Что понадобится?
Из посторонних библиотек – ничего. Из тех, что идут в комплекте с python, импортируем socket и threading. Давайте выполним импорт библиотек в скрипт.
Тут нужно сказать, что я решил сделать из данного сканера класс. Уж не знаю, зачем, может быть для того, чтобы можно было одновременно попытаться просканировать несколько сайтов. Тут еще надо подумать. Но, хотелось бы сказать, что с созданием класса использование функционала в коде стало намного легче и проще. Достаточно создать экземпляр класса, передать параметры и запустить функции.
Создание класса для сканирования портов
Давайте создадим файл portscan_v.py. Конечно же, вы можете назвать его так, как вам удобно. Как я писал уже выше, в него нужно будет импортировать библиотеки. После этого создадим функцию для инициализации класса: def __init__(self, target: str, port_counts: int). На вход она принимает цель для сканирования, то есть сайт или ip-адрес, а также количество портов для сканирования. Создадим сразу же при инициализации функции два словаря, в которые будем складывать найденные открытые порты с баннерами и без.
Проверка и преобразование полученного адреса сайта, получение ip-адреса
Создадим функцию check_target(self). С ее помощью мы будем проверять введенные пользователем данные. То есть, адрес для сканирования. Обрезать лишнее и получать ip-адрес домена.
Для начала проверяем, начинается ли полученный адрес с http. Если да, делим адрес по «/», забираем 2 элемент. Именно здесь лежит домен. Затем, с помощью функции сокетов gethostbyname получаем ip-адрес. Если адрес получен успешно, возвращаем его из функции. Если нет, завершаем работу по исключению, соответственно, из функции вернется None.
Сканирование порта
Создадим функцию для сканирования порта, scan_port(self, port: int). На вход она принимает порт. Адрес цели у нас глобален в пределах класса. Создаем объект сокета с соединением по TCP.
Выставляем таймаут соединения в 5 секунд. Этого будет достаточно, чтобы получить ответ от большинства сервисов. Проверяем, если ip не получен, выходим из функции.
Создаем соединение на полученный ранее ip и полученный на входе порт. Получаем баннер. Обернем функцию получения баннера в блок try – except. Для начала проверяем, не является ли ответ полученный от сервиса пустым. Если да, обновляем словарь открытых портов без баннеров, пытаемся получить с помощью вспомогательной функции название сервиса работающего на данном порту.
Если баннер получен, добавляем информацию в словарь открытых портов с баннерами. В случае ошибки получения баннера и возникновения исключения, также добавляем информацию об открытом порте в словарь отрытых портов без баннеров. Если возникает ошибка декодирования, добавляем в словарь портов с баннерами не декодированную информацию.
Ну и если возникают ошибки соединения, просто выходим из функции.
Запуск потоков для сканирования портов
Создадим функцию для запуска потоков сканирования портов port_rotate(self). Создадим список, в который будем добавлять запущенные потоки. Запускаем цикл по диапазону портов от 1 до полученного от пользователя. Создаем экземпляр потока с целевой функцией и параметрами для передачи, в данном случае, передать нужно порт. Указываем, что поток запустится как демон, стартуем поток и добавляем в список.
Вспомогательная функция для получения названия сервиса на порту
Создадим вспомогательную функцию get_serv(port). На вход она принимает порт, информацию о котором нужно получить. С помощью функции сокетов getservbyport выполняется попытка получения названия сервиса. Если она не удается, возвращается 'unassigned', если же название сервиса получено, возвращается название сервиса.
Использование созданного класса
Функция для сканирования портов
Теперь давайте используем созданный класс в деле. Создадим новый файл main.py. В него импортируем из модуля с созданным нами классом, собственно класс.
Создадим функцию main(). Запросим у пользователя домен или ip-адрес для сканирования. Запросим количество портов, которые нужно просканировать. По умолчанию передастся 1000 портов. Затем выведем сообщение, что весь в работе.
Создадим экземпляр класса PortScan, в него передадим домен или ip-адрес, количество портов. После чего запустим функцию сканирования. По окончании выведем сообщение о том, что все выполнено.
Дальше идут проверки. Если функция сканирования вернула нам оба пустых словаря, пишем, что ничего не найдено и выходим из функции. Если же не пустой хоть один словарь, перебираем и выводим информацию в терминал.
Для примера, ниже показан результат работы функции сканирования портов на тестовой машине Metasploitable2.
Таким образом, из полученной информации можно сделать вывод как об операционной системе, на которой работает данная машина, так и о сервисах, которые на ней запущены. Это, скажем так, простое сканирование с захватом баннеров. Надо сказать, что далеко не на всех доменах вам удастся получить столь подробную информацию, но, если данная информация будет получена, вы уже можете ее использовать по назначению. К примеру, по полученному баннеру сервиса можно попытаться узнать, существует ли уязвимость для данной версии, а также эксплоит к ней.
А на этом, пожалуй, все.
Спасибо за внимание. Надеюсь, что данная информация будет вам полезна
Дисклеймер: Все данные, предоставленные в данной статье, взяты из открытых источников, не призывают к действию и являются только лишь данными для ознакомления, и изучения механизмов используемых технологий.
В широком смысле, захват баннеров, это метод, который нередко используется для того, чтобы получить информацию о запущенных службах на целевой системе. В данном контексте, баннер, это сообщение, которое размещается на хосте и как правило выполняет роль приветствия или предоставления минимальной информации о версиях используемых служб и программных продуктов.
Что понадобится?
Из посторонних библиотек – ничего. Из тех, что идут в комплекте с 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.
Таким образом, из полученной информации можно сделать вывод как об операционной системе, на которой работает данная машина, так и о сервисах, которые на ней запущены. Это, скажем так, простое сканирование с захватом баннеров. Надо сказать, что далеко не на всех доменах вам удастся получить столь подробную информацию, но, если данная информация будет получена, вы уже можете ее использовать по назначению. К примеру, по полученному баннеру сервиса можно попытаться узнать, существует ли уязвимость для данной версии, а также эксплоит к ней.
А на этом, пожалуй, все.
Спасибо за внимание. Надеюсь, что данная информация будет вам полезна
Вложения
Последнее редактирование модератором: