What's new
Runion

This is a sample guest message. Register a free account today to become a member! Once signed in, you'll be able to participate on this site by adding your own topics and posts, as well as connect with other members through your own private inbox!

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

acc2ss

Midle Weight
Депозит
$0
Автор: acc2ss
источник:
runion.su

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

Перед началом основной части нужно понять, а зачем это вообще нужно? Что можно узнать о сайте? Что можно сделать с этой информацией?
Я бы сказал что осинт - самый первый этап в обработке таргета(после его поиска конечно), поэтому ему нужно уделять не меньше внимания, чем любым другим этапам. Во время осинта мы можем найти множество полезной для нас в будущем информации: начиная от оригинального 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: Скопировать в буфер обмена
Code:
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: Скопировать в буфер обмена
Code:
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: Скопировать в буфер обмена
Code:
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
 
Top