Статья Карманный бот для анализа веб-сайта в Телеграме

Введение:

в.png


Думаю многие сталкивались с проблемой, когда нужно было сделать базовый анализ какого либо вебсайта, но ПК/Ноутбук рядом не находились, но был смартфон.

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

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

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

Мы реализуем поддержку:
  • Censys
  • Самописного сканера портов
  • Детектора WAF
  • SearchSploit
План:
1. Подготовка почвы.
2. Создание модулей и подключение модулей.
2.1 Censys
2.2 Scaner
2.3 WAF
2.4 SearchSploit
3. Тесты


Подготовка почвы:

Перед работой нам понадобится установить telebot, прописываем:
Python:
pip3 install pytelegrambotapi
Далее, request и bs4 для работы с Censys и Exploit-DB.
Python:
pip3 install requests
pip3 install bs4

Мы также будем использовать wafw00f для создания детекта WAF.
Python:
pip3 install wafw00f

В телеграме начнем беседу с @BotFather, создадим нового бота с помощью команды /newbot, можно ввести произвольные парметры, после завершения мы получим так называемый токен, он нам пригодится в дальнейшем. Создадим Auxiliator.py, это будет основной файл где и будет сам бот. Импортируем нужный нам telebot:
Python:
import telebot

Теперь, нам нужен токен бота, это уникальный адрес вашего бота, вы должны были получить его при создании:
Python:
bot = telebot.TeleBot("ВАШ_ТОКЕН")

Используя команды, мы чаще всего будем так-же вводить цель, это будет аргументом и нам его нужно извлечь, напишем функцию:
Python:
def extract_arg(arg):
    return arg.split()[1:]

Теперь, определимся с командами с помощью которых мы будем управлять ботом:
Поиск информации в Censys по IP:
Python:
@bot.message_handler(commands=['searchbyip'])
def searchbyip(message):

Поиск информации в Censys по домену:
Python:
@bot.message_handler(commands=['searchbydomain'])
def searchbydomain(message):

Сканер портов:
Python:
@bot.message_handler(commands=['scan'])
def scan(message):

Тест на WAF:
Python:
@bot.message_handler(commands=['waf'])
def waf(message):

SearchSploit:
Python:
@bot.message_handler(commands=['searchsploit'])
def searchsploit(message):

Создание модулей:

Создадим модуль для поиска информации в Censys:
Python:
import requests #Библиотека для создания запроса
import re #Библиотека для форматирования
from bs4 import BeautifulSoup #Библиотека для парсинга

Сначала разберемся с функцией, целью которой будет поиск информации в Censys по IP:
Python:
def SearchByIp(target):

Создадим лист infos, где будем хранить всю нарытую информацию.
Python:
infos=[]

Зададим переменную header, чтобы Censys не выявил, что мы бот:
Python:
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:92.0) Gecko/20100101 Firefox/92.0',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
    'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
    'Connection': 'keep-alive',
    'Upgrade-Insecure-Requests': '1',
    'Sec-Fetch-Dest': 'document',
    'Sec-Fetch-Mode': 'navigate',
    'Sec-Fetch-Site': 'none',
    'Sec-Fetch-User': '?1',
    'Cache-Control': 'max-age=0',
    'TE': 'trailers',
}

Теперь нам нужно сделать запрос к сайту, все ссылки Censys имеют вид: {ЦЕЛЬ}, делаем запрос вместе с header и получаем ответ:
Python:
response = requests.get(f'''https://search.censys.io/hosts/{target}''', headers=headers)

Теперь, нам нужно использовать response.content, именно в нем содержатся данные которые нам передает Censys при обработке запроса:
Python:
soup = BeautifulSoup(response.content, "html.parser")
Здесь, мы используем BeautifulSoup для преобразования данных в другой формат и сохраняет в переменной soup.
Изучив страницу через F12, мы поймем, что нужные нам данные касательно IP хранятся в "content", в BeautifulSoup есть возможность поиска по id, используем его:
Python:
results = soup.find(id="content").

Это еще не все, дальше нам нужно найти порты и протокол которые засек Censys, сделаем это с помощью поиска по классу:
Python:
ports = results.find_all("div", class_="protocol-details")

Теперь нам нужно пройтись по каждому порту и сделать несколько преобразований, немного отформатировать строки и получить нормальные данные:
Python:
for port in ports:
    soup = BeautifulSoup(str(port), features="lxml") #Преобразуем наш результат в объект BeautifulSoup
    results = soup.h2 #Оставляем только h2
    results = str(results.text) #Преобразуем в строку
    results = results.replace(" ", "") #Удаляем пробелы
    results = results.split('\n') #Разбиваем на строки
    infos.append("Protocol:"+" "+results[1].split("/")[1]+" "+"is on port:"+" "+results[1].split("/")[0]) #Добавляем в лист infos протокол и порт

