LIMB
Вход Регистрация

тема

Бесплатный парсер парсера Shodan для поиска целей

mike_vazovski · · 3 ответов · 210 просмотров

Целью данной статьи будет показать одну интересную лазейку с помощью которой можно будет абсолютно легально, хоть и не так быстро выгружать из Shodan информацию по интересующим нас целям

И так, начнем.

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


Далее заезженная схема:
1) Абуз линукс 
2) Скрипт оркестратор массканов по открытым портам
3) Диапазоны подсетей CIDR из гитхаба
4) несколько дней мониторинга и ручной проверки
5) очистка строк от мусора
6) оркестратор на питоне который будет брать выгрузку масскана и детектить банеры по ключевым полям которые другой парсер будет ловить из 200-х responce http ответов
Заеб лютый, постоянно думаешь как чистить чанки со строками, как быть если диапазон глухой, или напоролся на cloudflare и тот сосет ресурс абуза и не отпускает соединение, одним словом - дрочь
К чему я это все?

Внимание на экран
обновляем страницу
(попытка 0)
Не удалось прогрузить медиа

 

попытка 1
Не удалось прогрузить медиа



для тех кто не заметил разницы - при обновлении запросов мы получаем разные ответы
а что если взять Дрочер контейнер на кали климакс, написать парсер не Selenium который будет имитировать поведение реального браузера, парсить ответы по html тегам и результат парсинга помещать в централизованную БД которая будет принимать информацию по уникальному ключу вида ip:port

Как говорил мой хороший приятель - ГАЗ!


какие требования мы предъявлеям к нашей системе основываясь на прошлом опыте?

1) Изоляция
2) На больших дистанциях и разнобойной географии - ебанутая изоляция всего
3) Централизованная база данных с метаданными - Страна, организация, доп поля (на всякий случай)
4) Логирования request и responce http а также дебаг в терминал чтобы не прерывая работы видеть особенные диапазоны и понимать вообще, может быть на той стороне уже что-то поняли 
5) Гибкость - нам нужно по одной строчке или конфигу сказать системе делай также, но: в других масштабах, по другим фильтрам, и критериям
6) В случае когда мы говорим про базу данных - нужно понять, какую нагрузку она должна выдерживать если 10 воркеров будут слать по 10 строк каждые 5 секунд
7) А если в бд уже 5000+ строк, она сможет каждые 5 секунд прогонять 50 строк на уникальность чтобы не было дублей и избыточной информации?


Идем дальше, какие требования мы предъявляем к коду и как оно должно работать?

Не удалось прогрузить медиа

 
С помощью инструментов разработчика я посмотрел как нужно сформировать запрос http get
Не удалось прогрузить медиа



отсюда мы можем получить информацию от имени того, кого потом будет дрочить и приказывать заебывать терпилу по имени шодан таким образом, чтобы он понимал что его заебывает настоящая марионетка в лице браузера с настоящим фингер принтом и коректными полями которые так важны
Bash:
curl 'https://www.shodan.io/search?query=FORTINET' \
  -H 'accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8' \
  -H 'accept-language: ru-RU,ru;q=0.5' \
  -H 'cache-control: max-age=0' \
  -b 'cf_clearance=zTwU1StnLnzduThbl4Sf5D5Opk2bSkqwx4igTRsckK4-1779574426-1.2.1.1-we7f4OVKLHphex_cASN9sZS5EmCeB1J62iIWi1vnfUGWsJ1w7G0oS0vh6_HgtrbwKBQ53IgDlzf6kvBgwz1sRP.Xws4czsCYtrfavB4Chk5aAdH2Icr1NdYLFu34BTZTtIfXMCBSnL6mDlaKAFbhZiugF7VMe4hQ90.Bi5fGQEgXCG7uRl1ZsMVqAcsrrNwN7UIVD3K1fTXVW8iMKz3P5zBpdUExtX9_odRxgNXr5nWxLZua6iDbosK3pGetA_QKmlql5k3fFoF9M3ymuWSVb7Ik7YPR69MpgUtQvXa5D4aMNgLBRW.X1cQ7yCghr.lBfHFF30kQzcZMKMgwQYuB8cvAyDkvx5qs42Hi2PLfB7gCTnVM_JurZ_Ge7ZLaw7iieqqpRdnM5_IUN9vU0cN7IKlgy3zEaHBXCxRPZkbDyTc' \
  -H 'priority: u=0, i' \
  -H 'referer: https://www.shodan.io/' \
  -H 'sec-ch-ua: "Chromium";v="148", "Brave";v="148", "Not/A)Brand";v="99"' \
  -H 'sec-ch-ua-arch: "x86"' \
  -H 'sec-ch-ua-bitness: "64"' \
  -H 'sec-ch-ua-full-version-list: "Chromium";v="148.0.0.0", "Brave";v="148.0.0.0", "Not/A)Brand";v="99.0.0.0"' \
  -H 'sec-ch-ua-mobile: ?0' \
  -H 'sec-ch-ua-model: ""' \
  -H 'sec-ch-ua-platform: "Windows"' \
  -H 'sec-ch-ua-platform-version: "19.0.0"' \
  -H 'sec-fetch-dest: document' \
  -H 'sec-fetch-mode: navigate' \
  -H 'sec-fetch-site: same-origin' \
  -H 'sec-fetch-user: ?1' \
  -H 'sec-gpc: 1' \
  -H 'upgrade-insecure-requests: 1' \
  -H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36'

