Статья Web Scraping для AntiFraud: Как Python и Scrapy помогают мониторить фейковые сайты и фишинг

1. Введение: proactive phishing monitoring​

Забудьте про времена, когда просто ждали, пока клиент пришлет скриншот фейковой формы в саппорт. Если сидите и ждете жалоб – вы уже проиграли. Наша задача – обнаружить фишинговый лендинг еще на этапе "прогрева" или сразу после регистрации сертификата, пока на него не нагнали трафик.

1.webp

1.1. Масштаб проблемы (РФ 2025)​

За 2025 год доля фишинговых атак впервые снизилась с 85,7% до 67,87%. На первый взгляд – позитивная динамика. Но злоумышленники перешли на более сложные и высокодоходные схемы. По данным ЦБ РФ, по итогам 2025 года сумма похищенных средств достигла 29,3 млрд рублей, увеличившись на 6,4%. Представитель МВД на форуме "Антифрод Россия" называл оценку свыше 154 млрд рублей, зампред Сбербанка оценил объём в 275–295 млрд рублей.

По данным системы Минцифры "Антифишинг" в 2025 году было выявлено более 74 тыс. фишинговых интернет-ресурсов. По подсчётам специалистов Центра мониторинга и управления сетью связи общего пользования, количество заблокированных фишинговых ресурсов в 2025 году превысило 100 тыс., что в 3,3 раза больше предыдущего года.

Координационный центр доменов .RU/.РФ опубликовал итоги 2025 года для российских национальных доменных зон. В течение года в .RU было зарегистрировано 1810861 новое доменное имя, число новых зарегистрированных доменных имен .РФ выросло на 207654. В течение года направлено 62168 обращений к регистраторам о прекращении делегирования вредоносных доменов. Доля фишинга в общем объеме составила 67,87% (~42192).

По данным компании F6, за первое полугодие 2025 года злоумышленники создавали около 2299 фишинговых ресурсов и 1238 скам-ресурсов на один бренд: то есть ежедневно злоумышленники запускали в среднем по 13 фишинговых и по 7 мошеннических ресурсов на каждый бренд. Примерно 80% создаваемых мошеннических ресурсов эксплуатируют бренды известных компаний, а лидером по числу атак стал ритейл: на него приходится 50% всех фишинговых и 32% скам-атак (похищение денежных средств).
Среди финансовых брендов мошенники чаще всего имитируют "Сбер" и "ТБанк", а в сегменте E-commerce – "Wildberries", "Авито" и "Юла".

1.2. Manual vs automated approach: почему ручной подход больше не работает​

Мы только что посмотрели на цифры. Серьезно, кто-то до сих пор пытается мониторить это вручную?
Пока аналитик открыл браузер, вбил URL, загрузил страницу, CAPTCHA, WAF, собрал информацию, отправил отчет, злоумышленник собрал сотни учёток, снес страницу и ушёл на следующий домен. Фишинг-как-услуга (PhaaS) и автоматизированные наборы инструментов демократизировали киберпреступность до уровня "купил подписку – запустил атаку". Среднее время жизни фишингового сайта – часы, ручное обнаружение и блокировка растягивается на недели.

Ручной анализ не работает даже в теории:
  • Объёмы: в 2025 году в зоне .RU десятки тысяч фишинговых доменов. Даже команда захлебнётся проверять вручную.
  • Скорость реакции злоумышленников: PhaaS позволяет развернуть готовую страницу-клон за 2 минуты, кампании запускают синхронно по 100+ доменам. Мы не можем отвечать со скоростью "открыл браузер – подумал – написал письмо".
  • Человеческий фактор: коэффициент ложных срабатываний растёт пропорционально усталости. Руководители SOC называют усталость аналитиков главной операционной угрозой. Ошибки в копировании IOC, пропущенные детали в whois, забытые вложения в abuse-отчёт – это снижает эффективность.
Автоматизированный пайплайн на Python + Scrapy – не про увольнение TI-специалистов. Это про то, чтобы перестать тратить мозги на рутинную работу.
  • Генерация кандидатов - тысячи вариантов подозрительных доменов через dnstwist, URLCrazy за секунды.
  • Мониторинг Certificate Transparency – crt.sh, Certstream позволяют слушать поток сертификатов 24/7.
  • Обход CAPTCHA и WAF – ротация прокси, User-Agent, retry-механизмы.
  • Скачивание страниц – парсер под тысячу потоков за минуту обработает то, на что человек потратит час.
  • Fuzzy matching текста (косинусное расстояние, Левенштейн) – алгоритм даёт результат за доли секунды с высокой точностью.
  • Сравнение скриншотов – скрипт находит расхождения там, где на глаз "вроде похоже".
  • Извлечение IOC – регулярки без устали вытаскивают домены, IP, email из HTML и JavaScript.
  • Отправка в MISP/TheHive – API-колл за доли секунды, без ручного копирования.
  • Генерация abuse-отчёта – шаблон на Jinja2 подставляется в API регистратора, не забывая приложить скриншот.

2. Typosquatting Detection: охота на близнецов​

Typosquatting detection - генерация, обнаружение и проверка доменов-"двойников", которые отличаются от вашего оригинального домена на один-два символа.