Далее, снова переопределяем переменную soup и ищем content:
Python:
soup = BeautifulSoup(response.content, "html.parser")
    results = soup.find(id="content")

Теперь мы попытаемся найти гораздо более интересную инфу, такую как:
Информация об ОС, информация о сети, информация о роутинге.
Ну и конечно же, сервисы на портах и их версия:
Python:
try:
    dl_data = results.find('dt', string='OS').find_next_siblings('dd') #Ищем все данные о ОС
    infos.append("OS:"+dl_data[0].string.replace("\n", "")) #Добавляем в лист
except:
    infos.append("OS Can't be specified!") #Если не нашли данные о ОС, выводим в лист ошибку
try:
    dl_data = results.find('dt', string='Network').find_next_siblings('dd') #Ищем все данные о сети
    infos.append("Network:"+dl_data[0].text.replace(" ", "").replace("\n", "")) #Добавляем в лист
except:
    infos.append("Network Can't be specified!") #Если не нашли данные о сети, выводим в лист ошибку
try:
    dl_data = results.find('dt', string='Routing').find_next_siblings('dd') #Ищем все данные о роутинге
    infos.append("Routing:"+dl_data[0].text.replace(" ", "").replace("\n", "")) #Добавляем в лист
except:
    infos.append("Routing Can't be specified!") #Если не нашли данные о роутинге, выводим в лист ошибку
И снова проходимся по каждому порту и ищем сервисы:
Python:
for portservice in ports:
    portlist=(portservice.find_next_siblings("div", class_="host-section")) #Ищемсоседние элементы
    port=re.sub('\n\n', '\n', portlist[0].text) #Удаляем пустые строки
    port=re.sub(' +', ' ', port) #Удаляем пробелы
    port=port.split("\n") #Разбиваем на строки
    infos.append(portservice.text.split("\n")[2].replace(" ", "")+":") #Добавляем в лист название порта
    for el in port: #Проходимся по каждой строке
        if el!='' and el!=' ' and "\r" not in el and "\n" not in el: #Если строка не пустая и не содержит пробелов и не содержит переносов строк
            if(el.startswith(" ")==True): #Если строка начинается с пробела
                infos.append(el.replace(" ", "-", 1)) #Удаляем пробел и добавляем в лист
            if(el.startswith(" ")==False): #Если строка не начинается с пробела
                infos.append(el+":") #Добавляем в лист
        elif(el!=' '): #Если строка не пустая
            infos.append(el)  #Добавляем в лист

Возвращаем нашу переменную:
Python:
return(infos)
Идем дальше, иногда сайт скрывает свой IP, через тот же WAF.
Предположим что мы с помощью будущей команды для WAF определили наличие CloudFlare, как нарыть инфу на сайт в таком случае?
К счастью, в Censys есть поиск и он поддерживает поиск по доменам, реализуем этот поиск в функции.
Python:
def SearchByDomain(target):

Мы будем хранить все найденные айпи в переменной
Python:
Мы будем хранить все найденные айпи в переменной addr=[]
Как и в прошлый раз зададим headers:
Код:
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:92.0) Gecko/20100101 Firefox/92.0',
    'Accept': '*/*',
    'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
    'X-Requested-With': 'XMLHttpRequest',
    'Connection': 'keep-alive',
    'Referer': 'https://search.censys.io/search?resource=hosts&q=randomfgnya.com',
    'Sec-Fetch-Dest': 'empty',
    'Sec-Fetch-Mode': 'cors',
    'Sec-Fetch-Site': 'same-origin',
    'Cache-Control': 'max-age=0',
}

Но того мало, нам так-же нужно задать доп. параметры для поиска
Код:
params = (
    ('resource', 'hosts'),
    ('q', f''' services.tls.certificates.leaf_data.names: {target}''')
)

Теперь делаем запрос к серверу вместе со всеми параметрами и headers:
Python:
response = requests.get('https://search.censys.io/_search', headers=headers, params=params) #Запрос к серверу

