Логотип ШифрыШифры
ГлавнаяЖурналЛотыУзел ИИВойти
ГлавнаяЖурналЛотыУзел ИИ
Войти
Логотип ШифрыШифры2К26Версияъ1
EmailMAX
Назад к логу

Атака NatJack на таблицу NAT угоняет TCP-сессии и подменяет DNS в Windows, Linux и Docker

8 августа 2026 г.•Репа
#атаки#код#сети

В зале Oceanside D на Black Hat USA в этом августе у Malcolm Stagg было тридцать минут. Он не принёс ни свежего 0-day в популярной железке, ни цепочки эксплойтов с эффектным демо на сцене. Он принёс класс атак на то, что стоит в каждой сети от домашнего роутера до облачного гейтвея и что никто никогда не считал мишенью – на таблицу трансляции адресов.

Приём называется NatJack, и формулируется он до обидного просто. Если ты сидишь за тем же NAT, что и жертва, ты можешь писать в её строку в этой таблице. Не читать трафик, не ломать шифрование, не подбирать пароли – просто редактировать чужую запись в общей структуре данных. Дальше по вкусу: угнать живую TCP-сессию, увести DNS-ответ мимо жертвы, вычислить её внешний порт и внутренний адрес, забить таблицу так, что весь сегмент перестанет открывать соединения.

Любой сетевой инженер скажет, что NAT никогда не был средством безопасности. Это правда, это написано в каждом учебнике. Беда в том, что отрасль двадцать лет строила на нём изоляцию: bridge-сеть Docker, CNI в Kubernetes, виртуальные свитчи Hyper-V, NAT Gateway в облаках. Общий NAT оказался общей памятью, в которую пишут все соседи. И патча для класса не существует – только митигации.

Допущение, которое не пересматривали тридцать лет

NAT рождался как заплатка на нехватку адресов в мире, где сеть была кооперативной средой. Устройства за одним роутером принадлежали одной организации, чаще одному человеку, и вопрос «а что, если сосед по подсети настроен враждебно» звучал примерно как «а что, если мой принтер меня ненавидит». Из этого выросло допущение, которое с тех пор никто не трогал: хосты за общим NAT не манипулируют состоянием соединений друг друга.

Допущение держалось ровно до тех пор, пока за одним NAT сидели коллеги. Потом появились контейнеры, поды, виртуалки, мультитенантные ноды и serverless, и «сосед по подсети» превратился в чужой код, который ты выкачал из реестра пять минут назад. Устройство осталось прежним, модель угрозы поменялась целиком, а код трансляции продолжил жить по правилам девяностых.

Stagg копал в эту сторону почти три года, в основном в свободное время, через собственную консультацию SODIUM-24 – идея, по его словам, пришла во время рядового задания на площадке Synack. Результат он проверил на десятках реальных продуктов сетевой инфраструктуры от разных производителей с независимыми кодовыми базами. Вывод неприятный своей универсальностью: все протестированные устройства, выполняющие любую форму NAT, оказались уязвимы к части атак или ко всем сразу. Не «эти вендоры накосячили», а «так спроектировано везде».

Почему это не ARP-спуфинг, хотя выглядит похоже

По результату NatJack напоминает классическую подмену ARP: чужой трафик едет к тебе, ты можешь его читать, рвать и вклиниваться. По исполнению – совсем другая история, и вот здесь начинается самое интересное.

Атака на ARP живёт в пределах широковещательного домена. Против неё за двадцать лет накопился нормальный арсенал: Dynamic ARP Inspection, DHCP snooping, port security, статические привязки. Главный же ответ архитектора звучит так: разнеси недоверенное по разным VLAN, и проблема закрыта.

NatJack обходит весь этот арсенал целиком, потому что работает не на втором уровне, а на состоянии трансляции. Атакующему не нужно быть в одном широковещательном домене с жертвой. Другая подсеть – годится. Другой VLAN – годится. Единственное, что должно быть общим, это NAT. Каноничный совет «изолируй недоверенное в отдельный VLAN» здесь не просто не помогает – он создаёт ложное чувство, что вопрос закрыт, пока обе зоны продолжают ходить наружу через один и тот же роутер.