весь этот фарш мы оставляем маринтоваться и помозгуем другое

Большинство парсеров работают через обычные HTTP-запросы (библиотеки requests, httpx, curl). Это быстро, но легко обнаруживается — Cloudflare и другие системы защиты смотрят на TLS-fingerprint, заголовки, поведение соединения и сразу понимают что запрос пришёл не от человека.
Данный парсер использует принципиально другой подход: он запускает настоящий браузер Chrome/Chromium и управляет им через протокол ChromeDriver. С точки зрения Shodan и Cloudflare — это обычный пользователь открыл браузер.

​

Как работает обход защиты​

1. Настоящий браузер, не эмуляция​

Библиотека undetected-chromedriver запускает реальный процесс Chromium. Браузер выполняет настоящий TLS-хендшейк с правильным fingerprint, загружает JavaScript, строит DOM, выполняет все проверки — точно так же как это делает живой пользователь.

2. Патч ChromeDriver​

Стандартный ChromeDriver добавляет в браузер JavaScript-переменные (window.navigator.webdriver = true), по которым сайты обнаруживают автоматизацию. undetected-chromedriver патчит бинарный файл ChromeDriver перед запуском, убирая эти маркеры

3. Автоматическое прохождение Cloudflare Challenge​

Когда Cloudflare показывает страницу проверки ("Just a moment..."), браузер честно её проходит — выполняет JavaScript-задачу, получает cookie cf_clearance и продолжает работу. Парсер ждёт до 20 секунд пока появятся результаты.
Python:
# Ждём появления результатов или Cloudflare challenge
deadline = time.time() + 20
while time.time() < deadline:
    html = driver.page_source
    if 'class="result"' in html:
        break  # результаты загрузились
    if "just a moment" in html.lower():
        time.sleep(2)  # ждём пока Cloudflare отпустит
        continue
    time.sleep(1)

4. Случайные паузы (jitter)​

Между раундами добавляется случайный разброс времени чтобы не создавать подозрительный ритмичный паттерн запросов:
Python:
sleep_sec = interval + random.uniform(-jitter, jitter)
# interval=30, jitter=5 → пауза от 25 до 35 секунд
Компоненты:
КомпонентНазначениеundetected-chromedriverЗапуск и управление Chromium без детектированияseleniumПротокол взаимодействия с браузером (ChromeDriver)BeautifulSoupПарсинг HTML страницы результатовSQLiteХранение уникальных хостов (UNIQUE по ip+port)

Что собирается​

С каждой страницы Shodan парсер извлекает:
ПолеПримерip81.242.255.136port443countryBelgiumorganizationProximus NVhostnamemail.example.comseen_at2026-05-24T12:00:00

Дубликаты исключаются на уровне БД: пара (ip, port) уникальна. Один IP может появляться несколько раз если открыт на разных портах.