Теперь, нам нужно начинать парсинг, аналогично с предыдущей функцией используем response.text
Python:
soup = BeautifulSoup(response.text, "html.parser") #Парсим ответ
results=soup.text.replace("\n", "") #Удаляем переносы строк
results=re.sub(' +', ' ', results) #Удаляем пробелы
results=results.split(" ") #Разбиваем по пробелам
for result in results: #Проходимся по каждому элементу
    preresult=str(result).replace("\n", "") #Удаляем переносы строк
    preresult=preresult.replace(" ", "") #Удаляем пробелы
    resultis=(preresult.split("\n")) #Разбиваем по переносам строк
    if(any(c.isalpha() for c in resultis[0])==False and ")" not in resultis[0] and resultis[0]!=""): #Если элемент не содержит буквы и не содержит закрывающую скобку и не пустой
        addr.append(resultis[0])  #Добавляем в лист
return(addr) #Возвращаем нашу переменную
Финальный штрих:
Python:
if __name__ == "__main__":
    SearchByDomain(target)
    SearchByIp(target)

Это нужно, чтобы при импорте файла не выполнились все функции, а просто вызывались только нужные.
В корневой директории проекта создаем папку modules, добавляем туда файл censys.py, в котором будут две только что написанные функции.
Далее, нам нужно создать пустой __init__.py, чтобы Python знал, что это папка с модулями
Теперь мы можем использовать эти функции в нашем проекте.
Заходим в Auxiliator.py, импортируем Censys в начале файла:
Python:
import modules.censys as censys

Переходим к функции
Python:
Переходим к функции def searchbyip(message):

В ней, нам нужно извлечь аргумент, то что идет после команды, в нашем случае это IP адрес.
Используем функцию extract_arg(arg), которую написали в начале статьи:
Python:
IP=extract_arg(message.text)[0]

Далее:
Python:
info="\n".join(censys.SearchByIp(IP))
bot.send_message(message.chat.id, info)

Тоже самое с searchbydomain:
Python:
domain=extract_arg(message.text)[0]
info="\n".join(censys.SearchByDomain(domain))
bot.send_message(message.chat.id, info)

Наши первые две команды готовы!

Scaner:

Создание сканера будет легче, чем парсинг Censys. Я уже выпускал статью о создании сканера и в этой сделаю все кратко.

Создадим в папке modules scaner.py:

Чтобы сканер был достаточно быстрым, мы реализуем многопоточность с помощью следуещего модуля:
Python:
from concurrent.futures import ThreadPoolExecutor

Далее, для подключения к портам мы будем использовать socket:
Python:
import socket

Все будет происходить в функции portscan:
Python:
def portscan(host):
    ports=list(range(1, 65535)) #Создаем список портов
    portsopened=[] #Создаем пустой список для открытых портов

Внутри этой функции будет находится функция для проверки одного порта:
Python:
def scanner(port):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #Создаем сокет
    s.settimeout(0.5) #Устанавливаем таймаут для сокета
    try:
        con = s.connect((host, port)) #Пытаемся подключиться к порту
        portsopened.append(port) #Если подключились, добавляем порт в список открытых
        con.close() #Если подключились, закрываем сокет
    except:
        pass #Если не подключились, просто пропускаем

В конце portscan() прописываем:
Python:
with ThreadPoolExecutor(max_workers=5000) as pool:
    pool.map(scanner, ports)
return portsopened

Теперь мы можем использовать эту функцию в нашем проекте.
Заходим в Auxiliator.py, импортируем наш модуль:
import modules.scanner as scanner
В начале статьи мы создали заготовку:
Python:
@bot.message_handler(commands=['scan'])
def scan(message):

Добавляем сюда:
Python:
target=extract_arg(message.text)[0] #Извлекаем аргумент
bot.send_message(message.chat.id, "Scanning Started!") #Информируем о начале сканирования
openedports=scanner.portscan(target) #Сканируем порты и получаем открытые
info="\n".join(str(port) for port in openedports) #Преобразуем в строку
bot.send_message(message.chat.id, "Opened Ports\n"+info) #Отправляем информацию об открытых портах

Детекторы WAF:

Существует замечательная утилита для выявления фаерволла на питоне, wafw00f.

К сожалению, по умолчанию ее нельзя использовать как модуль, но если подредактировать исходный код с гитхаба, то возможно все:
wafw00f/main.py at master · EnableSecurity/wafw00f, это оригинальный файл который выводит результаты сканирования на WAF.
А это, RollerScanner/wafmeow.py at main · MajorRaccoon/RollerScanner, отредактированный мной main.py, для работы с моим сканером, в нем лишь нужно заменить print на return:

В функции def wafsearch(target, scheme) я уберу все случаи использования цветного текста, заменю print на return и получу:
Python:
res='The site %s%s%s is behind %s%s%s WAF.'
req='Number of requests: %s'
target=scheme+target
results = []
attacker = WAFW00F(target)
waf = attacker.identwaf(True)
if len(waf) > 0:
    for i in waf:
        results.append(buildResultRecord(target, i))
    return(res % (B, target, E, C, (E+' and/or '+C).join(waf), E))