Входной билет тоже стоит недорого: нужен привилегированный доступ на хосте позади того же NAT и возможность отправлять пакеты с подменённым адресом. То есть raw-сокеты. То есть NET_RAW – capability, которая в Docker выдаётся контейнеру по умолчанию.

Пять способов написать в чужую строку

Класс разбит на пять техник, и они не равнозначны: одна кормит другую разведданными, третья работает вообще без предварительных условий.

Угон TCP через downstream-спуфинг. Атакующий с LAN-стороны шлёт пакеты от имени жертвы и добивается того, чтобы NAT снёс или переписал маппинг её активного соединения на себя. С этого момента трафик, который сервер отправляет жертве, приезжает атакующему. Дальше он может представляться жертвой перед сервером, перехватывать валидный трафик, вкидывать свои данные на прикладном уровне, обрывать сессии и разворачивать ограниченный MITM против долгоживущих HTTP-соединений. Ограниченный – потому что TLS никуда не делся, и это важно проговорить честно: шифрованную сессию так не прочитать.

Угон TCP через upstream-спуфинг. Вариант дороже: нужен и хост внизу, и внешний сервер, умеющий спуфить адреса. Зато трафик уводится от легитимного клиента полностью. У техники есть входное условие – надо знать внешний эфемерный порт жертвы. За этим идём к следующему пункту.

Раскрытие порта и адреса жертвы. Реализации NAT раздают внешние порты предсказуемо, и по реакции устройства на аккуратно составленные пробы можно вычислить, какой внешний порт соответствует соединению жертвы, а при удачном стечении – и её внутренний адрес. Предсказуемость эта не от лени: сохранение исходного номера порта и endpoint-independent mapping существуют, чтобы работали P2P, STUN, игровые сессии и VoIP. Классический размен удобства на предсказуемость, где предсказуемость оказалась разведданными для соседа.

Угон UDP DNS. У записи под UDP защита ещё жиже: нет рукопожатия, нет состояния сессии, нет ничего, что можно было бы провалидировать. Атакующий сносит или подменяет NAT-запись, соответствующую DNS-запросу жертвы, легитимный ответ резолвера уезжает к нему, а жертве прилетает поддельный. Обрати внимание на разницу с классикой: тут не нужно угадывать transaction ID и порт источника, как в классической атаке Каминского. Ты не гоняешь вероятности – ты перехватываешь ответ, потому что таблица отправляет его тебе. Мы недавно разбирали захват DNS в отельном Wi-Fi, и там атакующему требовалось владеть самим шлюзом. NatJack снимает и это требование: достаточно стоять рядом.

Исчерпание таблицы. Самая тупая и самая надёжная из пяти: генерируешь массу спуфнутых потоков, каждый оставляет в таблице персистентную запись, таблица забивается, легитимные клиенты перестают открывать новые исходящие соединения. Отказ в обслуживании не по полосе, а по состоянию – канал свободен, а коннектов нет.

Одна строка, которой не было двадцать лет

Самое предметное во всей истории – Linux. Здесь у нас не абстрактное «design-level», а конкретная строка кода с конкретной датой рождения.

CVE-2026-63913, подсистема Netfilter. В конечном автомате TCP conntrack обнаружилась вот какая логика: после того как замечен SYN, пришедший RST с невалидным номером последовательности переводил запись в состояние TCP_CONNTRACK_CLOSE – без проверки направления. Код исходил из того, что RST это ответ на исходящий SYN, и не проверял ни направление пакета, ни то, что встречный SYN вообще посылали. В переводе на человеческий: крафченая последовательность из SYN и следом RST с мусорным seq досрочно убивала живую запись NAT.

Патч, попавший в мейнлайн, выглядит так:

--- a/net/netfilter/nf_conntrack_proto_tcp.c
+++ b/net/netfilter/nf_conntrack_proto_tcp.c
@@ -1220,7 +1220,8 @@ int nf_conntrack_tcp_packet(struct nf_conn *ct,
 			new_state = old_state;
 		}
 		if (((test_bit(IPS_SEEN_REPLY_BIT, &ct->status)
-			 && ct->proto.tcp.last_index == TCP_SYN_SET)
+			 && ct->proto.tcp.last_index == TCP_SYN_SET
+			 && ct->proto.tcp.last_dir != dir)
 			|| (!test_bit(IPS_ASSURED_BIT, &ct->status)
 			    && ct->proto.tcp.last_index == TCP_ACK_SET))
 		    && ntohl(th->ack_seq) == ct->proto.tcp.last_end) {

Одна строка. Одно сравнение направления пакета с направлением ранее увиденного SYN. Двадцать лет её там не было.

Не фигура речи: в заголовке коммита стоит Fixes: 9fb9cbb1082d ("[NETFILTER]: Add nf_conntrack subsystem."). Это коммит Yasuyuki Kozakai от 10 ноября 2005 года, которым nf_conntrack вообще появился в ядре. Дыра ровесница подсистемы, поражены все версии начиная с 2.6.15.

Оценка от CNA ядра – 8.2 по CVSS, вектор AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:H. Разбор вектора тут поучительнее самой цифры. C:N – это логическая ошибка автомата, ни утечки памяти, ни чтения чужих данных, портится исключительно метаданные conntrack. I:L – ровно та же причина, модифицируется состояние трекинга, а не что-то ценное. А вот A:H заслуженная: рвать произвольные отслеживаемые соединения можно без аутентификации и сколько угодно раз, а на NAT-гейтвее или stateful-файрволе это означает устойчивый отказ для всех потоков, которые через него идут.

Исправлено в стабильных ветках 5.10.259, 5.15.210, 6.1.176, 6.6.143, 6.12.93, 6.18.35, 7.0.12 и в мейнлайне 7.1. Обновляться надо, но иллюзий строить не стоит: сам Stagg прямо говорит, что коммит чинит код-флоу и лишь митигирует downstream-спуфинг, повышая сложность атаки, а не закрывая её. Класс остаётся на месте.

«Totally bogus» – ровно до звонка из Azure

А вот теперь самая показательная часть истории, и она не про технику.

Двадцатого мая Greg Kroah-Hartman от имени команды безопасности ядра сообщает исследователю, что его отчёт уже отклонялся ранее как «totally bogus». Полностью бредовый. Точка.

Одиннадцатого июня Microsoft принимает отчёты по своему сервису AKS как проблему средней тяжести и обещает фикс начиная с ядра ветки 6.6.142.1.

Девятнадцатого июня в ядро приезжает митигация того самого downstream-спуфинга. Автор патча – Hamza Mahfooz с адресом hamzamahfooz@linux.microsoft.com. В подписях под коммитом Florian Westphal и Greg Kroah-Hartman.

Тот же баг. Тот же исследователь. Тот же мейнтейнер в подписи. Поменялся ровно один параметр – домен отправителя. Пока в дверь стучится независимый исследователь с PoC, отчёт «totally bogus»; когда стучится инженер облачной платформы с бизнесом на AKS, отчёт превращается в коммит за восемь дней.

Отдельная деталь для коллекции: Microsoft пометила upstream-вариант атаки как дубль downstream-варианта. Фикс же, как мы только что разобрали, митигирует только downstream. То есть половину закрыли, а вторую списали как повтор первой.

Windows: ошибка валидации происхождения

Вторая CVE – Microsoft, CVE-2026-56181, классифицирована как CWE-346, ошибка валидации происхождения. Речь про WinNAT, и задевает она Hyper-V в downstream-конфигурации.

Вектор: AV:A/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H, базовая оценка 8.3. Тут ключевое S:C – смена области. WinNAT обслуживает виртуальные сети гипервизора, и атака из одной виртуалки дотягивается до соединений соседней. Ровно тот сценарий, ради предотвращения которого виртуализацию и покупают. Уязвимы сборки Windows 11 24H2 до 26100.8875, 25H2 до 26200.8875, 26H1 до 28000.2525 и Windows Server 2025 до 26100.33158; патч приехал четырнадцатого июля.

В тот же день Microsoft присвоила уязвимости среднюю степень тяжести и заплатила за неё ноль. Обоснование дословно: атака зависит от аллокации эфемерных портов. Прекрасный аргумент, если не знать, что тот же самый исследователь в том же самом пакете отчётов приложил технику, которая перебирает весь диапазон эфемерных портов за секунды. Довод строится на препятствии, которое в комплекте идёт уже снесённым.

Горизонтальная диаграмма выплат за исследование NatJack: Google Cloud NAT 600 долларов, Google GKE 500 долларов, Microsoft WinNAT 0 долларов
Суммарная оценка трёхлетнего исследования по программам вознаграждений

Google оказался щедрее: пятьсот долларов за GKE с оговоркой, что чинить надо стороннюю библиотеку, и шестьсот за Cloud NAT четвёртого августа, с понижением суммы за формулировку «трудно эксплуатировать на практике». Итого класс атак против всей отрасли, три года работы, пять техник, две CVE и мировая пресса оценены в тысячу сто долларов.

Для полноты картины ещё два эпизода из того же таймлайна. Шестого июля Microsoft закрыла один из отчётов по Azure Firewall как «не уязвимость», сославшись на невоспроизведение, и добавила, что даже воспроизведись оно – всё равно ниже порога обслуживания из-за смежного доступа и высокой сложности. Шестнадцатого июля она же сообщила, что межтенантная проблема в Azure Functions исправлена; проверка показала, что нет.

Кто чинил, а кто объяснял

Справедливости ради, отреагировали не все одинаково, и разница показательная.

AWS сделал работу. Развернул усиленную валидацию состояния соединений на NAT Gateway и Network Load Balancer, в том числе дополнительную проверку TCP-пакетов с флагом RST перед изменением состояния потока, и раскатал это по всем регионам. Более того, честно предупредил клиентов о побочке: удержание портов вырастет, и при больших объёмах долгоживущих соединений может понадобиться выделить дополнительные Elastic IP или расширить целевые группы. Отдельно признали технику раскрытия занятых внешних портов через реакцию на SYN- и ACK-флуд, но резонно заметили, что без возможности манипулировать сессией толку от этих сведений немного.

eero выкатил eeroOS 7.16.1 всему парку устройств ещё до публичного раскрытия, добавил строгий контроль состояния соединений и завёл отдельную настройку режима портов NAT: по умолчанию прямой, ради совместимости с играми, голосом и прочим P2P, но по желанию рандомизированный. И дал единственную по-настоящему трезвую формулировку из всех вендорских: девяносто пять процентов трафика шифровано, NatJack может порвать сессию, но не расшифровать её содержимое.

Остальные предпочли объяснить. Cisco: это не уязвимости, а «ограничения NAT на уровне дизайна», вот вам список митигаций, часть придётся включить руками. FreeBSD: отчёт из категории «ресурсы конечны». Google по точке доступа Android: угон TCP – «не уязвимость», потому что нас спасает TLS, а вот подмену DNS признаём средней тяжести – она проходит там, где приватный DNS выключен. Kubernetes: «PoC тщательный, техника корректная» – и следом закрытие как «информативно», потому что Calico и Flannel вне рамок программы. Docker: это баг Netfilter, которым пользуется наш bridge-драйвер, чинится обновлением ядра.

Тут стоит остановиться и сказать неудобное. Формально не соврал никто. Cisco права – это действительно уровень дизайна. Docker прав – код действительно в Netfilter. Kubernetes прав – CNI действительно чужой. Каждый безупречно прав в границах своей ответственности, а дыра лежит ровно между границами, и владельца у неё нет. Это не баг вендора. Это баг разграничения ответственности, и чинить его придётся не коммитом.

Правильное поведение лежало в OpenBSD с 2016 года

Финальный штрих, от которого хочется приложиться лбом о стол.

В пакетном фильтре pf есть корректная реакция на SYN, попадающий в уже существующее состояние: не сносить запись, а отправить челлендж-ACK и посмотреть, что будет.

static void
pf_send_challenge_ack(struct pf_pdesc *pd, struct pf_kstate *s,
    struct pf_state_peer *src, struct pf_state_peer *dst)
{
	/*
	 * We are sending challenge ACK as a response to SYN packet, which
	 * matches existing state (modulo TCP window check). Therefore packet
	 * must be sent on behalf of destination.
	 *
	 * We expect sender to remain either silent, or send RST packet
	 * so both, firewall and remote peer, can purge dead state from
	 * memory.
	 */
	pf_send_tcp(s->rule, pd->af, pd->dst, pd->src,
	    pd->hdr.tcp.th_dport, pd->hdr.tcp.th_sport, dst->seqlo,
	    src->seqlo, TH_ACK, 0, 0, s->rule->return_ttl, 0, 0, 0,
	    s->rule->rtableid);
}

Логика элементарная: подозрительный пакет не управляет состоянием напрямую, а обязан пройти проверку на знание реальных номеров последовательности. Идея не нова совсем – челлендж-ACK против слепых атак на TCP-соединения описан в RFC 5961 ещё в 2010 году.

В OpenBSD это поведение живёт с 2016-го. В FreeBSD оно приехало в мае 2025 года коммитом Kristof Provost, взятым напрямую из OpenBSD, и попало в релиз 15.0.0 – к исследованию Stagg этот патч отношения не имеет вообще, чистое совпадение, спонсированное Netgate. По оценке самого исследователя, эта заплатка делает большинство атак заметно менее практичными и бьёт и по downstream-, и по upstream-варианту.

Итог: рабочее решение существовало десять лет в системе, которую все хвалят и мало кто ставит, случайно расползлось в соседнюю BSD за год до раскрытия и так и не доехало до тех стеков, на которых крутится индустрия.

Об этом уже говорили два года назад, с цифрами

NatJack не свалился с неба. В 2024 году на NDSS вышла работа Yang и соавторов из Цинхуа, Университета Джорджа Мейсона и Юго-Восточного университета – про утечку номеров последовательности и угон TCP в Wi-Fi-сетях с NAT. Механика родственная: сохранение исходного порта плюс недостаточная проверка обратного пути плюс отключённое отслеживание TCP-окна, которое, цитируя авторов, «годами добросовестно реализовано в большинстве роутеров».

Цифры оттуда стоит перечитать медленно. Из 67 популярных роутеров тридцати вендоров уязвимы оказались 52. Из 93 обследованных реальных Wi-Fi-сетей полностью уязвимы 75, то есть 81 процент. Разрыв SSH-соединения занимал в среднем 17,5 секунды при успешности 87,4 процента, выкачивание приватных файлов с FTP – 19,4 секунды при 82,6 процента, инжект поддельного HTTP-ответа – 54,5 секунды при 76,1 процента. Работа принесла десять CVE, с CVE-2023-30305 по CVE-2023-30314.

Отрасли предъявили измеренную статистику и десяток идентификаторов два года назад. Ответ был ровно тот, который мы наблюдаем сейчас в таймлайне Stagg: точечные фиксы у роутерных вендоров и ноль изменений в модели угроз. Он пришёл с тем же тезисом уровнем выше – в контейнеры, гипервизоры и облака – и получил те же отписки, только от компаний покрупнее.

Что делать, пока патча для класса нет

Прикладная часть, без воды.

Шифрование – единственное, что убивает пользу от угона. TLS везде, включая внутренние сервисы, где «мы же в своей сети». DNSSEC против подмены ответов, DoH или DoT против их перехвата. Сессию тебе всё равно смогут порвать, а вот прочитать и подменить содержимое – уже нет.

Контейнеры. Забирай у недоверенного контейнера возможность слать сырые пакеты, она ему выдана по умолчанию и почти никогда не нужна:

docker run --cap-drop=NET_RAW --user 1000:1000 untrusted/image

Недоверенное – в приватную внутреннюю сеть или вообще на отдельный хост, и не под root.

Kubernetes. То же самое декларативно, плюс разнос по нодам: недоверенные поды не должны жить на одной ноде с доверенными, ни локально, ни в облаке.

securityContext:
  runAsNonRoot: true
  capabilities:
    drop: ["NET_RAW"]

Роутеры и файрволы. Отключить режимы, разрешающие завести соединение в таблицу без валидного рукопожатия:

sysctl -w net.netfilter.nf_conntrack_tcp_loose=0

В pf аналог – убрать flags any из правил и включить modulate state для рандомизации начальных номеров последовательности. Дальше по списку: IP Source Guard против спуфинга адресов клиентами, недоверенные устройства в отдельной подсети, явные L3/L4-списки доступа, ограничение числа соединений на клиента разумной величиной порядка десяти тысяч, отключение сохранения исходного порта и endpoint-independent mapping там, где это не сломает P2P.

Облако. Не сажать доверенные и недоверенные нагрузки за один NAT Gateway или интернет-шлюз. Для serverless брать выделенные адреса вместо общих – это буквально изоляция от соседа по тенанту.

Детект. Индикаторы у автора расписаны хорошо, и они реалистичные: таблица NAT под потолок; флуд от одного хоста с широченным диапазоном исходящих портов; один адрес, приходящий с двух разных физических адресов, или один физический с нескольких IP; последовательности SYN→RST и SYN→RST→SYN с постоянной парой портов; флуд RST с плавающим номером последовательности; номера, улетевшие далеко за пределы текущего окна.

И отдельно – самая изящная деталь во всём наборе: пониженный TTL. Атакующий подбирает время жизни так, чтобы пакет дошёл ровно до NAT-устройства и там сдох, не долетев до жертвы. Состояние в таблице обновится, а конечный хост ничего не заметит и не отправит в ответ RST, который выдал бы манипуляцию. Работа ведётся в слепой зоне обеих сторон соединения, и это единственное место, где NatJack выглядит не эксплуатацией дизайнерского допущения, а честной хакерской работой руками.

Что мы имеем в сухом остатке

NAT не был границей безопасности никогда. Это знает каждый, кто сдавал сети. Но знать и строить – разные глаголы, и последние двадцать лет отрасль строила: bridge-драйвер Docker, сетевые плагины Kubernetes, виртуальные свитчи гипервизоров, облачные шлюзы. Не потому что кто-то верил в NAT как в защиту, а потому что изоляция получалась бесплатно, из коробки и вроде бы работала.

Stagg всего лишь задал вопрос, который никто не задавал тридцать лет: а если сосед враждебный? Ответ – все протестированные устройства.

Дальше отрасль повела себя ровно так, как ведёт себя всегда, когда проблема не помещается в одну зону ответственности: каждый указал на соседа и остался формально прав. Ярче всех вышло у ядра – объявило отчёт полным бредом и передумало через месяц, когда попросил не исследователь, а платящий облачный клиент.

Тысяча сто долларов за три года работы – это не про скупость Microsoft и Google. Это точная рыночная цена уязвимости, у которой нет хозяина: заплатить за неё готов только тот, кому она лично мешает продавать сервис, и ровно в размере своего кусочка. Класс атак целиком не покупает никто, потому что он ничей.

Практический вывод для тех, кто это читает не ради драмы. Считай NAT тем, чем он является – механизмом экономии адресов, и ни в коем случае не периметром. Всё, что реально держит границу, ты ставишь сам: TLS на внутренних маршрутах, DoT на резолвере, --cap-drop=NET_RAW на всём, что пришло снаружи, недоверенные поды на отдельной ноде и списки доступа между зонами. Патча для допущения не будет – допущения не патчатся.

Исследование: SODIUM-24. Предшествующая работа: NDSS 2024.

Автор: hacker@shifry.local