Запуск в Docker​

Docker решает все проблемы с зависимостями: нужная версия Python, Chromium, патчи — всё внутри образа.

Bash:
# Собрать образ
docker compose build
# Запустить в фоне
docker compose up -d
# Логи
docker compose logs -f
# Остановить
docker compose down

Результаты сохраняются в ./data/shodan.db и ./data/shodan.csv на хосте.
Параметры задаются в docker-compose.yml:
YAML:
command:
  - --query
  - FORTINET      # поисковый запрос
  - --interval
  - "30"          # секунд между раундами
  - --pages
  - "3"           # страниц за раунд

Параметры командной строки​

Code:
--query      Поисковый запрос (default: FORTINET)
--interval   Секунд между раундами (default: 30)
--pages      Страниц за один раунд (default: 1)
--rounds     Количество раундов, 0 = бесконечно (default: 0)
--output     Путь к файлу БД (default: shodan_results.db)
--headless   Без окна браузера
--export-csv Только экспорт БД в CSV, без парсинга

Зависимости
Code:
undetected-chromedriver==3.5.5
selenium>=4.0.0
beautifulsoup4>=4.12.0
setuptools
packaging


Для удобства рекомендую пользоваться DockerDesktop, в нем можно быстро скачаивать готовые образы, той же кали линукс
Не удалось прогрузить медиа

Не удалось прогрузить медиа



Не удалось прогрузить медиа



Теперь давайте поговорим про код
Python:
#!/usr/bin/env python3
"""
Shodan scraper через живой браузер (undetected-chromedriver).
Cloudflare обходится автоматически — никаких cookie вручную.

Установка:
    pip install undetected-chromedriver selenium beautifulsoup4

Использование:
    python shodan_scraper.py --query FORTINET --interval 30
    python shodan_scraper.py --query FORTINET --interval 30 --headless   # без окна
    python shodan_scraper.py --query FORTINET --rounds 5 --pages 3
"""

import argparse
import csv
import re
import sqlite3
import time
import random
from datetime import datetime
from pathlib import Path

from bs4 import BeautifulSoup

# ── ANSI ──────────────────────────────────────────────────────────────────────
R="\033[0m"; BOLD="\033[1m"; DIM="\033[2m"
RED="\033[31m"; GREEN="\033[32m"; YELLOW="\033[33m"; CYAN="\033[36m"; WHITE="\033[97m"
def c(t,*cc): return "".join(cc)+str(t)+R
def hr(ch="─",w=90,col=DIM): print(c(ch*w,col))

# ── Terminal output ────────────────────────────────────────────────────────────
def print_header(query,interval,db_path,headless):
    hr("═",col=CYAN)
    mode = "HEADLESS" if headless else "VISIBLE BROWSER"
    print(c("  SHODAN SCRAPER",BOLD,CYAN)+c(f"  [{mode}]  query={query!r}  interval={interval}s  db={db_path}",DIM))
    hr("═",col=CYAN)

def print_request(rnd,page,url):
    print()
    hr()
    print(c(f"  ▶  ROUND {rnd}  PAGE {page}",BOLD,YELLOW))
    print(c("     URL : ",DIM)+c(url,WHITE))

def print_response(nbytes,ms,title=None):
    line=c(f"     OK  {nbytes} bytes  {ms:.0f} ms",GREEN,BOLD)
    if title:
        line += c(f"  <title>{title[:60]}</title>",DIM)
    print(line)

def print_table(records,new_flags):
    COL=[18,6,22,36]
    HDR=["IP","PORT","COUNTRY","ORGANIZATION"]
    sep="┼".join("─"*(w+2) for w in COL)
    rfmt="│".join(f" {{:<{w}}} " for w in COL)
    print()
    print(c("  "+"┬".join("─"*(w+2) for w in COL),DIM))
    print(c("  "+rfmt.format(*HDR),BOLD,CYAN))
    print(c("  "+sep,DIM))
    for rec,is_new in zip(records,new_flags):
        ip   = (rec["ip"]           or "")[:COL[0]]
        port = str(rec["port"]      or "")[:COL[1]]
        coun = (rec["country"]      or "")[:COL[2]]
        org  = (rec["organization"] or "")[:COL[3]]
        mark = c("  ★",GREEN,BOLD) if is_new else c("  ·",DIM)
        line = "  "+rfmt.format(ip,port,coun,org)
        print((c(line,GREEN) if is_new else line)+mark)
    print(c("  "+"┴".join("─"*(w+2) for w in COL),DIM))