return(req % attacker.requestnumber)

Полученный файл сохраню как wafmeow.py в моей папке modules. Теперь можно использовать его в нашем проекте. Возвращаемся в Auxiliator.py:
Импортируем наш модифицированный модуль:
Python:
undefined

В начале статьи у нас была заготовка:
Python:
@bot.message_handler(commands=['waf'])
def waf(message):

Сюда добавляем:
Python:
target=extract_arg(message.text)[0] #Извлекаем аргумент

Далее, модулю нужно понять какая схема используется, а значит она должна быть в аргументе:
Python:
if("https" in target): #Если в аргументе есть https
    firewall=wafmeow.wafsearch(target.replace("https://", ""), "https://") #То используем https и получаем ответ
    bot.send_message(message.chat.id, firewall) #Отправляем ответ
if("http" in target): #Если в аргументе есть http
    firewall=wafmeow.wafsearch(target.replace("http://", ""), "http://") #То используем http и получаем ответ
    bot.send_message(message.chat.id, firewall) #Отправляем ответ
else:
    bot.send_message(message.chat.id, "No scheme is provided, use either http or https!") #Если нет схемы, то отправляем сообщение об этом

Мы реализовали еще одну команду и у нас осталась только реализация searchsploit.

SearchSploit:

На главной странице exploit-db есть встроенная возможность поиска по базе:

Мы воспользуемся этим и через F12 найдем запрос к серверу:

image_2021-12-24_11-40-20.png


Копируем как curl, заметим, что ответ к нам приходит в виде JSON.

С помощью мы можем конвертировать curl в python.

Получаем полностью рабочий код, который мы добавим в функцию.

Получаем:
Python:
def searchsploit(query):
    results={}
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:93.0) Gecko/20100101 Firefox/93.0',
        'Accept': 'application/json, text/javascript, */*; q=0.01',
        'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
        'Referer': 'https://www.exploit-db.com/',
        'X-Requested-With': 'XMLHttpRequest',
        'Connection': 'keep-alive',
        'Sec-Fetch-Dest': 'empty',
        'Sec-Fetch-Mode': 'cors',
        'Sec-Fetch-Site': 'same-origin',
        'TE': 'trailers',
    }
    params = (
        ('draw', '1'),
        ('columns[0][data]', 'date_published'),
        ('columns[0][name]', 'date_published'),
        ('columns[0][searchable]', 'true'),
        ('columns[0][orderable]', 'true'),
        ('columns[0][search][value]', ''),
        ('columns[0][search][regex]', 'false'),
        ('columns[1][data]', 'download'),
        ('columns[1][name]', 'download'),
        ('columns[1][searchable]', 'false'),
        ('columns[1][orderable]', 'false'),
        ('columns[1][search][value]', ''),
        ('columns[1][search][regex]', 'false'),
        ('columns[2][data]', 'application_md5'),
        ('columns[2][name]', 'application_md5'),
        ('columns[2][searchable]', 'true'),
        ('columns[2][orderable]', 'false'),
        ('columns[2][search][value]', ''),
        ('columns[2][search][regex]', 'false'),
        ('columns[3][data]', 'verified'),
        ('columns[3][name]', 'verified'),
        ('columns[3][searchable]', 'true'),
        ('columns[3][orderable]', 'false'),
        ('columns[3][search][value]', ''),
        ('columns[3][search][regex]', 'false'),
        ('columns[4][data]', 'description'),
        ('columns[4][name]', 'description'),
        ('columns[4][searchable]', 'true'),
        ('columns[4][orderable]', 'false'),
        ('columns[4][search][value]', ''),
        ('columns[4][search][regex]', 'false'),
        ('columns[5][data]', 'type_id'),
        ('columns[5][name]', 'type_id'),
        ('columns[5][searchable]', 'true'),
        ('columns[5][orderable]', 'false'),
        ('columns[5][search][value]', ''),
        ('columns[5][search][regex]', 'false'),
        ('columns[6][data]', 'platform_id'),
        ('columns[6][name]', 'platform_id'),
        ('columns[6][searchable]', 'true'),
        ('columns[6][orderable]', 'false'),
        ('columns[6][search][value]', ''),
        ('columns[6][search][regex]', 'false'),
        ('columns[7][data]', 'author_id'),
        ('columns[7][name]', 'author_id'),
        ('columns[7][searchable]', 'false'),
        ('columns[7][orderable]', 'false'),
        ('columns[7][search][value]', ''),
        ('columns[7][search][regex]', 'false'),
        ('columns[8][data]', 'code'),
        ('columns[8][name]', 'code.code'),
        ('columns[8][searchable]', 'true'),
        ('columns[8][orderable]', 'true'),
        ('columns[8][search][value]', ''),
        ('columns[8][search][regex]', 'false'),
        ('columns[9][data]', 'id'),
        ('columns[9][name]', 'id'),
        ('columns[9][searchable]', 'false'),
        ('columns[9][orderable]', 'true'),
        ('columns[9][search][value]', ''),
        ('columns[9][search][regex]', 'false'),
        ('order[0][column]', '9'),
        ('order[0][dir]', 'desc'),
        ('start', '0'),
        ('length', '15'),
        ('search[value]', query),
        ('search[regex]', 'false'),
        ('author', ''),
        ('port', ''),
        ('type', ''),
        ('tag', ''),
        ('platform', ''),
        ('_', '1636022484613'),
    )
    response = requests.get('https://www.exploit-db.com/', headers=headers, params=params)