2.1. dnstwist: генерация вариаций​

dnstwist – швейцарский нож для тайпсквоттинга берёт домен и генерирует сотни вариаций, которые злоумышленник мог бы зарегистрировать.

Основные алгоритмы dnstwist: соседние клавиши, омоглифы (визуально похожие буквы из других алфавитов), добавление/удаление символов, перестановка соседних символов, использование цифр, добавление суффиксов через внешние словари, альтернативные доменные зоны.

Запуск:
Bash:
dnstwist --registered superbank.ru > candidates.txt
--registered - заставляет dnstwist сразу делать DNS-проверку – показывает только те домены, которые зарегистрированы и имеют A/AAAA записи.

2.2. URLCrazy: алгоритмы подмены​

URLCrazy – делает упор на алгоритмы подмены, характерные именно для фишинга, генерация вариаций на основе звукоподобия и обфускации.

Чего нет в dnstwist, но есть в URLCrazy:
  • Замена букв по звучаниюsuperbank => superbeink (ai => e)
  • Более гибкое добавление префиксов/суффиксов
  • Поддержка нескольких раскладок клавиатуры: генерация опечаток для QWERTY, AZERTY, QWERTZ и DVORAK
  • База из 8000+ распространённых опечаток
Запуск:
Bash:
urlcrazy -f json superbank.ru > urlcrazy_candidates.json
Большинство команд используют dnstwist как основной, а URLCrazy – как дополнение для специфичных кейсов.

2.3. Фильтрация: DNS resolution check​

DNSTwist и URLCrazy сгенерировали тысячи доменов-кандидатов. Из них реально зарегистрировано, скажем, 200. Нам нужны те, у которых открыт 80/443 порт и есть веб-интерфейс. Поэтому после генерации делаем DNS resolution check – проверяем, что домен резолвится и IP отвечает на HTTP-запрос.

Используя библиотеку asyncio для асинхронности, aiodns для DNS‑запросов и aiohttp для HTTP‑проверок, реализуем двухэтапную фильтрацию доменов-кандидатов. На первом этапе для каждого домена параллельно запрашиваются A‑записи (IPv4) и AAAA‑записи (IPv6) через публичные DNS‑серверы (например, 8.8.8.8), чтобы обойти системный DNS. Домены, которые не получили ни одного IP (нерезолвящиеся), отбрасываются. На втором этапе для оставшихся доменов асинхронно проверяется доступность по HTTP/HTTPS с игнорированием ошибок SSL‑сертификатов (ssl=False – необходимо, так как фишинговые сайты часто используют невалидные или самоподписанные сертификаты). Запросы выполняются с таймаутом, успехом считается любой HTTP‑статус в диапазоне 200–499 (сайт отвечает). В итоге получаем для каждого домена информацию о наличии IP, сами IP‑адреса и статус веб‑доступности – всё без блокировок и за счёт асинхронности с минимальными задержками. После DNS-фильтрации остаётся, допустим, 50 доменов, которые реально резолвятся. Это и есть кандидаты для Scrapy.

Важные нюансы:
  • некоторые фишеры используют fast-flux DNS – IP постоянно меняется, но домен резолвится всегда, его всё равно передаем дальше.
  • регистраторы часто паркуют неиспользуемые домены на заглушки с рекламой, такие домены резолвятся, веб-сервер возвращает 200OK с заглушкой. Их фильтруем по контенту (слова "parking", "domain for sale").

3. Certificate Transparency: ловим на взлёте​

Certificate Transparency даёт нам реальные, только что родившиеся домены с сертификатами. Фишер регистрирует домен – получает сертификат – сертификат попадает в публичные CT-логи. Засветился ещё до того, как отправил первое письмо.
Ключевая метрика для понимания масштаба: Let's Encrypt, крупнейший центр сертификации, в конце 2025 года выдаёт 10 миллионов сертификатов в день. 450 тысяч сертификатов каждый час проходят через всю экосистему WebPKI.

3.1. crt.sh API: база, которую мы заслужили​

crt.sh – поисковик по CT-логам. По сути, огромная база данных, в которой можно найти все сертификаты, выпущенные для домена или по маске.

Запрос:
Bash:
curl -s "https://crt.sh?q=%25.superbank.ru&exclude=expired&output=json"
excluded=expired – отсекаем просроченные сертификаты, чтобы не плодить шум.

Получаем:
JSON:
[
    ...
    {
        "issuer_ca_id":213378,
        "issuer_name":"C=US, O=Let's Encrypt, CN=R13", // кто выпустил
        "name_value":"p2p.superbank.ru", // домен или список SAN
        "id":25413374039,
        "entry_timestamp":"2026-04-06T14:06:10.498", // когда попал в лог
        "not_before":"2026-04-06T13:07:39",
        "not_after":"2026-07-05T13:07:38", // срок действия
        "serial_number":"0474ac7b90be68283b6d0d1467724398e405",
    },
    ...
],
Subject Alternative Name (SAN) – поле в SSL/TLS-сертификате, которое позволяет указать несколько доменных имён (или IP-адресов), для которых этот сертификат считается валидным. Может содержать несколько доменов, разделённых символом новой строки (\n) или запятыми. Поэтому когда парсим ответ от crt.sh, обязательно нужно разбивать name_value по \n.