def print_sleep(sec):
    print(c(f"\n  ⏱  следующий раунд через {sec:.1f} с  (Ctrl+C для остановки)\n",DIM))

# ── DB ────────────────────────────────────────────────────────────────────────
def init_db(path):
    conn=sqlite3.connect(path,check_same_thread=False)
    conn.execute("""CREATE TABLE IF NOT EXISTS hosts(
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        ip TEXT NOT NULL, port INTEGER, country TEXT,
        organization TEXT, hostname TEXT, seen_at TEXT NOT NULL,
        UNIQUE(ip,port))""")
    conn.commit()
    return conn

def upsert(conn,rec):
    cur=conn.execute(
        "INSERT OR IGNORE INTO hosts(ip,port,country,organization,hostname,seen_at)"
        " VALUES(:ip,:port,:country,:organization,:hostname,:seen_at)",rec)
    conn.commit()
    return cur.rowcount>0

def db_count(conn):
    return conn.execute("SELECT COUNT(*) FROM hosts").fetchone()[0]

def export_csv(conn,path):
    rows=conn.execute(
        "SELECT ip,port,country,organization,hostname,seen_at FROM hosts ORDER BY id"
    ).fetchall()
    with open(path,"w",newline="",encoding="utf-8") as f:
        w=csv.writer(f)
        w.writerow(["ip","port","country","organization","hostname","seen_at"])
        w.writerows(rows)
    print(c(f"\n  CSV → {path}  ({len(rows)} строк)",CYAN,BOLD))

# ── Parser ────────────────────────────────────────────────────────────────────
def get_page_title(html):
    m=re.search(r"<title>(.*?)</title>",html,re.I|re.S)
    return m.group(1).strip()[:80] if m else ""

def parse_results(html):
    soup=BeautifulSoup(html,"html.parser")
    out=[]
    for div in soup.select("div.result"):
        title_tag=div.select_one("a.title")
        if not title_tag: continue
        ip=title_tag.get_text(strip=True)

        port=None
        ext=div.select_one("a[rel~='noopener']")
        if ext and ext.get("href"):
            m=re.search(r":(\d+)/?$",ext["href"])
            if m: port=int(m.group(1))

        flag=div.select_one("img.flag")
        country=flag["title"] if flag and flag.get("title") else None

        org_t=div.select_one("a.filter-org")
        org=org_t.get_text(strip=True) if org_t else None

        host_t=div.select_one("li.hostnames")
        hostname=host_t.get_text(strip=True) if host_t else None

        out.append({"ip":ip,"port":port,"country":country,
                    "organization":org,"hostname":hostname,
                    "seen_at":datetime.utcnow().isoformat(timespec="seconds")})
    return out

# ── Browser ───────────────────────────────────────────────────────────────────
def find_chrome_binary():
    """Ищет Chrome или Chromium на системе."""
    import shutil
    candidates = [
        "google-chrome", "google-chrome-stable",
        "chromium", "chromium-browser",
        "/usr/bin/google-chrome", "/usr/bin/chromium",
        "/usr/bin/chromium-browser",
        "/opt/google/chrome/chrome",
    ]
    for name in candidates:
        path = shutil.which(name) or (name if Path(name).exists() else None)
        if path:
            return path
    return None

