• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья OSINT сайтов, или как собрать много информации.

acc2ss

(L3) cache
Пользователь
Регистрация
26.08.2023
Сообщения
161
Реакции
222
Гарант сделки
2
Депозит
0.00
Автор: acc2ss
источник:
xss.pro

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

Перед началом основной части нужно понять, а зачем это вообще нужно? Что можно узнать о сайте? Что можно сделать с этой информацией?
Я бы сказал что осинт - самый первый этап в обработке таргета(после его поиска конечно), поэтому ему нужно уделять не меньше внимания, чем любым другим этапам. Во время осинта мы можем найти множество полезной для нас в будущем информации: начиная от оригинального ip сервера, заканчивая психозом от акамаи админками где логин и пароль 123.
Сам осинт проводится с помощью различных сервисов, в ручную вы вряд ли сможете собрать много информации.
Сегодня мы будем собирать такую информацию как:

1. оригинальный ip сайта
2. поддомены
3. технологии
4. порты
5. потенциально уязвимые страницы, параметры, формы
6. технические страницы
7. изучение js кода и т.п
8. WAF и возможность его обойти

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

Таргет на сегодняшнюю статью будет - https://e-shop.robotis.co.jp/
Начнем с попыток поиска оригинального ip. Это можно сделать через сервисы: Fofa, Censys, Shodan. Как видим таргет состоит из поддомена и основного домена, если попробовать убрать поддомен, то нас будет редиректить обратно, так что для начала я хочу проверить трагет с поддоменом. вставляем во все сервисы e-shop.robotis.co.jp и смотрим результат.
1721199983876.png

censys сразу выдал ip, и если его проверить, то нас перекидывает на сайт. Хоть он и будет отличаться от того что с поддоменом, но мы можем увидеть что ip принадлежит тому сайту что мы ищем. P.S советую искать сразу на censys, он чаще всех остальных показывает ip.
Интересный факт(для тех кто не знал) - fofa иногда может показывать связанные с доменом поддомены:
1721204851754.png

Достаточно удобная фишка, в censys я такого не видел. Если сравнивать одинаковые запросы в этих 2-х сервисах, то fofa покажет больше информации:
1721204953156.png

Было слишком просто, поэтому сделаем вид что ip мы сразу не нашли :)

Поиск поддоменов можно проводить как через сервисы, так и самому, скриптами. Из сервисов я могу порекомендовать securitytrails.com. Вводим домен и видим все поддомены:
1721199995984.png

Если оригинальный ip не был найден сразу, то можно проверить поддомены, иногда через них тоже можно найти. Так и сделаем. Пробуем вставить поддомены и видим такой результат:
Никакие поддомены не показали оригинального ip. Тут как повезет.

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

Здесь для нас может быть полезным знать что используется php, клауд и jquery.

Поиск портов можно проводить через сервисы, либо через nmap. Тут на ваше усмотрение, но я буду использовать свой сервис. Вводим либо оригинальный ip(если уже нашли), либо домен сайта. На нашем таргете открыты 3 порта:
1721200009518.png

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

Потенциально уязвимые страницы, параметры и формы можно искать в ручную. У меня есть скрипт для поиска форм и ссылок, я буду использовать его. Если вам не лень, то можете поискать скрипты на гитхабе :)
Python:
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin, urlparse
import logging
from concurrent.futures import ThreadPoolExecutor, as_completed
import argparse
import time
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

def get_all_forms(url):
    logging.info(f"Fetching forms from: {url}")
    try:
        response = requests.get(url, verify=False)
    except requests.exceptions.SSLError as e:
        logging.error(f"SSL error for {url}: {e}")
        return []
    except Exception as e:
        logging.error(f"Error fetching {url}: {e}")
        return []
 
    soup = BeautifulSoup(response.text, "html.parser")
    return [(form.get('action'), form) for form in soup.find_all("form")]