Запрос:
Bash:
curl -s "https://crt.sh?q=%25.superbank.ru&exclude=expired&output=json" | jq -r '.[].name_value'

Ответ:
Код:
p2p.superbank.ru
*.superbank.ru
superbank.ru
emails.superbank.ru

Важные нюансы:
  1. Rate Limiting: crt.sh ограничивает запросы до 60 запросов в минуту на IP. Если ваш парсер запускается раз в час с одним IP – ок. Если чаще – нужна ротация IP.
  2. crt.sh не даёт real-time: данные поступают в crt.sh с задержкой, поэтому для оперативного мониторинга используем Certstream, а crt.sh оставляем для периодических "прочёсываний" (раз в 6-12 часов) на случай пропусков.

3.2. Certstream: real-time monitoring пока теплый​

Certstream – это WebSocket-поток, агрегирующий свежие сертификаты со всех CT-логов в реальном времени. В него попадают все сертификаты, выпускаемые всеми центрами сертификации для всех доменов в мире. Как только какой-то центр сертификации (Let's Encrypt, DigiCert и т.д.) выпускает свежий сертификат, Certstream отправляет уведомление. Идеально для автоматического мониторинга 24/7.

Подключение к потоку Certstream:
Python:
from certstream import cert_stream_callback
import json

# Сгенерированные dnstwist'ом варианты в set
MONITORED_DOMAINS = {'superbenk.ru', 'superbakn.ru', 'superrbank.ru', ...}

def callback(message, context):
    # message – словарь, который Certstream присылает по WebSocket
    # Выбираем сообщения о новом сертификате
    if message['message_type'] == 'certificate_update':
        cert_data = message.get('data', {})
        san = cert_data.get('leaf_cert', {}).get('extensions', {}).get('san', [])
       
        for dns_name in san:
            if dns_name.get('type') == 'DNS':
                domain = dns_name.get('value')
                # Если домен из сертификата есть в списке – это наш кандидат
                if domain in MONITORED_DOMAINS:
                    print(f"Найден фишинговый домен: {domain}")
                    # Отправляем в Scrapy на анализ

cert_stream_callback(callback, skip_heartbeats=True)
# skip_heartbeats=True – убирает служебные сообщения, которые приходят раз в 30 секунд, чтобы держать соединение открытым
В боевом пайплайне добавляем домен в очередь (redis, kafka), откуда Scrapy забирает задачи на скачивание. И так как в message получаем информацию вообще обо всех новых сертификатах, необходимо делать фильтрацию – проверять, содержит список доменов интересующих нас кандидатов или ключевое слово.

Почему Certstream, а не crt.sh в polling-режиме:
  • Real-time – узнаем о новом домене в момент его появления. Certstream выдаёт сертификат через 1-5 секунд после его публикации в CT-лог.
  • Не нужно дёргать API каждую минуту и заморачиваться с rate limits.
  • Certstream отдаёт только новые сертификаты, а crt.sh возвращает всю историю.
Нюансы для реальной эксплуатации:
  • В Certstream один и тот же сертификат может попасть в несколько CT-логов. Поэтому на приёме нужно делать дедупликацию по domain, not_before или по хешу сертификата.
  • Certstream может отвалиться. В бою оборачивай cert_stream_callback в цикл с try-except и переподключением.
  • Certstream – это публичный сервис, он может лечь или замедлиться. В боевых системах стоит предусмотреть fallback на crt.sh.

3.3. Keyword matching: зерна от плевел​

Подключились к Certstream, летят десятки тысяч событий в час. Задача keyword matching – отсеять 99.9% шума и оставить домены, которые связаны с нашим брендом.

Строим фильтр на трёх китах:
  1. Присутствие бренда – домен должен содержать название нашей компании (или его опечатку/мутацию).
  2. Исключение своих доменов – whitelist легитимных доменов, которые не трогаем.
  3. Фишинговые паттерны – слова, характерные для мошеннических страниц (login, verify, secure, account, confirm и т.д.).
Пишем функцию, которая принимает домен и возвращает флаг + скор:
Python:
BRAND = 'superbank'
PHISHING_PATTERNS = ['login', 'verify', 'secure', 'account', 'auth', 'confirm']
WHITELIST = {'superbank.ru', 'www.superbank.ru', 'login.superbank.ru'}

def is_relevant(domain: str) -> tuple[bool, int]:
    domain_lower = domain.lower()
   
    # 1. Свои домены пропускаем
    if domain_lower in WHITELIST:
        return False, 0
   
    # 2. Проверяем наличие бренда
    if BRAND not in domain_lower:
        return False, 0
   
    score = 10  # базовый вес за упоминание бренда
   
    # 3. Бонус за структуру официального поддомена (но не в whitelist)
    if domain_lower.endswith(f'.{BRAND}.ru') or domain_lower == f'{BRAND}.ru':
        score += 5
   
    # 4. Штрафуем фишинговые паттерны
    for pattern in PHISHING_PATTERNS:
        if pattern in domain_lower:
            score += 15
            break
   
    return True, score

При интеграции в Certstream-коллбэк функция callback с фильтрацией выглядит так:
Python:
def callback(message, context):
    if message['message_type'] == 'certificate_update':
        cert_data = message.get('data', {})
        san = cert_data.get('leaf_cert', {}).get('extensions', {}).get('san', [])
       
        for dns_name in san:
            if dns_name.get('type') == 'DNS':
                domain = dns_name.get('value')
               
                # Проверяем релевантен ли домен и его скор
                relevant, score = is_relevant(domain)
                if relevant:
                    # Отправляем в очередь Scrapy
                    send_to_scrapy_queue(domain, score)
                    log.info(f"Found candidate: {domain} (score={score})")
Итог
  • Keyword matching – первый этап фильтрации в потоке сертификатов.
  • Проверяем наличие бренда, исключаем whitelist, штрафуем фишинговые слова.
  • Не пытаемся заменить dnstwist – его задача генерировать мутации, а на этапе keyword matching – проверка точных вхождений. Keyword matching нужен для доменов, которые не попали в MONITORED_DOMAINS (п.3.2.), но всё ещё подозрительны.

4. Scrapy Spider: монстр на службе антифрода​

Сгенерировали dnstwist'ом, отфильтровали по DNS, наловили сертификатов через Certstream и прогнали через keyword matching. В очереди лежат сотни URL. Задача: скачать страницы так, чтобы не заблокировали, и подготовить HTML для анализа.
Scrapy – это асинхронный фреймворк, который переваривает тысячи запросов в секунду. В отличие от requests с потоками, Scrapy использует Twisted, за счёт event loop он крутит десятки параллельных соединений в одном потоке и не блокируется на ожидании ответа.

4.1. Настройка проекта​

Создаём проект:
Bash:
startproject phishing_monitor
cd phishing_monitor
scrapy genspider clone_checker example.com # команда для создания файла-заготовки для нового парсера

Структура проекта:
Код:
phishing_monitor/
├── phishing_monitor/
│   ├── spiders/
│   │   └── clone_checker.py     # наш парсер
│   ├── middlewares.py           # кастомные middleware
│   ├── settings.py              # настройки проекта
│   └── items.py                 # модель данных (опционально)
└── scrapy.cfg

Не пишем start_requests статически – будем забирать URL из очереди, например из Redis:
Python:
# clone_checker.py

import scrapy
import redis

class CloneCheckerSpider(scrapy.Spider):
    name = 'clone_checker'
   
    def start_requests(self):
        r = redis.Redis(host='localhost', port=6379, db=0)
        while True: # костыль для примера, не для прода
            # Блокирующая pop – спим, ждём новые URL
            url = r.brpop('phishing:urls')[1].decode()
            yield scrapy.Request(url, callback=self.parse)
   
    def parse(self, response):
        # Здесь будет анализ страницы
        self.logger.info(f"Processing {response.url}")
        # Сохраняем HTML для дальнейшего fuzzy matching
        yield {
            'url': response.url,
            'body': response.text,
            'status': response.status
        }
В боевом окружении используем scrapy-redis: Scrapy слушает Redis-очередь, а Certstream-коллбэк пушит туда URL.

Для запуска команда crawl:
Bash:
scrapy crawl clone_checker
# JSON Lines (рекомендуется для потоковой записи)
scrapy crawl clone_checker -o output.jl
# JSON
scrapy crawl clone_checker -o output.json

Важные настройки в settings.py:
Python:
# Для задач антифрода обычно устанавливают `False`
ROBOTSTXT_OBEY = False

# Глубина обхода
DEPTH_LIMIT = 1

# Для задержки между запросами к одному домену
DOWNLOAD_DELAY = 1
RANDOMIZE_DOWNLOAD_DELAY = True

# Автоматическое ограничение параллельных запросов
CONCURRENT_REQUESTS = 32
CONCURRENT_REQUESTS_PER_DOMAIN = 8

# Таймауты – чтобы не висеть на мёртвых сайтах
DOWNLOAD_TIMEOUT = 15

# Retry
RETRY_ENABLED = True
RETRY_TIMES = 2

# Куда складывать результат
FEEDS = {
    'output/phishing.json': {
        'format': 'jsonlines',
        'encoding': 'utf8',
    }
}

4.2. Middleware: proxy rotation, user-agent чтобы не вычислили​

Фишинговые сайты часто висят на дешёвом хостинге, но некоторые используют WAF и блокируют ботов. Без ротации прокси и User-Agent IP забанят после 5-10 запросов. Scrapy позволяет написать Downloader Middleware, которая подменяет параметры запроса.

Пример middleware для ротации User-Agent:
Python:
# middlewares.py

from fake_useragent import UserAgent

class RandomUserAgentMiddleware:
    def __init__(self):
        self.ua = UserAgent(fallback='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36') # fallback на случай недоступности сервера fake-useragent
   
    # метод, который Scrapy вызывает перед отправкой каждого HTTP-запроса
    def process_request(self, request, spider):
        request.headers['User-Agent'] = self.ua.random

Подключаем в settings.py:
Python:
DOWNLOADER_MIDDLEWARES = {
    # отключаем встроенный UA, чтобы не перезаписывал кастомный
    'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
    # приоритет выполнения для кастомных middleware
    'phishing_monitor.middlewares.RandomUserAgentMiddleware': 400,
}

Proxy rotation – позволяет использовать публичные списки или купить пул резидентных прокси. Пример middleware, который берёт прокси из Redis-списка:
Python:
class ProxyMiddleware:
    def __init__(self):
        # список проксей
        self.proxies = [
            'http://user:pass@proxy1:8080',
            'http://user:pass@proxy2:8080',
        ]
        self.current = 0
   
    def process_request(self, request, spider):
        proxy = self.proxies[self.current % len(self.proxies)]
        # если в meta есть ключ 'proxy', Scrapy автоматически отправляет запрос через указанный прокси-сервер
        request.meta['proxy'] = proxy
        # таймаут, чтобы не висеть на мёртвом прокси
        request.meta['download_timeout'] = 15
        self.current += 1
Для реального антифрод-пайплайна его нужно доработать: добавить проверку здоровья прокси, вынос конфигурации, обработку ошибок. В бою используют scrapy-rotating-proxies или аналоги, а этот код – для понимания. Для надёжности лучше использовать библиотеку scrapy-rotating-proxies или крутить прокси через scrapy-proxy-middleware.

4.3. Стратегии обхода CAPTCHA: я не робот​

Злоумышленники ставят капчи, чтобы отсеять ботов. Полностью автоматически решать reCAPTCHA сложно и дорого, но есть несколько стратегий:
  1. Игнорирование страниц с капчей скриптами: детектируем, отправляем аналитику на ручную проверку.
  2. Использование сервисов распознавания: для простых капч (текстовых, математических) можно использовать 2Captcha, ruCaptcha. Они стоят копейки, но добавляют задержку. В Scrapy это делается через middleware, которая при получении капчи приостанавливает запрос, отправляет капчу в сервис, получает ответ и повторяет запрос с токеном.
  3. Эмуляция браузера: Scrapy не умеет рендерить JavaScript. Поэтому для сайтов, где капча появляется после выполнения JS, можно использовать scrapy-selenium или scrapy-playwright.

5. Content Analysis: какое совпадение​

Есть подозрительный домен, Scrapy скачал HTML, возможно, даже сделан скриншот через Selenium. Это фишинг или нет? Применяем систему оценки, которая комбинирует несколько сигналов: текст, структура страницы, визуальное сходство.

2.webp


5.1. Text similarity (Levenshtein, cosine)​

Текстовое сравнение – первая линия обороны. Выкачиваем из оригинального сайта (эталон) и из подозрительной страницы все видимые тексты, очищаем от HTML-тегов, скриптов, лишних пробелов, и сравниваем.

Расстояние Левенштейна
Левенштейн считает, сколько односимвольных правок (вставка, удаление, замена) нужно сделать, чтобы превратить одну строку в другую. Хорош для коротких строк: заголовков, текста кнопок, форм.
Python:
from rapidfuzz import distance # В rapidfuzz реализован алгоритм Левенштейна

title_original = "Login to Super-Bank"
title_phish = "Login to SyperBank"

lev = distance.Levenshtein.normalized_distance(title_original, title_phish)
# Получим нормализованное расстояние Левенштейна: 0.1053
К примеру, порог 0.2 – эмпирически обоснованная отправная точка, но требует калибровки на своих данных. Если полученное значение меньше 0.2 – считаем строки практически одинаковыми. Результат 0.1053 говорит о высокой степени сходства, что добавляет очков в скоринговую модель.

Подводные камни:
  • Ложноотрицательные срабатывания. Расстояние Левенштейна – мощный инструмент для коротких строк, но оно слепо к омоглифам. Строки "Вход в Супер-Банк" и "Вход в Супер-Bank" дадут расстояние 0.2353 > 0.2. Модель скажет, что заголовки не похожи. А ведь это явный фишинг!
  • Обязательно применяйте транслитерацию для кириллицы перед сравнением. После транслитерации строки станут идентичными, и расстояние Левенштейна станет 0.
  • Либо повышайте порог срабатывания, к примеру до 0.3. Тогда 0.2353 < 0.3 – заголовки будут признаны похожими. Однако это увеличит риск ложных срабатываний. Поэтому порог нужно подбирать эмпирически на тестовой выборке.
Косинусное расстояние
Косинусное расстояние – это способ сравнивать длинные тексты по смыслу, игнорируя перестановки слов и замену синонимов. Косинусное расстояние смотрит на частотность слов и выдаёт сходство от 0 до 1.

Как это работает упрощённо:
  • Превращаем текст в вектор – список чисел, где каждое число – важность слова.
  • Сравниваем угол между векторами. Если угол маленький (косинус близок к 1) – тексты похожи по смыслу.
Python:
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

original_text = "Для входа в онлайн-банк используйте ... из любой точки мира +7(495)999-99-99. Электронная почта help@superbank.ru ..."
phish_text = "Для входа в банк используйте ... из любой точки мира +7(987)654-32-10. Электронная почта help@suprbank.info ..."

# Превращаем в векторы
vectorizer = TfidfVectorizer().fit_transform([original_text, phish_text])
cos_sim = cosine_similarity(vectorizer[0:1], vectorizer[1:2])[0][0]
# cos_sim = 0.81 (чем ближе к 1, тем больше похожи)
К примеру, порог 0.7: если косинусное сходство > 0.7 – страницы семантически близки. Снова, никакого мирового стандарта, 0.7 – лишь популярная отправная точка, результат практического опыта.
Ещё можно сравнивать: title, h1, атрибуты action и name, мета-теги. Для длинных страниц – это главный инструмент, для коротких строк – Левенштейн. Вместе они дают надёжную систему детекции клонов.

5.2. Visual: SSIM comparison - сходство не случайно​

Текстовый анализ может ошибаться, если фишер полностью переписал текст, но сохранил дизайн. В таких случаях нас спасает сравнение скриншотов.

Structural Similarity Index Measure (SSIM) – метрика, которая оценивает структурное сходство двух изображений. В отличие от прямого попиксельного сравнения, SSIM учитывает яркость, контраст и структуру (границы, формы, расположение объектов). SSIM смотрит на картинку как человек.

Алгоритм:
  1. Делаем скриншот оригинальной страницы (эталон).
  2. Для каждого подозрительного домена через Selenium рендерим страницу, делаем скриншот.
  3. Сравниваем скриншоты через skimage.metrics.structural_similarity.
Python:
from skimage.metrics import structural_similarity as ssim
import cv2

original = cv2.imread("original.png", cv2.IMREAD_GRAYSCALE)
candidate = cv2.imread("candidate.png", cv2.IMREAD_GRAYSCALE)

# Приводим к одному размеру
h = min(original.shape[0], candidate.shape[0])
w = min(original.shape[1], candidate.shape[1])
original = cv2.resize(original, (w, h))
candidate = cv2.resize(candidate, (w, h))

score = ssim(original, candidate)
# score ≈ 0.92 (чем ближе к 1, тем более похожи)
Порог: если SSIM > 0.85 – почти наверняка визуальный клон.

Подводные камни:
  • Динамический контент (баннеры, курс валют) снижает SSIM.
  • Разная ширина вьюпорта, структура страницы может измениться. SSIM чувствителен к структурным изменениям. Фиксируем размер окна браузера.
  • Производительность: рендеринг скриншота – 2–5 секунд. Не применяем ко всем подряд, только к самым подозрительным кандидатам после текстового анализа.

5.3. Scoring model: взвешенное решение​

У нас есть три группы метрик (текст, структура DOM, визуал), каждая даёт свой score. Нужно их скомбинировать и принять решение: фишинг, чисто или требует ручной проверки.

Пример простой скоринговой модели:
  • Тайпсквоттинг: вычисляем расстояние Левенштейна между доменами, если проходит по нашему порогу, score +10.
  • Наличие фишинговых слов в домене: проводим keyword matching. Есть – score +15.
  • Текстовое сходство: cos_sim = 0.85 > 0.7, score +30.
  • Проверка action форм: указывает на чужой домен, на текущий подозрительный домен, или пустой – один из самых явных признаков фишинга, score +20.
  • Визуальное сходство: SSIM > 0.85, score +25.
Итоговый score = сумма сигналов. Максимум – 100.

Правила принятия решения:
  • Score ≥ 70 => фишинг. Автоматически генерируем abuse-отчёт и отправляем в takedown.
  • 40 ≤ Score < 70 => подозрительно. Отправляем аналитику на ручную верификацию.
  • Score < 40 => ложное срабатывание. Игнорируем.
Как улучшить модель:
  • Добавить проверку структуры DOM (схожесть HTML-дерева).
  • Анализировать SSL-сертификат: выпущен ли недавно, самоподписанный или Let's Encrypt.
Веса и пороги подбираются эмпирически под ваш бренд. Нужно прогонять тестовую выборку из реальных фишинговых и легитимных сайтов, смотреть на распределение score, регулировать пороги, чтобы минимизировать ложные срабатывания.

6. IOC Extraction: собираем улики​

Перед нами фишинг, что делать дальше? Просто заблокировать – мало. Нужно вытащить из неё IOC, которые помогут защитить инфраструктуру: домены, IP-адреса, email’ы злоумышленников. А после извлечения – отправить в MISP, где эти индикаторы будут жить, обогащаться и использоваться для детекции будущих атак.

6.1. Парсинг: домены, IP, email, все, что не приколочено​

Задача: из фишинговой страницы вытащить индикаторы, полезные для TI: домены перенаправления, IP-адреса панелей управления и C2, email’ы. Самый простой и надёжный способ – использовать регулярные выражения для поиска паттернов во всём HTML и JavaScript-коде.

Домены
Python:
import re

def extract_domains(html):
    pattern = r'(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}'
    domains = set(re.findall(pattern, html))
    blacklist = {'example.com', 'localhost', '0.0.0.0'}
    return [d for d in domains if d not in blacklist and len(d) < 255]

IP-адреса
Python:
def extract_ips(html):
    ipv4_pattern = r'\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b'
    ipv6_pattern = r'\b(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\b'
    ips = set(re.findall(ipv4_pattern, html) + re.findall(ipv6_pattern, html))
   
    # Отсеиваем невалидные и приватные
    import ipaddress
    valid = []
    for ip in ips:
        try:
            ip_obj = ipaddress.ip_address(ip)
            if not ip_obj.is_private:
                valid.append(ip)
        except:
            continue
    return valid

Email
Можно использовать готовую библиотеку email-harvest – она умеет распознавать [at], (at), HTML-сущности, URL-кодирование и прочее.
Python:
from email_extractor import EmailExtractor # pip install email-harvest

ex = EmailExtractor()
emails = ex.from_html(html)  # деобфусцированные адреса

6.2. Интеграция с MISP: наш покедекс​

Malware Information Sharing Platform (MISP) – платформа с открытым исходным кодом для сбора, хранения, корреляции и обмена информацией об угрозах. В пайплайне автоматически создаем событие для каждого фишингового домена и добавляем туда найденные IOC в виде атрибутов события.
Функция, которая вызывается после того, как убедились, что страница – фишинг:
Python:
from pymisp import PyMISP # pip install pymisp
from email_extractor import EmailExtractor

misp_url = 'https://your-misp-instance.org'
misp_key = 'your-api-key' # API-ключ в веб-интерфейсе нашего MISP-сервера

def push_to_misp(phishing_domain, html_content):
    misp = PyMISP(misp_url, misp_key, ssl=True) # ssl=False для тестового окружения
   
    # Проверяем, существует ли событие для этого домена
    if misp.search(controller='events', value=phishing_domain):
        print(f"Событие для {phishing_domain} уже существует")
        return
   
    # Извлекаем IOC (см. п.6.1)
    domains = extract_domains(html_content)
    ips = extract_ips(html_content)
    emails = EmailExtractor().from_html(html_content)
   
    # Создаём событие
    event = misp.new_event(
        info=f"Phishing site: {phishing_domain}", # краткое описание события
        distribution=1, # уровень распространения
        threat_level_id=2, # уровень угрозы
        analysis=2 # стадия анализа
    )
   
    # Добавляем в атрибуты - индикаторы (IP, домен, email, хеш файла)
    for domain in domains:
        misp.add_attribute(event, type='domain', value=domain, category='Network activity')
    for ip in ips:
        misp.add_attribute(event, type='ip-dst', value=ip, category='Payload delivery')
    for email in emails:
        misp.add_attribute(event, type='email-dst', value=email, category='Financial fraud')
   
    misp.tag(event, 'type:phishing')
    misp.tag(event, f'osint:brand="SuperBank"')
   
    print(f"Событие {event.uuid} создано. Доменов: {len(domains)}, IP: {len(ips)}, Email: {len(emails)}")
Поля и атрибуты события MISP помогают SOC быстро классифицировать инцидент и приоритезировать работу.

7. Takedown Automation: не ну это уже бан​

Венцом всей прошлой работы является блокировка – чтобы сайт перестал открываться. Автоматизация takedown'а сокращает время жизни фишингового сайта с дней до часов/минут.

7.1. Abuse Report Template: время писать отчеты​

Abuse-отчёт – это обращение с требованием заблокировать ресурс. Сначала нужно понять, кто управляет доменом: регистратор (управляет делегированием) или хостинг-провайдер (размещает контент). Самый простой путь – использовать библиотеку abuse-whois (альтернатива python-whois). Она заточена на поиск контактов для сообщений об инцидентах, работает с доменами, IP, URL и email.
Python:
import asyncio
from abuse_whois import get_abuse_contacts # pip install abuse-whois

async def get_abuse_info(resource: str):
    try:
        abuse_info = await get_abuse_contacts(resource)
        if abuse_info:
            contacts = abuse_info.get('abuse_contacts')
            return contacts[0] if contacts else None
    except Exception as e:
        print(e)
    return None

abuse_email = asyncio.run(get_abuse_info("superbank-verify.click"))

Пример шаблона отчета для Jinja2:
Код:
To: {{ abuse_contact }}
Subject: [Phishing] Domain {{ domain }} impersonating {{ brand }}

Dear {{ registrar_name }} Abuse Team,
We have detected an active phishing website that is using your domain services:

- **Phishing URL**: https://{{ domain }}{{ path }}
- **Targeted Brand**: {{ brand }}
- **Detection Time**: {{ timestamp }}
- **Evidence Screenshot**: [Attached]

**This domain is an exact visual/textual clone of our official website.**
SSIM similarity score: {{ ssim_score }}. It is actively collecting user credentials.
Please take immediate action to suspend this domain or remove the abusive content to prevent further fraud.

Contact:
{{ analyst_name }}
{{ company }} Anti-Fraud Department
{{ contact_email }}
Case ID: {{ case_id }}

Генерация в Python:
Python:
from jinja2 import Template
from pathlib import Path # pip install Jinja2

template_content = Path('abuse_template.txt').read_text(encoding='utf-8')
template = Template(template_content)

report = template.render(
    abuse_contact='abuse@registrar.com',
    domain='superbank-verify.click',
    brand='SuperBank',
    registrar_name='Namecheap',
    path='/login',
    timestamp='2025-04-21 12:00:00 UTC',
    ssim_score=0.94,
    analyst_name='Ivan Petrov',
    company='SuperBank Security',
    contact_email='antifraud@superbank.ru',
    case_id='PH-2025-0421-001'
)

Лучшие практики составления отчёта:
  • Отправляйте жалобу оперативно: каждая минута промедления – новые жертвы.
  • Доказательства: прикладывайте скриншот страницы с видимым URL, HTTP-заголовки (особенно Host и Referer), путь к форме и скриншот оригинала.
  • Точность: конкретный URL фишинговой страницы, а также IP-адрес и whois-данные.
  • Бренд: указывайте, какой именно бренд нарушен.
  • Не спамьте: у регистраторов есть SLA.

7.2. Registrar API: хоть кто-то примите​

Международные доменные зоны
Здесь нет четких правил блокировки и каждый регистратор сам принимает решение о блокировке. Например, Cloudflare требует отчёт через API:
Python:
import requests

def report_to_cloudflare(domain, url, email, api_token, account_id):
    headers = {
        'Authorization': f'Bearer {api_token}',
        'Content-Type': 'application/json'
    }
    data = {
        'act': 'abuse_phishing',
        'email': email,
        'urls': [url],
        'type': 'PHISH'
    }
    resp = requests.post(
        f'https://api.cloudflare.com/client/v4/accounts/{account_id}/abuse-reports',
        json=data,
        headers=headers
    )
    return resp.json()

Правила игры в национальных зонах .RU и .РФ: Domain Patrol
В России в соответствии с пунктами правил регистрации доменных имён в доменах .RU и .РФ Координационного центра национального домена сети Интернет регистратор не имеет полномочий самостоятельно принимать решение о блокировке доменных имен. Регистратор вправе прекратить делегирование домена при поступлении регистратору мотивированного обращения организации, указанной Координационным центром как компетентной в определении нарушений в сети Интернет. Среди компетентных организаций: НКЦКИ, ЦБ РФ, F6, Ростелеком Солар, Kaspersky, BI.ZONE, Positive Technologies, Dr.Web, RU-CERT, Лига безопасного интернета, Роскомнадзор, ФГБУ НИИ Интеграл. В область компетенции этих организаций входит противодействие использованию доменных имен в целях фишинга, несанкционированного доступа в информационные системы третьих лиц, распространения вредоносных программ и управления вредоносными программами, находящихся в зонах .РФ и .RU. При обнаружении фактов неподобающего использования доменного имени обращаться на горячую линию компетентных организаций. Кроме того, можно интегрироваться с проектом "Доменный патруль".
Для автоматизации процесса takedown автоматический пайплайн должен быть интегрирован с системой компетентной организации. Вы передаете данные о фишинговом сайте в API вашего партнера, который уже отправляет мотивированное обращение регистратору.
Ваш Scrapy обнаружил фишинг => MISP сформировал событие => скрипт отправляет данные в API (или через SMTP на email-адреса) компетентной организации => партнер направляет обращение регистратору.

8. Full Pipeline: end‑to‑end - по полной программе​

Вот схема потока данных:
  1. Генерация кандидатов
    • Крон (раз в 6–12 часов) запускает dnstwist для вашего бренда.
    • dnstwist --registered выдаёт JSON со списком живых тайпо‑доменов.
    • Этот список загружается в Redis Set monitored_typos.
  2. Real‑time мониторинг
    • Демон Certstream крутится 24/7. На каждый новый сертификат:
      • Извлекаем домены из SAN.
      • Сначала проверяем if domain in monitored_typos: (совпадение с мутацией).
      • Если нет – прогоняем keyword matching (бренд + фишинговые слова).
    • При совпадении → rpush в Redis List scrapy:urls.
  3. Scrapy
    • Висит и ждёт URL.
    • Для каждого URL:
      • Скачивает страницу, обходит простую капчу (или маркирует).
      • Возвращает PhishingItem с HTML, скриншотом и метаданными.
  4. Content Analysis
    • Вычисляем текстовое сходство.
    • Сравниваем скриншоты (SSIM).
    • Выставляем общий score (0–100) по взвешенной модели.
  5. Decision & IOC Extraction
    • score >= 70 → фишинг.
      • Из HTML вытаскиваем домены, IP, email.
    • 40 <= score < 70 → отправляем аналитику на ручную верификацию.
  6. Threat Intelligence
    • Создаём событие в MISP.
    • Добавляем индикаторы с категориями Network activity, Payload delivery.
  7. Takedown
    • Определяем abuse‑контакты через abuse-whois.
    • Генерируем abuse‑отчёт.
    • Пробуем отправить через API регистратора.
    • Если API нет – падаем на email (SMTP) или на веб‑форму через requests.
    • В российских зонах .RU/.РФ – передаём в API, на email (SMTP) или на веб‑форму компетентной организации.
3.webp

Фишинг не исчезнет. Проактивный мониторинг + автоматический takedown = минимум жертв. Внедряйте, тестируйте, масштабируйте, спите спокойно.
 
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab

🚀 Первый раз на Codeby?
Гайд для новичков: что делать в первые 15 минут, ключевые разделы, правила
Начать здесь →
🔴 Свежие CVE, 0-day и инциденты
То, о чём ChatGPT ещё не знает — обсуждаем в реальном времени
Threat Intel →
💼 Вакансии и заказы в ИБ
Pentest, SOC, DevSecOps, bug bounty — работа и проекты от проверенных компаний
Карьера в ИБ →

HackerLab