Каждый раз, когда в компании случается очередное «мне написал директор, попросил срочно оплатить», разбор начинается одинаково: в SOC прилетает скриншот из мессенджера. Иногда — обычная пересылка письма, что не сильно лучше: клиент пересобирает MIME, и половина служебных заголовков до нас не доезжает. Смотреть, по сути, не на что, поэтому обсуждение сводится к подписи, стилю письма и спору о том, похоже или не похоже.
Так вот. Похоже или не похоже — это не метод. В письме есть примерно полтора десятка мест, где отправитель либо палится, либо нет, и почти все они читаются глазами за пару минут, без песочницы за миллион и без «отправьте нам образец, мы посмотрим». Ниже — то, на что я смотрю сам и чему учу первую линию, в том порядке, в котором это реально делается.
Сразу дисклеймер: ничего из этого не помогает против нормально сделанной атаки с захваченного легитимного ящика внутри доверенного контрагента. Там письмо пройдёт все проверки, потому что оно настоящее. Про это будет в конце.
Ноль: получить исходник
Первое и самое скучное, но без этого дальше можно не начинать. Нужен .eml — письмо целиком, со всеми служебными заголовками. Пересылка вложением (message/rfc822) их сохраняет, обычная — нет.
Outlook: открыть письмо в отдельном окне → Файл → Свойства → поле «Заголовки Интернета». Или пересылка «как вложение» (Ctrl+Alt+F).
Gmail: «Show original», там же кнопка «Download original» — получаете
.eml.Thunderbird: Ctrl+U.
На шлюзе (postfix + dovecot) файл просто лежит в maildir, забирается
cat-ом.
Если у вас есть Exchange/Defender — Get-MessageTrace и Get-MessageTraceDetail покажут маршрут и вердикт, но сырых заголовков в них нет, так что просить у пользователя вложение всё равно придётся.
Дальше я показываю разбор на синтетическом письме — я собрал его сам из типичных кусков, которые встречались в подобных рассылках, чтобы можно было спокойно печатать домены и не палить чужие инциденты. Механика от этого не меняется.
Один: Received читается снизу вверх
Received: from mail.corp-holding.ru (mail.corp-holding.ru [10.20.1.14]) by mx01.corp-holding.ru (Postfix) with ESMTP id 4B2C1A0F3 for <buhgalteria@corp-holding.ru>; Tue, 16 Sep 2026 09:41:22 +0300 (MSK) Received: from vps-8842.hostprovider.example (vps-8842.hostprovider.example [203.0.113.77]) by mail.corp-holding.ru (Postfix) with ESMTPS id 91EE420B4 for <buhgalteria@corp-holding.ru>; Tue, 16 Sep 2026 09:41:19 +0300 (MSK) Received: from User (unknown [198.51.100.23]) by vps-8842.hostprovider.example with SMTP id 7fa2c31b for <buhgalteria@corp-holding.ru>; Tue, 16 Sep 2026 06:41:11 +0000 (UTC)
Каждый сервер, через который прошло письмо, дописывает свой Received сверху. Значит, читать надо снизу вверх: нижний — самый ранний, ближайший к отправителю.
Что здесь видно:
Самый нижний хоп — from User (unknown [198.51.100.23]). Литеральное User в качестве HELO-имени — это дефолт нескольких старых рассыльщиков, живой почтовый сервер так себя не представляет. unknown означает, что обратной DNS-записи для IP нет либо она не сходится с прямой. Уже двух этих деталей достаточно, чтобы не читать письмо, а идти дальше по чек-листу.
Второй момент — арифметика по времени. От 06:41:11 UTC до 09:41:19 MSK, то есть 06:41:19 UTC — восемь секунд на весь путь. Тут всё честно. А вот когда между соседними хопами разрыв в несколько часов или, наоборот, время идёт назад — это либо кривая рассылка, либо подделанные вручную заголовки. Подделать можно только те Received, которые ниже вашего периметра: всё, что дописали ваши собственные MX, доверять можно, всё, что ниже — это слова отправителя.
И третье: провайдер. 203.0.113.77 — VPS у хостера. Письмо якобы от гендиректора вашей компании, а вылетает с арендованной виртуалки. Проверяется в одну команду:
whois 203.0.113.77 | grep -iE 'netname|orgname|country|descr' dig -x 203.0.113.77 +short
Два: Authentication-Results — три проверки, которые чаще всего понимают неправильно
Если ни одной из трёх аббревиатур вы не касаетесь по работе каждый день, вот минимум, без которого дальше будет непонятно.
В письме два разных «от». Первое — MAIL FROM из SMTP-конверта: адрес, который отправляющий сервер называет при доставке, в заголовках он виден как Return-Path. Пользователь его не видит никогда. Второе — заголовок From, он же header.from; вот его и рисует почтовый клиент в списке писем. Совпадать они не обязаны, и вся дальнейшая история растёт именно отсюда.
SPF отвечает на вопрос «имел ли право этот сервер отправлять почту от такого домена» и смотрит при этом на MAIL FROM. Владелец домена публикует в DNS список своих отправителей, принимающая сторона сверяет с ним IP.
DKIM — криптографическая подпись письма. Отправляющий сервер подписывает заголовки и тело приватным ключом, публичный кладёт в DNS. Получатель проверяет подпись и понимает: письмо в пути не подменили, и подписал его домен, указанный в подписи как header.d.
DMARC связывает первые два с тем From, который видит человек, и говорит получателю, что делать с письмом, если связь не сходится. Без DMARC обе предыдущие проверки можно пройти, вообще не имея отношения к домену из From — что в примере ниже и происходит.
Authentication-Results: mx01.corp-holding.ru; spf=pass (mx01.corp-holding.ru: domain of bounce@mailer-8842.example designates 203.0.113.77 as permitted sender) smtp.mailfrom=bounce@mailer-8842.example; dkim=pass header.d=mailer-8842.example header.s=k1; dmarc=fail (p=NONE sp=NONE dis=NONE) header.from=corp-holding.ru
Вот на этом месте первая линия обычно и ошибается: видит два pass и делает вывод, что письмо легитимное. А оно нет.
SPF. В примере проверялся MAIL FROM, то есть bounce@mailer-8842.example. Атакующий владеет доменом mailer-8842.example, у него там своя SPF-запись, в которой честно перечислен его же VPS. Разумеется, pass. SPF сказал ровно одно: «этот сервер имеет право слать от имени того домена, который сам себе прописал».
DKIM. Публичный ключ лежит в TXT-записи <селектор>._domainkey.<домен>, селектор берётся из header.s. Проверить руками:
dig +short k1._domainkey.mailer-8842.example TXT
Тут pass и header.d=mailer-8842.example. То есть подписал письмо снова свой домен атакующего, а не corp-holding.ru. DKIM=pass без взгляда на header.d не значит ничего вообще.
DMARC. Здесь появляется alignment — требование, чтобы домен из header.from (тот самый видимый From) сошёлся либо с доменом SPF, либо с доменом DKIM-подписи. В relaxed-режиме — с точностью до организационного домена, то есть поддомен подойдёт. Здесь header.from=corp-holding.ru, а оба pass пришли от mailer-8842.example — не сходится ни там, ни там. dmarc=fail.
И самое неприятное в этой строке: p=NONE. Политика домена — «ничего не делай». То есть владелец домена DMARC-запись опубликовал, отчёты, наверное, куда-то собирает, а вот применять не рискнул. Письмо доехало.
Проверьте прямо сейчас свой домен:
dig +short _dmarc.corp-holding.ru TXT "v=DMARC1; p=none; rua=mailto:dmarc@corp-holding.ru; fo=1"
Если у вас там p=none дольше, чем несколько месяцев «на прогрев» — вы не защищены, вы собираете статистику. Я понимаю, почему так: переключить домен на p=quarantine, тем более на p=reject — это риск потерять легитимную почту от какого-нибудь отдела маркетинга, который рассылает через сервис, о котором никто в IT не знает. Это нормальный страх. Но он лечится за пару недель по rua-отчётам, а не годами. Промежуточная ступенька есть: p=quarantine; pct=10, дальше по нарастающей.
Отдельная тонкость про sp=. Если основной домен вы закрыли, а поддомены оставили без политики — рассылать будут от hr.corp-holding.ru, и получатель увидит вполне корпоративное имя. sp=reject ставится вместе с p=.
Три: From, Reply-To и то, что показывает клиент
From: "Иванов Сергей Петрович | Генеральный директор" <s.ivanov@corp-hoIding.ru> Reply-To: s.ivanov.corp@mail-box.example Return-Path: <bounce@mailer-8842.example>
Три разных адреса в трёх полях. Классика: From — для глаз, Reply-To — куда реально уйдёт ответ, Return-Path — куда уйдут уведомления о недоставке.
Теперь присмотритесь к домену в From. corp-hoIding.ru — там не строчная l, а заглавная I. В большинстве шрифтов интерфейса они неотличимы. Это даже не самый злой вариант, потому что бывает хуже — кириллица:
python3 -c "print('corp-holding.ru'.encode('idna'))" b'corp-holding.ru' python3 -c "print('соrp-holding.ru'.encode('idna'))" # 'с' и 'о' — кириллические b'xn--rp-holding-dvi9a.ru'
Если домен из From при кодировании в IDNA превращается в xn--..., а выглядел как латиница — это гомоглиф, и разговаривать больше не о чем. Правило для почтового шлюза формулируется ровно так и живёт годами без фолзов.
И ещё: Reply-To на публичном почтовом сервисе в письме, которое якобы отправил ваш сотрудник со своего корпоративного ящика, — это не «он с телефона писал». Такого не бывает.
Четыре: тело, ссылки и вложения
Смотреть на отрендеренный HTML бессмысленно, смотреть надо в исходник. Первое, что делаю — вытаскиваю все URL и сравниваю текст ссылки с href:
import email, re from email import policy msg = email.message_from_file(open('sample.eml'), policy=policy.default) body = msg.get_body(preferencelist=('html', 'plain')).get_content() for href, text in re.findall(r'<a[^>]+href="([^"]+)"[^>]*>(.*?)</a>', body, re.S | re.I): text = re.sub(r'<[^>]+>|\s+', ' ', text).strip() print(f'{text[:60]:<60} -> {href[:90]}')
Дальше — то, что вылезает почти в каждом таком письме.
Первое, ссылки. Часто ведут на редирект-сервис или на легитимный домен с открытым редиректом — это способ обойти фильтрацию по репутации домена. Разворачивать надо без загрузки контента:
curl -sIL -A 'Mozilla/5.0' -o /dev/null -w '%{url_effective}\n' 'https://<ссылка>'
Только не с рабочей машины, не с корпоративного IP: многие фишинг-киты фильтруют посетителей по ASN, User-Agent, геолокации. Вам отдадут заглушку, вы решите, что всё чисто. Плюс сам факт клика иногда подтверждает атакующему, что адрес живой.
Второе — вложение. .html, .htm, .svg во вложении — почти всегда локальная фишинговая форма, которая открывается прямо в браузере жертвы, поэтому её не видит ни один URL-фильтр. .svg особенно, потому что это XML, внутри которого спокойно живёт <script>. Архивы с паролем, где пароль в теле письма — это прямой обход антивируса на шлюзе, легитимного применения у этого приёма практически нет.
Третье — X-Mailer и Message-ID. Письмо якобы из Outlook, а Message-ID сформирован по чужому шаблону, или X-Mailer: PHPMailer 6.x. По одному этому признаку решение не принимается, но в общую копилку идёт.
Пять: складываем в скоринг, а не в «подозрительное»
Отдельный признак почти всегда объясним. Гомоглиф-домен объясним быть не может, а вот dmarc=fail — легко: домен-отправитель просто не настроен. Поэтому в правилах я не режу по одному признаку, а складываю.
Скелет, из которого у меня обычно начинается такой скрипт:
import email, re from email import policy WEIGHTS = { 'dmarc_fail': 3, 'homoglyph_from': 5, 'replyto_mismatch': 2, 'freemail_replyto': 2, 'display_name_vip': 2, 'html_attachment': 3, 'first_contact': 2, # адрес не встречался в логах шлюза за 90 дней } VIP = re.compile(r'директор|генеральн|финанс|CFO|CEO', re.I) FREE = ('mail.ru', 'yandex.ru', 'gmail.com', 'bk.ru', 'inbox.ru', 'list.ru') def score(path, known_senders): msg = email.message_from_file(open(path), policy=policy.default) hits, auth = [], msg.get('Authentication-Results', '') from_hdr = msg.get('From', '') from_dom = from_hdr.rsplit('@', 1)[-1].strip('> ').lower() reply_to = msg.get('Reply-To', '') if 'dmarc=fail' in auth or 'dmarc=none' in auth: hits.append('dmarc_fail') try: if from_dom.encode('idna').decode().startswith('xn--'): hits.append('homoglyph_from') except UnicodeError: hits.append('homoglyph_from') if reply_to and reply_to.rsplit('@', 1)[-1].strip('> ').lower() != from_dom: hits.append('replyto_mismatch') if reply_to.rsplit('@', 1)[-1].strip('> ').lower() in FREE: hits.append('freemail_replyto') if VIP.search(from_hdr): hits.append('display_name_vip') for part in msg.walk(): fn = (part.get_filename() or '').lower() if fn.endswith(('.html', '.htm', '.svg')): hits.append('html_attachment') break if from_hdr not in known_senders: hits.append('first_contact') return sum(WEIGHTS[h] for h in hits), hits
Числа тут не священные, их надо подкручивать под свой поток. Смысл в другом: display_name_vip сам по себе весит 2 — это ничто, у вас директор действительно пишет письма. Но display_name_vip + first_contact + freemail_replyto — это уже 6, и на такое стоит смотреть человеку.
Отдельно про first_contact. По моему опыту это один из самых недооценённых признаков и при этом самый дешёвый: у вас на шлюзе уже есть логи за последние месяцы, из них собирается множество адресов, с которыми компания реально переписывалась. Первое письмо от нового отправителя, где просят что-то сделать со сроком — это совершенно другой уровень риска, чем то же письмо от контрагента, с которым вы год ведёте переписку. Многие шлюзы умеют вешать на такие письма плашку «внешний отправитель, первое письмо» прямо в теле, и это, пожалуй, самая эффективная строчка текста, которую можно добавить в почтовую систему.
Шесть: где всё это не работает
Теперь честная часть, ради которой я в основном и пишу.
Всё вышеописанное ловит подделку. Оно не ловит захват.
Если атакующий получил доступ к реальному ящику вашего контрагента — а это самый частый сценарий в BEC — письмо приходит с настоящего сервера, проходит SPF, DKIM и DMARC, лежит в существующем треде, внизу — корректная подпись и вся история переписки. Скоринг даст ноль. Никакой заголовок не поможет, потому что письмо технически подлинное, поддельно только намерение.
Что там остаётся:
Аномалии поведения, а не заголовков: отправитель, который два года писал раз в неделю по будням из одной страны, вдруг шлёт ночью из другой ASN, а в письме — смена реквизитов. Для Microsoft 365 это Get-MailboxAuditLog, правила пересылки (первое, что делает атакующий после захвата ящика — ставит правило, прячущее ответы в «Архив»), новые OAuth-разрешения приложений.
И процедура на стороне бухгалтерии. Смена платёжных реквизитов подтверждается звонком по номеру из договора — не по номеру из письма. Это скучная, нетехническая мера, и по итогу она спасает деньги чаще, чем весь мой скоринг.
Собственно, поэтому я и не верю в разговоры о том, что фишинг решается покупкой правильного продукта. Заголовки отсекают массовое. Дорогое, адресное упирается в другое: есть ли у человека право сказать «я перезвоню, уточню» — не получив за это по шапке.
Что можно сделать за один вечер
Если из всего текста хочется взять что-то немедленно, я бы взял вот это, в таком порядке:
Посмотреть свою DMARC-запись. Если p=none — поставить в план переход на quarantine с pct, начиная с малого процента, и не забыть sp=.
Включить на шлюзе плашку для внешних отправителей, если её нет. Это делается за час и работает лучше, чем годовой курс security awareness.
Написать проверку на IDNA-домены в From. Двадцать строк, фолзов в русскоязычной корпоративной почте почти не даёт.
И научить первую линию читать Authentication-Results целиком, а не искать в ней слово pass. Из всех пунктов этот дешевле всего и экономит больше всего времени на разборах.
Комментарии (2)

zemeroff
25.08.2026 15:05Как будто компания не может арендовать виртуалку
Может и тогда при настройке почтового сервера делает PTR запись с почтовым корп доменом типа mail.corp-hoIding.ru, а мошенник не делает
andreymal
Как будто компания не может арендовать виртуалку
...то ничего страшного не случится, потому что по умолчанию
spиnpберут значение изpИз актуальной спецификации DMARC
pctуже удалёнА вообще вы в целом просто сделали то, что уже и так по умолчанию делает любой нормальный антиспам-движок (разве что переход по ссылкам для меня выглядит крайне сомнительно — для спамера это будет сигналом о том, что его письмо действительно дошло)