def get_all_links(url, base_domain):
    logging.info(f"Fetching links from: {url}")
    try:
        response = requests.get(url, verify=False)
    except requests.exceptions.SSLError as e:
        logging.error(f"SSL error for {url}: {e}")
        return set()
    except Exception as e:
        logging.error(f"Error fetching {url}: {e}")
        return set()
 
    soup = BeautifulSoup(response.text, "html.parser")
    links = set()
    for link in soup.find_all("a"):
        href = link.get("href")
        if href and not href.startswith("javascript:void(0)"):
            full_url = urljoin(url, href)
            parsed_link = urlparse(full_url)
            if parsed_link.netloc == "" or parsed_link.netloc == base_domain:
                links.add(full_url)
    return links


def crawl_page(url, depth, max_depth, delay, base_domain):
    if depth > max_depth:
        return [], []

    time.sleep(delay)

    forms = get_all_forms(url)
    links = get_all_links(url, base_domain)
    return forms, links

def crawl_site(start_url, max_depth=2, max_workers=10, delay=1):
    visited = set()
    to_visit = [(start_url, 0)]
    forms_data = []
    links_data = []
    base_domain = urlparse(start_url).netloc
 
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        futures = {executor.submit(crawl_page, url, depth, max_depth, delay, base_domain): (url, depth) for url, depth in to_visit}
     
        while futures:
            for future in as_completed(futures):
                url, depth = futures[future]
                try:
                    forms, links = future.result()
                    if forms:
                        forms_data.append((url, forms))
                    if links:
                        links_data.append((url, links))
                    visited.add(url)
                 
                    for link in links:
                        if link not in visited and (link, depth + 1) not in futures:
                            futures[executor.submit(crawl_page, link, depth + 1, max_depth, delay, base_domain)] = (link, depth + 1)
                except Exception as e:
                    logging.error(f"Error visiting {url}: {e}")
             
                del futures[future]
 
    return forms_data, links_data

def save_to_file(forms_data, links_data, filename="output.txt"):
    with open(filename, "w", encoding='utf-8') as f:
        f.write("Forms:\n")
        for url, forms in forms_data:
            f.write(f"URL: {url}\n")
            for action, form in forms:
                f.write(f"Form action: {action}\n")
            f.write("\n")
     
        f.write("\nLinks:\n")
        for url, links in links_data:
            f.write(f"URL: {url}\n")
            for link in links:
                f.write(f"{link}\n")
            f.write("\n")

def main():
    parser = argparse.ArgumentParser(description="Web Crawler")
    parser.add_argument("url", help="URL for crawler")
    parser.add_argument("--depth", type=int, default=2, help="Maximum depth to crawl")
    parser.add_argument("--threads", type=int, default=10, help="Number of threads to use")
    parser.add_argument("--output", type=str, default="output.txt", help="Output file")
    parser.add_argument("--delay", type=float, default=1, help="Delay between requests in seconds")

    args = parser.parse_args()

    parsed_url = urlparse(args.url)
    if parsed_url.scheme == "https":
        try:
            response = requests.get(args.url, verify=False)
            response.raise_for_status()
        except requests.exceptions.SSLError:
            args.url = parsed_url._replace(scheme="http").geturl()

    forms_data, links_data = crawl_site(args.url, max_depth=args.depth, max_workers=args.threads, delay=args.delay)
 
    save_to_file(forms_data, links_data, filename=args.output)

    for url, forms in forms_data:
        logging.info(f"URL: {url} - Found {len(forms)} forms")
        for action, form in forms:
            logging.info(f"Form action: {action}")

    for url, links in links_data:
        logging.info(f"URL: {url} - Found {len(links)} links")
        for link in links:
            logging.info(link)

if __name__ == "__main__":
    main()
Пример запуска: py main.py https://121.78.116.92/ --depth 2 --threads 10 --delay 0.5 --output results.txt
(--help для помощи)

После поиска всех страниц и форм можно приступать к "Фильтрации" полученных данных. Нас интересуют ссылки где есть формы и ссылки с параметрами. Сидим, смотрим, ищем и выписываем то что нужно.
У меня получился такой небольшой список:
1721200020298.png

Форма 1 и есть на всех страницах, поэтому здесь ее нет.

Для поиска технических страниц так же будем использовать скрипт. Если вам не лень, то можете поискать другие.
Python:
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin, urlparse
import logging
from concurrent.futures import ThreadPoolExecutor, as_completed
import argparse
import time
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

