СТАТЬЯ AppSec

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

Revoltage
Revoltage Newbie · 42 сообщений
Подписаться
11,1 тыс.
[ обложка статьи ]
Режим чтения
Введение:

в.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 имеют вид: https://search.censys.io/hosts/{ЦЕЛЬ}, делаем запрос вместе с 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.

С помощью Convert curl commands to code мы можем конвертировать 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
Из ответа получаем следующие подходящие по версии эксплоиты:



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

Весь код можно найти здесь
Полезно · 11

Комментарии

1
не хитро придумал то так мне нравится возьму на воружение
 
  • Нравится
Реакции: ace911, Vladislav Orlov и ROP
3