def create_driver(headless=False):
    """Создаёт undetected Chrome драйвер."""
    try:
        import undetected_chromedriver as uc
    except Exception as e:
        print(c(f"  ✖  Ошибка импорта undetected-chromedriver: {e}",RED,BOLD))
        print(c("      Попробуй: pip install --upgrade undetected-chromedriver selenium",YELLOW))
        raise SystemExit(1)

    # Ищем браузер
    chrome_bin = find_chrome_binary()
    if chrome_bin:
        print(c(f"  ✔  Найден браузер: {chrome_bin}",GREEN))
    else:
        print(c("  ✖  Chrome/Chromium не найден!",RED,BOLD))
        print(c("      Kali Linux: sudo apt install chromium",YELLOW))
        print(c("      Или: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb && sudo dpkg -i google-chrome*.deb",YELLOW))
        raise SystemExit(1)

    import os
    display = os.environ.get("DISPLAY", "")
    if not display and not headless:
        print(c("  ℹ  DISPLAY не найден → включаем headless режим",YELLOW))
        headless = True

    options = uc.ChromeOptions()
    options.binary_location = chrome_bin
    options.add_argument("--no-sandbox")
    options.add_argument("--disable-dev-shm-usage")
    options.add_argument("--window-size=1920,1080")
    options.add_argument("--lang=en-US")
    options.add_argument("--disable-blink-features=AutomationControlled")
    options.add_argument("--disable-gpu")
    options.add_argument("--remote-debugging-port=0")

    if headless:
        options.add_argument("--headless=new")
        options.add_argument("--disable-gpu")

    try:
        driver = uc.Chrome(options=options, version_main=147)
    except Exception as e:
        print(c(f"  ✖  Не удалось запустить Chrome: {e}",RED,BOLD))
        print(c("      Убедись что ChromeDriver совместим с версией Chrome",YELLOW))
        print(c("      Попробуй: pip install --upgrade undetected-chromedriver",YELLOW))
        raise SystemExit(1)

    driver.set_page_load_timeout(40)
    return driver

def fetch_page(driver, url, wait_sec=4):
    """
    Открывает URL в браузере, ждёт загрузки, возвращает (html, ms, title).
    """
    t0 = time.perf_counter()
    driver.get(url)

    # Ждём пока появятся результаты или пройдёт таймаут
    deadline = time.time() + 20
    while time.time() < deadline:
        html = driver.page_source
        if 'class="result"' in html:
            break
        # Cloudflare challenge — ждём дольше
        if "just a moment" in html.lower() or "cf-browser-verification" in html.lower():
            print(c("  ⏳ Cloudflare challenge, ждём...",YELLOW),end="\r")
            time.sleep(2)
            continue
        time.sleep(1)

    ms = (time.perf_counter()-t0)*1000
    html = driver.page_source
    title = driver.title
    return html, ms, title

# ── Main loop ─────────────────────────────────────────────────────────────────
def poll(query,interval,db_path,max_rounds,jitter,pages_per_round,headless):
    conn=init_db(db_path)
    total_new=0
    round_num=0

    print_header(query,interval,db_path,headless)
    print(c("  Запускаем браузер...",DIM))

    driver = create_driver(headless=headless)

    # Первый визит на главную — получаем cf_clearance автоматически
    print(c("  Открываем shodan.io для получения cookies...",DIM))
    try:
        driver.get("https://www.shodan.io/")
        time.sleep(3)  # даём Cloudflare обработать
    except Exception:
        pass

    print(c("  Браузер готов. Начинаем сбор.\n",GREEN,BOLD))

    try:
        while max_rounds==0 or round_num<max_rounds:
            round_num+=1

            for page in range(1,pages_per_round+1):
                url=f"https://www.shodan.io/search?query={query}&page={page}"
                print_request(round_num,page,url)

                try:
                    html,ms,title=fetch_page(driver,url)
                except Exception as e:
                    print(c(f"\n  ✖  Ошибка браузера: {e}",RED,BOLD))
                    break

                print_response(len(html.encode()),ms,title)

                # Проверяем блокировку
                if 'class="result"' not in html:
                    print(c(f"\n  ⚠  Результатов нет на странице. Title: {title}",YELLOW))
                    print(c(f"     Сохраняю HTML → debug_last.html",DIM))
                    Path("debug_last.html").write_text(html,encoding="utf-8")
                    break

                records=parse_results(html)
                if not records:
                    print(c(f"\n  ⚠  Парсер нашёл 0 записей",YELLOW))
                    break

                new_flags=[upsert(conn,r) for r in records]
                new_count=sum(new_flags)
                total_new+=new_count
                print_table(records,new_flags)
                print(c(f"\n  parsed={len(records)}  new={new_count}  DB total={db_count(conn)}",BOLD))

                # Пауза между страницами
                if page<pages_per_round:
                    pause=random.uniform(3.0,7.0)
                    print(c(f"  ⏳ пауза {pause:.1f}с между страницами...",DIM))
                    time.sleep(pause)

            # Пауза между раундами
            if max_rounds==0 or round_num<max_rounds:
                sleep_sec=max(1.0,interval+random.uniform(-jitter,jitter))
                print_sleep(sleep_sec)
                time.sleep(sleep_sec)

    except KeyboardInterrupt:
        print(c("\n\n  Остановлено.",YELLOW,BOLD))
    finally:
        try:
            driver.quit()
        except Exception:
            pass
        csv_path=(db_path[:-3]+".csv") if db_path.endswith(".db") else db_path+".csv"
        export_csv(conn,csv_path)
        print(c(f"  Всего новых уникальных хостов: {total_new}",BOLD,GREEN))
        conn.close()

