SMTP smuggling: как расхождение в конце DATA обходит SPF и DKIM — и как закрыть Postfix
SMTP smuggling — это не дыра в SPF или DKIM, а расхождение в том, как разные MTA определяют конец блока DATA. Разбираем атаку на уровне байтов, показываем payload-строки и даём рабочий main.cf, который закрывает Postfix и как отправляющий, и как принимающий сервер.
EvilMail Team1 августа 2026 г.11 мин чтения
Приходит письмо с идеальным spf=pass, dkim=pass и dmarc=pass, но от лица вашего CEO — с домена, к которому отправитель не имеет никакого отношения. Интуиция гонит искать утечку ключа или дыру в DNS. Искать надо не там. SMTP smuggling ломает не криптографию и не политики домена — он ломает согласие двух серверов о том, где именно заканчивается тело письма. Атакующий отправляет один поток DATA; отправляющий релей видит в нём одно письмо, а принимающий MTA — два. Второе, «контрабандное», письмо стартует уже внутри аутентифицированной TLS-сессии доверенного релея, поэтому проходит все проверки с чужим доменом в конверте.
Что на самом деле ломается: конец DATA, а не подпись
RFC 5321 §4.1.1.4 предельно конкретен: тело письма в фазе DATA завершается последовательностью из пяти байт — <CR><LF>.<CR><LF>, то есть 0D 0A 2E 0D 0A. Точка на отдельной строке, обрамлённая каноническими переводами строки. Всё остальное — нестандарт.
Проблема в том, что «нестандарт» разные MTA обрабатывают по-разному. Одни считают концом данных одиночный <LF>.<LF>
(
0A 2E 0A
), другие —
<CR>.<CR>
(
0D 2E 0D
), третьи строго требуют канонический CRLF и всё прочее игнорируют. Пока обе стороны диалога одинаково строги или одинаково либеральны — беды нет. Атака живёт ровно на стыке:
строгий отправитель + либеральный получатель
(или наоборот).
Определение, которое стоит держать в голове: smuggling — это один непрерывный TCP-поток фазы DATA, который релей передаёт целиком как тело одного сообщения, а получатель разрезает на две отдельные SMTP-транзакции. Никакого второго соединения, никакого второго IP. Один коннект, одна сессия, два письма на выходе.
Механика на уровне байтов
Пошагово атака выглядит так. У атакующего есть легитимный доступ к отправке через доверенный релей — общий Exchange Online, сервис рассылок, корпоративный smarthost. Он формирует письмо, тело которого содержит подделанный терминатор и вторую транзакцию:
Ключевой момент — байты между «телом первого письма» и вторым MAIL FROM. Атакующий вставляет туда \n.\n вместо канонического \r\n.\r\n.
Отправляющий релей строг: он ждёт \r\n.\r\n и одиночный \n.\n концом данных не считает. Для него всё, включая вторую пачку команд, — это просто тело первого письма. Он аутентифицирован, у него валидный DKIM-подписыватель, его IP в SPF-записи legit-sender.com. Он честно ретранслирует весь поток получателю по своей доверенной сессии.
Принимающий MTA либерален: он видит \n.\n, считает это концом первого письма, завершает первую транзакцию — и трактует остаток потока как новую транзакцию MAIL FROM:<[email protected]>. Но source IP и TLS-сессия по-прежнему принадлежат доверенному релею.
Почему SPF и DKIM тут бессильны
Разберём каждую проверку отдельно — «магия» рассеивается ровно тогда, когда понимаешь, на каком слое работает каждый механизм.
SPF валидирует envelope MAIL FROM против IP-адреса подключающегося сервера. Для контрабандного письма IP — это IP доверенного релея, который абсолютно легитимен. Домен во втором конверте атакующий ставит любой. Формально SPF проверяет «этот IP имеет право слать за этот домен» — но получатель применяет проверку к транзакции, которую релей вообще не считал отдельной транзакцией. Результат pass технически честен и содержательно бесполезен.
DKIM подписывает выбранные заголовки и тело письма. Но подписывающий сервер (релей) никогда не видел контрабандное письмо как отдельное сообщение — для него это был кусок тела письма №1. Значит, подписи для письма №2 просто не существует. Дальше всё решает политика получателя: при p=none, мягком DMARC или отсутствии строгой проверки отсутствие подписи не приводит к отклонению.
DMARC должен ловить именно спуфинг заголовка From, требуя выравнивания (alignment) между доменом из From и доменом, прошедшим SPF или DKIM. Но получатель видит контрабандное письмо как самостоятельную транзакцию с From: [email protected], а SPF-домен берёт из подставленного конверта — и здесь границы транзакции расходятся с тем, что подразумевал релей. В зависимости от реализации выравнивание либо ломается в пользу атакующего, либо проверка вовсе не срабатывает на «внутренней» второй транзакции.
Вывод секции ровно один: SPF, DKIM и DMARC — механизмы message-layer. Они рассуждают о содержимом сообщения и о том, кто имел право его отправить. SMTP smuggling — это transport-layer confusion, рассогласование парсеров на уровне TCP-потока. DNS-механизмы физически не видят проблему, потому что к моменту их работы поток уже разрезан на «сообщения» тем самым либеральным парсером, который и создал уязвимость. Докручивать DNS здесь — лечить симптом на другом органе.
Кто уязвим и где граница ответственности
Уязвимость всегда парная, поэтому и ответственность делится на две роли.
Sender (исходящий релей) обязан не пропускать «bare» LF/CR внутри тела: либо нормализовать одиночные переводы строки в CRLF, либо разорвать сессию. Если релей ретранслирует поток как есть, он становится оружием.
Receiver (входящий MTA) обязан не принимать нестандартные завершители как конец DATA. Строгий парсер, признающий только \r\n.\r\n, не даст разрезать поток в неожиданном месте.
Публично атаку раскрыл Тимо Лонгин (SEC Consult) в декабре 2023 — с демонстрацией на 37C3; уязвимости присвоили CVE-2023-51764. В списке затронутых — GMX, Ionos, Cisco Secure Email (бывший IronPort), а также сценарии с Exchange Online на исходящем плече. Postfix по умолчанию требует канонический \r\n.\r\n как конец DATA, поэтому в роли получателя он не «режет» поток по bare-LF так охотно, как либеральные стеки. Тем не менее ему тоже присвоили CVE-2023-51764, и разработчики добавили штатную защиту для обеих ролей — параметр smtpd_forbid_bare_newline, о котором ниже.
Воспроизведение в лаборатории
Чистый способ увидеть атаку — говорить с сервером сырым протоколом и параллельно снимать дамп. Тело с подделанным терминатором нельзя гнать через swaks или почтовую библиотеку: они нормализуют переводы строк и уничтожат ровно тот bare-LF, который и есть суть атаки. Готовим полную сессию в файле, где сами контролируем каждый байт:
bash
# session.txt: между телом письма №1 и вторым MAIL FROM стоит bare-LF (0A 2E 0A)
printf 'EHLO test\r\nMAIL FROM:<[email protected]>\r\nRCPT TO:<[email protected]>\r\nDATA\r\nSubject: one\r\n\r\nbody one\n.\nMAIL FROM:<[email protected]>\r\nRCPT TO:<[email protected]>\r\nDATA\r\nSubject: smuggled\r\nFrom: CEO <[email protected]>\r\n\r\nbody two\r\n.\r\nQUIT\r\n' > session.txt
# снимаем байты сессии в другом терминале
tcpdump -i any -A 'port 25' -w smuggle.pcap
# отправляем байт-в-байт, без нормализации переводов строк
nc mx.target.example 25 < session.txt
Диагностика на стороне получателя — самое наглядное. Если MTA уязвим, в мейллоге появятся две записиpostfix/cleanup ... message-id= от одного входящего postfix/smtpd ... connect from. Один коннект, один IP, два message-id — подпись разрезанной сессии. На защищённом Postfix со значением normalize вторая запись не появляется: контрабандная транзакция растворяется в теле письма №1, и на один коннект приходится один message-id. При значении reject в логе появится строка отказа 550 5.5.2 Error: bare <LF> received, а при пайплайнинге — improper use of SMTP command pipelining.
Защита Postfix: конфиг, который закрывает обе роли
Ядро всей темы — несколько строк в main.cf. Начинаем с проверки версии, потому что нужный параметр появился не везде:
bash
postconf mail_version
Параметр smtpd_forbid_bare_newline появился в стабильных ветках Postfix 3.8.4, 3.7.9, 3.6.13 и 3.5.23 (релизы от 22 декабря 2023 года), а также в 3.9. В обновлении от 21 января 2024 года — 3.8.5, 3.7.10, 3.6.14, 3.5.24 — добавили значение normalize, которое разработчики теперь и рекомендуют по умолчанию. Если у вас старее — обновляйте пакет, ручной бэкпорт нежелателен. Рабочая конфигурация:
ini
# требовать канонический конец DATA; bare LF/CR трактовать как обычный текст,
# а не как терминатор — smuggling закрыт без отклонения легитимной почты
smtpd_forbid_bare_newline = normalize
# не трогать доверенные внутренние источники
smtpd_forbid_bare_newline_exclusions = $mynetworks
# зарезать смуглинг через раннюю конвейеризацию команд
smtpd_data_restrictions = reject_unauth_pipelining
# строгий синтаксис адресов конверта (только <addr> в скобках)
strict_rfc821_envelopes = yes
# закрыть побочные каналы разведки
disable_vrfy_command = yes
Что делает каждая строка:
smtpd_forbid_bare_newline = normalize — концом данных Postfix признаёт только канонический \r\n.\r\n. Одиночный LF/CR больше не разрезает поток: строка \n.\n остаётся обычным текстом внутри тела, а не терминатором. При этом легитимные письма с редким bare-LF не отклоняются. Для максимальной строгости есть значение reject — оно рвёт сессию с любым bare-LF (550 5.5.2 Error: bare <LF> received), но выше риск отсечь легитимную почту. Устаревшее yes в новых версиях — просто алиас для normalize.
smtpd_forbid_bare_newline_exclusions = $mynetworks — исключение для доверенных сетей. Ставьте его осознанно: любое исключение возвращает риск для источников внутри него.
reject_unauth_pipelining в smtpd_data_restrictions — отвергает клиента, который шлёт команды пачкой, не дождавшись ответа, вне разрешённого PIPELINING. Смуглинг-полезная нагрузка часто выглядит именно так.
strict_rfc821_envelopes = yes — запрещает вольности в MAIL FROM/RCPT TO, сужая пространство для трюков с конвертом.
Если ваш сервер (или, в нашем случае, инфраструктура evilmail.pro) ретранслирует чужую почту, роль sender не менее важна. Здесь та же bare-newline защита включается без широкого исключения для аутентифицированных клиентов — именно они чаще всего и есть точка входа для контрабанды. Значение normalize тут особенно уместно: Postfix приводит одиночные LF к CRLF ещё на входе, и до получателя физически не доходят байты, которые тот мог бы истолковать как терминатор. Логируйте reject-строки postfix/smtpd, заведите алерт на всплеск improper command pipelining: рост этих событий — ранний признак, что кто-то прощупывает разрезание сессии. Для прикладных сервисов рассылки добавьте санитизацию тела на уровне приложения: заменяйте любой одиночный LF на CRLF до передачи в SMTP.
Отдельно про DMARC. Ужесточение до v=DMARC1; p=reject; adkim=s; aspf=s — правильная гигиена и уменьшает общую поверхность спуфинга, но не считайте это защитой от smuggling. Строгий alignment работает на message-layer и по-прежнему слеп к рассогласованию парсеров. Он снижает шум, не закрывает саму дыру.
Включить strict_rfc821_envelopes = yes и disable_vrfy_command = yes.
Ужесточить DMARC до p=reject; adkim=s; aspf=s как гигиену, не как защиту от smuggling.
Настроить алерт на всплеск improper command pipelining и bare <LF> received в логах.
Прогнать nc-тест с полной сырой сессией и bare-LF терминатором против собственного MX.
Проверить мейллог: два message-id из одной входящей сессии одного IP = сервер разрезан, конфиг не применился.
Задокументировать роль каждого MTA (sender/receiver), выполнить postfix reload и провалидировать postconf -n.
SMTP smuggling — хорошее напоминание, что почтовая безопасность многослойна и слой транспорта нельзя чинить средствами слоя сообщений. SPF, DKIM и DMARC остаются обязательными, но границу письма определяет парсер DATA, и именно за ним нужен строгий Postfix — с обеих сторон релея.