def load_technical_pages(filename):
    try:
        with open(filename, 'r', encoding='utf-8') as f:
            return [line.strip() for line in f if line.strip()]
    except Exception as e:
        logging.error(f"Ошибка при чтении файла {filename}: {e}")
        return []

def get_all_forms(url):
    logging.info(f"Fetching forms from: {url}")
    try:
        response = requests.get(url, verify=False)
        response.raise_for_status()
    except requests.exceptions.SSLError as e:
        logging.error(f"SSL error for {url}: {e}")
        return []
    except Exception as e:
        logging.error(f"Error fetching {url}: {e}")
        return []

    soup = BeautifulSoup(response.text, "html.parser")
    return [(form.get('action'), form) for form in soup.find_all("form")]

def get_all_links(url, base_domain):
    logging.info(f"Fetching links from: {url}")
    try:
        response = requests.get(url, verify=False)
        response.raise_for_status()
    except requests.exceptions.SSLError as e:
        logging.error(f"SSL error for {url}: {e}")
        return set()
    except Exception as e:
        logging.error(f"Error fetching {url}: {e}")
        return set()

    soup = BeautifulSoup(response.text, "html.parser")
    links = set()
    for link in soup.find_all("a"):
        href = link.get("href")
        if href and not href.startswith("javascript:void(0)"):
            full_url = urljoin(url, href)
            parsed_link = urlparse(full_url)
            if parsed_link.netloc == "" or parsed_link.netloc == base_domain:
                links.add(full_url)
    return links

def check_technical_page(url):
    logging.info(f"Checking technical page: {url}")
    try:
        response = requests.get(url, verify=False)
        if response.status_code == 200:
            logging.info(f"Technical page found: {url}")
            return url
    except Exception as e:
        logging.error(f"Error checking {url}: {e}")
    return None

def check_technical_pages(base_url, technical_pages, visited):
    found_pages = []
    with ThreadPoolExecutor() as executor:
        futures = {executor.submit(check_technical_page, urljoin(base_url, page)): page for page in technical_pages if urljoin(base_url, page) not in visited}
        for future in as_completed(futures):
            page = futures[future]
            result = future.result()
            if result:
                found_pages.append(result)
                visited.add(result)
    return found_pages

def crawl_page(url, depth, max_depth, delay, base_domain, search_type, technical_pages, visited):
    if search_type == "technical":
        time.sleep(delay)
        return [], check_technical_pages(url, technical_pages, visited)

    if depth > max_depth:
        return [], []

    time.sleep(delay)
  
    forms = get_all_forms(url)
    links = get_all_links(url, base_domain)
    return forms, links

def crawl_site(start_url, search_type, technical_pages, max_depth=2, max_workers=10, delay=1):
    visited = set()
    to_visit = [(start_url, 0)]
    forms_data = []
    links_data = set()
    base_domain = urlparse(start_url).netloc

    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        futures = {executor.submit(crawl_page, url, depth, max_depth, delay, base_domain, search_type, technical_pages, visited): (url, depth) for url, depth in to_visit}
    
        while futures:
            for future in as_completed(futures):
                url, depth = futures[future]
                try:
                    forms, links = future.result()
                    if forms:
                        forms_data.append((url, forms))
                    if links:
                        links_data.update(links)
                    visited.add(url)
                
                    for link in links:
                        if link not in visited and (link, depth + 1) not in futures:
                            futures[executor.submit(crawl_page, link, depth + 1, max_depth, delay, base_domain, search_type, technical_pages, visited)] = (link, depth + 1)
                except Exception as e:
                    logging.error(f"Error visiting {url}: {e}")
            
                del futures[future]
 
    return forms_data, links_data

def save_to_file(forms_data, links_data, found_technical_pages, filename="output.txt"):
    with open(filename, "w", encoding='utf-8') as f:
        f.write("Forms:\n")
        for url, forms in forms_data:
            f.write(f"URL: {url}\n")
            for action, form in forms:
                f.write(f"Form action: {action}\n")
            f.write("\n")
    
        f.write("\nLinks:\n")
        for link in links_data:
            f.write(f"{link}\n")
        f.write("\n")

        f.write("\nFound Technical Pages:\n")
        for page in found_technical_pages:
            f.write(f"{page}\n")
        f.write("\n")