# ── CLI ───────────────────────────────────────────────────────────────────────
def main():
    p=argparse.ArgumentParser(
        description="Shodan scraper через живой браузер (Cloudflare обходится автоматически)"
    )
    p.add_argument("--query",    default="FORTINET",   help="Поисковый запрос Shodan")
    p.add_argument("--interval", type=float,default=30,help="Секунд между раундами")
    p.add_argument("--output",   default="shodan_results.db")
    p.add_argument("--rounds",   type=int,  default=0, help="0 = бесконечно")
    p.add_argument("--jitter",   type=float,default=5.0)
    p.add_argument("--pages",    type=int,  default=1, help="Страниц за раунд")
    p.add_argument("--headless", action="store_true",  help="Без окна браузера")
    p.add_argument("--export-csv",action="store_true", help="Только экспорт БД в CSV")
    args=p.parse_args()

    if args.export_csv:
        conn=init_db(args.output)
        csv_path=(args.output[:-3]+".csv") if args.output.endswith(".db") else args.output+".csv"
        export_csv(conn,csv_path)
        conn.close()
        return

    poll(args.query,args.interval,args.output,
         args.rounds,args.jitter,args.pages,args.headless)

if __name__=="__main__":
    main()

вот такая вот хуйня пацаны и девчули, жду мнение экспертов по масштабированию через Kubernets

с Вами был Майк Вазовски, специально для LIMB!​
 ​
👍15 👏2 😢3
1) подобный каркас уже ранее был детально одокументирован в работе @DCcorp с примерно похожим механизмом реализации
2) андетекты так просто уже пробить не выйдет, эмуляция через playwright в этом вопросе выглядит кратно компетентнее и надежнее
3) шодан желательно дурить отдельными раскачанными сессиями на прокси с определенным интервалом использования забивая не только однотипные запросы, механика прогрев => использование, это то что уже успел заметить у себя
4) по опционалу хрома для многих антифродок подобный софт будет излишне палевным
5) реализация интересна во многом но недостаточно хорошо описана, это не похоже на детальное описание реализуемой механики

на данный момент к сожалению статья не попадает на оплату
👍4 ❤️1
факт остается фактом результат работы за 4 дня
механизм работает, антидетекты не палят 

я добавил в цикл генерацию js api чтобы через Chrome dev tools, это про историю когда при http  get мы порлучаем прогрузку множества js скриптов которые позволяют узнать разрешение экрана, количество ядер, язык и оперативку

также добавлена подмена юзерагента

профили изолированны, куки генерируются при каждом последующем запросе

также обрати внимание на то что стоит рандомный генератор и запросы не в тупую идут а с каким-то временным запросом

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

для пруфа могу тебе файл бд скинуть и сам все увидишь

еще вопросы?
Не удалось прогрузить медиа
👍1
Любая реализация имеет место быть, любой труд и старание, главное чтобы был результат.;)
👍1

Гостевой просмотр. Чтобы отвечать и ставить реакции, войдите или зарегистрируйтесь.