В headers мы задаем параметры, чтобы нас не заблокировала защита от ботов, если она конечно есть. В params стоят параметры поиска, сам текст поиска содержится в search[value]. В последней строке мы отправляем запрос на сервер с нашими параметрами.

Мы получим неупорядоченный JSON, поэтому нам понадобится библиотека json:
Python:
import json.

Теперь начнем сортировку, в data будут содержаться нужные нам элементами, поэтому для каждого из них мы будем
Python:
for element in data["data"]:
    index=data["data"].index(element)
    results["https://www.exploit-db.com/exploits/"+data["data"][index]["id"]] = data["data"][index]["description"][1]
return(results)

Модуль готов к работе с ботом. Заходим в Auxiliator.py, используем нашу заготовку из начала статьи:
Python:
@bot.message_handler(commands=['searchsploit'])
def searchsploit(message):
query=extract_arg(message.text)[0] #Получаем запрос для поиска
info=exploitsearch.searchsploit(query) #Ищем эксплоиты, получаем словарь
for exploit in info: #Для каждой ссылки на эксплоит
    bot.send_message(message.chat.id, exploit+": "+info[exploit]) #Отправляем ссылку на эксплоит вместе с названием

Под конец, во все команды я добавил исключения, чтобы бот не крашнулся при опечатке.
Python:
try:
    Код, код, код
except:
    bot.send_message(message.chat.id, "Error!")

Тесты:
Предположим, у нас есть сайт artscp.com и телеграм бот. Сначала посмотрим есть ли у сайта защита фаерволла:
Код:
/waf https://artscp.com

Нам приходит ответ:
Код:
The site https://artscp.com is behind Cloudflare (Cloudflare Inc.) WAF.

Значит смысла в сканировании сейчас нету, попробуем найти настоящий IP:
Получаем:
95.216.162.249
130.193.62.38

Первый принадлежит app.artscp.com, второй artscp.com. Сканируем 130.193.62.38:
/scan 130.193.62.38

Спустя ~8 секунд получаю список:
Код:
Opened Ports
22
80
443

Теперь хочется узнать поподробнее о том что стоит на портах:
Код:
/searchbyip 130.193.62.38

Получаю достаточное кол-во инфы:
Код:
Protocol: SSH is on port: 22
Protocol: HTTP is on port: 80
Protocol: HTTP is on port: 443
OS:Ubuntu Linux 18.04
Network:YANDEXCLOUD(RU)
Routing:130.193.62.0/24 via AS200350
22/SSH:
Software:
-linux
CPE:
-cpe:2.3:o:*:linux:*:*:*:*:*:*:*:*
Ubuntu Linux:
Version:
-18.04
CPE:
-cpe:2.3:o:canonical:ubuntu_linux:18.04:*:*:*:*:*:*:*
OpenBSD OpenSSH:
Version:
-7.6
CPE:
-cpe:2.3:a:openbsd:openssh:7.6:p1:*:*:*:*:*:*

80/HTTP:
Software:
-nginx
CPE:
-cpe:2.3:a:nginx:nginx:*:*:*:*:*:*:*:*

443/HTTP:
Software:
-nginx
CPE:
-cpe:2.3:a:nginx:nginx:*:*:*:*:*:*:*:*

Попробуем найти эксплоиты для openssh 7.6:
Код:
/searchsploit openssh 7

Из ответа получаем следующие подходящие по версии эксплоиты:

Они могут быть полезны при дальнейшем изучении и эксплуатации машины, когда мы наконец-то доберемся до основной рабочей станции.

Весь код можно найти здесь
 
Последнее редактирование:
Мы в соцсетях:

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