def main():
    parser = argparse.ArgumentParser(description="Web Crawler")
    parser.add_argument("url", help="URL for crawler")
    parser.add_argument("--depth", type=int, default=2, help="Maximum depth to crawl")
    parser.add_argument("--threads", type=int, default=10, help="Number of threads")
    parser.add_argument("--output", type=str, default="output.txt", help="Output file")
    parser.add_argument("--delay", type=float, default=1, help="Delay between requests in seconds")
    parser.add_argument("--teh", action='store_true', help="Search technical pages")
    parser.add_argument("--fl", action='store_true', help="Search forms and links")
    parser.add_argument("--tech-file", type=str, required=True, help="File technical pages")

    args = parser.parse_args()

    if not (args.teh or args.fl):
        logging.error("You must specify either --teh or --fl.")
        return

    technical_pages = load_technical_pages(args.tech_file)

    parsed_url = urlparse(args.url)
    if parsed_url.scheme == "https":
        try:
            response = requests.get(args.url, verify=False)
            response.raise_for_status()
        except requests.exceptions.SSLError:
            args.url = parsed_url._replace(scheme="http").geturl()

    search_type = "technical" if args.teh else "forms_links"
    forms_data, links_data = crawl_site(args.url, search_type, technical_pages, max_depth=args.depth if search_type == "forms_links" else None, max_workers=args.threads, delay=args.delay)
 
    save_to_file(forms_data, links_data, links_data, filename=args.output)

    for url, forms in forms_data:
        logging.info(f"URL: {url} - Found {len(forms)} forms")
        for action, form in forms:
            logging.info(f"Form action: {action}")

    for link in links_data:
        logging.info(f"Found link: {link}")

if __name__ == "__main__":
    main()
Это тот же скрипт что выше, но немного переделанный. Пример запуска: py main.py https://121.78.116.92/ --teh --threads 10 --output output.txt --tech-file tech.txt --delay 0.1
Поиск занимает долгое время, поэтому для примера я вставил страницы которые есть на сайте.
1721201843516.png

wordlist'ы вы можете собрать с гитхаба, их там горы.

И так, к этому моменту мы собрали уже достаточно много информации для дальнейшей работы:
1721201934815.png

еще 1 пункт - изучения js кода. Это достаточно муторный процесс, и делать его вы вряд ли будете. Рассказывать и показывать я тоже не буду. Я не мазохист :) просто смотрите код на предмет интересных строчек.
Возможно вы найдете отправку данных из js в бэкэнд. Что то типо такого:
JavaScript:
async function sendData() {
    const data = { key: "value" };

    try {
        const response = await fetch('http://1.1.1.1:5000/popa', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(data)
        });

        const result = await response.json();
        console.log(result);
    } catch (error) {
        console.error('Ошибка:', error);
    }
}
sendData();

Определение WAF. наверно самый простой этап. Его можно узнать через wappalyzer, chek-host, либо скриптами - например Whatwaf. Попробуем запихнуть наш таргет в скрипт и посмотрим что он даст.
К сожалению инструмент мне ничего не показал, поэтому смотрим wappalyzer и видим клауд. Вообщем все просто. Так же сюда можно добавить попытки обхода WAF в случае если не нашли ориг ip. Ищем параметры или формы, пробуем вставить пэйлоад и если видим 403, то пробуем использовать тамперы. Это уже не совсем тема осинта.

Еще одним вариантом поиска поддоменов и ip является - burp. Если поиск поддоменов понятен сразу всем, то с ip у некоторых могут появится вопросы.

Поиск поддоменов через burp:
Открываем сайт, заходим в Proxy -> http history и отправляем таргет в интрудер. ставим §§ перед доменом и меняем хост на основной домен.
1721202553628.png

В пэйлоадах вписываем поддомены и запускаем атаку. По окончанию сортируем по status code и получаем рабочие поддомены:
1721202641831.png


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

И так, основные этапы я вроде рассказал. Посмотрим еще раз что мы нашли:
1721204306466.png

Информации достаточно много. Ip можно использовать для обхода WAF, поддомены для поиска других язв либо ip, технологии для понимания из чего состоит сайт, порты для знания открытых служб, формы и параметры для поиска язв, тех страницы для поиск других уязвимостей.

То что я показал в статье не является полным "мануалом" для осинта, т.к все используют разные инструменты, которые могут показывать менее/более полную информацию.
Если у вас есть какие либо вопросы касающиеся пентестинга - смело пишите в теме/лс форума/tox Отвечу всем по мере возможности.

Так же пока есть такая возможность, оставлю ссылку на свой сервис - https://domain-scanner.dosx.su/ все пока в ранней бетке и работает не всегда, но улучшения потихоньку идут. Все абсолютно бесплатно. Из функционала:
1721206450053.png

Поддержать разработку сайта и автора:
USDT trc(20) TNjxY6W1buZWP47WFi8BMXKhKaUr4U5hTi
BTC bc1qnhu6nfawzx9crfr3vvr65zrjdjrz3et7qajd4e
 
Последнее редактирование модератором:
это рикрол? нет, это xss.pro

Посмотреть вложение 89795

можно докрутить, но мне сказали что не надо
Я в этой вресии никаких дыр и не закрывал :)
Щас редизайн делаю, там уже подзакрою все
 
Просто шикарная статья! Особая ценность в том, что используется подход с самописными tools. Хотя конечно искать запросы с параметрами приятнее через Burp Suite, даже Community Edition это умеет.
Тема SQLi через параметры раскрыта во многих статьях хорошо, а вот темы с LFI и IDOR (и даже RCE бывает иногда через параметры!) как-то обычно мало раскрываются, а очень жаль.
SQLmap тул известный, а интересно есть ли что-то подобное для LFI например?
 
Что можно сделать в случаях, когда IP за клаудом известен, но прямое подключение к нему запрещенно?
Какие есть варианты?
Если речь идёт об HTTP(s) протоколе, то например, в некоторых случаях подключаясь к reverse proxy Web серверу (это например "nginx, за которым апач") и указывая "Host: 192.168.1.x" в заголовке (обычно делаю в Burp) можно даже "пошариться" по intranet (локалке) :)
Это если reverse proxy Web server сконфигурирован как попало, что нередкое явление.
 
Чтобы не заходить на каждый ресурс и не смотреть присутствует на нём WAF или же нет, или какой там WAF - можно написать небольшой скрипт, который будет прогонять список собранных IP адресов с какого-то агрегатора(Censys/Shodan/Fofa/Zoomeye/другие) на соответствие CIDR подсети. У всех WAF-ов имеется такая информация в публичном доступе. Например, отсюда можно вытащить большинство таких зон: https://github.com/stamparm/maltrai...d0e956bda5597ec55f003dbd5/misc/cdn_ranges.txt

В файл target_ips.txt запихиваешь IP набор из твоих спаршенных таргетов
В файле waf_ranges.txt хранишь CIDR записи об WAF сервисах
Python:
import ipaddress
import asyncio
import aiofiles

async def read_target_ips(read_path='./target_ips.txt'):
    async with aiofiles.open(file=read_path, mode='r') as target_ranges:
        return [line.strip() for line in await target_ranges.readlines()]

# https://github.com/stamparm/maltrail/blob/0ab74279149da62d0e956bda5597ec55f003dbd5/misc/cdn_ranges.txt
async def read_waf_ranges(read_path='./waf_ranges.txt'):
    async with aiofiles.open(file=read_path, mode='r') as waf_ranges:
        lines = await waf_ranges.readlines()
        return [ipaddress.ip_network(line.strip()) for line in lines]

async def filter_waf_ips():
    target_ips = await read_target_ips()
    waf_ranges = await read_waf_ranges()
    clear_ips = set()
    for ip in target_ips:
        ip_addr = ipaddress.ip_address(ip)
        if any(ip_addr in net for net in waf_ranges):
            clear_ips.add(ip)
    return clear_ips

if __name__ == '__main__':
    non_waf_ips = asyncio.run(filter_waf_ips())
    print(non_waf_ips)


SQLmap тул известный, а интересно есть ли что-то подобное для LFI например?
LFISuite
LFIMap
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх