Ваш firewall пропускает атаку по правилам

от автора

В первой статье я говорил, что безопасность — это свойство архитектуры, а во второй разложил по полочкам способы построить logical air gap. Теперь самое главное: «А зачем вообще это всё? У нас есть firewall, WAF, TLS и патч-менеджмент».

Короткий ответ

Все традиционные средства — firewall, WAF (Web Application Firewall), IPS (Intrusion Prevention System) — исходят из того, что сквозной путь от недоверенной зоны к доверенной существует, и ставят на этом пути фильтр. Само существование этого пути имеет три конкретных последствия:

  1. Байты атакующего доходят до кода доверенного сегмента. Их разбирают прошивка NIC, драйверы, сетевой стек ядра, TLS-библиотека — и всё это до того, как приложение проверит хоть один токен. Любая ошибка памяти в этой цепочке эксплуатируется удалённо и без аутентификации.

  2. Безопасность равна корректности каждого элемента на пути. Ошибка в фильтре, уязвимость в самом фильтре или одна лишняя строка в правиле — и путь открыт целиком. Это цепочка условий, которая со временем только удлиняется.

  3. Любой скомпрометированный узел на пути уже имеет доступ внутрь. Ему не нужно пробивать периметр — он его часть.

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

Периметр: два источника уязвимостей, и оба — не ошибки конфигурации

Типовая схема публикации внутреннего API:

Здесь два источника уязвимостей, и ни один из них не является ошибкой конфигурации — оба следуют из самой архитектуры:

  1. Достижимость доверенного кода. Пакет клиента доезжает до сетевого интерфейса внутреннего сервиса. Значит, вся цепочка разбора на этой машине — от прошивки карты до парсера прикладного протокола — становится pre-auth-поверхностью атаки, доступной снаружи.

  2. Слушающий порт как готовая точка входа. Внутренний сервис принимает входящие соединения из DMZ. Скомпрометированному узлу DMZ не нужно искать вход — он открыт и разрешён правилом, которое нельзя убрать, потому что оно нужно приложению.

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

Настоящая атакуемая поверхность: всё, что разбирает пакет до приложения

Когда мы говорим «входящий запрос», мы представляем обработчик в приложении: он проверит токен, валидирует тело, отклонит лишнее. Но до первой строки прикладного кода пакет проходит длинную цепочку программ, и каждая из них разбирает недоверенные байты до любой аутентификации.

Голое железо — минимальный случай. Прошивка сетевой карты (разбор кадров, offload вроде TSO/LRO и контрольных сумм), драйвер в ядре с DMA и кольцевыми буферами, сетевой стек ОС — ARP, IP с фрагментацией и сборкой, ICMP, конечный автомат TCP с опциями и расширениями, — затем conntrack и netfilter, TLS-библиотека и парсер прикладного протокола: HTTP/1.1, у HTTP/2 — мультиплексор кадров и сжатие заголовков HPACK, у gRPC — ещё и десериализация protobuf до перехватчиков авторизации, у WebSocket — разбор фреймов и маскирования, у HTTP/3 — весь транспорт QUIC, переехавший в user space. Сотни тысяч строк преимущественно C-кода, и все они трогают байты атакующего раньше, чем приложение узнаёт о существовании запроса.

Виртуализация удлиняет цепочку. Пакеты обрабатывают драйвер, стек и виртуальный коммутатор хоста, затем виртуальное устройство гипервизора — virtio-net, vmxnet3 или эмуляция e1000, — и только потом начинается путь по гостевой ОС: снова драйвер, снова стек. Каждый слой — отдельная, независимо написанная реализация разбора одних и тех же байтов.

Худший и при этом самый типовой случай — Docker на виртуалке:

Закрытый код больше не сокращает эту поверхность. Возражение «у нас проприетарный стек, исходников нет, искать там уязвимости слишком дорого» держалось на одной экономике: ручной реверс-инжиниринг бинарника — это месяцы работы редкого специалиста. Именно эту цену и убирают инструменты автоматического и AI-анализа: языковая модель поверх декомпилятора восстанавливает из машинного кода структуры, форматы и логику разбора, а дальше связка «символьное исполнение плюс фаззинг с направляющей моделью» ищет ошибку без единой строки исходников. Закрытость меняет не наличие уязвимости, а только то, кто найдёт её первым и станет ли находка публичной.

Важно: закрытые исходники больше не защитная мера. Уязвимости находят машины, и уже не в лабораторных условиях:

  • ноябрь 2024 — агент Big Sleep (Google Project Zero и DeepMind) самостоятельно нашёл ранее неизвестный stack buffer underflow в SQLite; Google описал это как первый публичный случай, когда ИИ-агент обнаружил ранее неизвестную уязвимость памяти в широко используемом ПО. В июле 2025 тот же агент нашёл в SQLite CVE-2025-6965 — повреждение памяти, о котором, по оценке Google, на тот момент знали только атакующие;

  • октябрь 2024 — OSS-Fuzz с фаззинг-таргетами, сгенерированными языковой моделью, нашёл в OpenSSL CVE-2024-9143: выход за границы буфера в низкоуровневом API эллиптических кривых GF(2^m), пролежавший в открытом и годами аудируемом коде около двух десятилетий;

  • август 2025 — в финале DARPA AI Cyber Challenge автономные системы нашли около трёх четвертей специально внедрённых уязвимостей и вдобавок 18 ранее неизвестных в реальных open-source-проектах, причём сами же сгенерировали к ним патчи;

  • бинарники — не исключение: ещё на DARPA Cyber Grand Challenge в 2016 году машины находили и эксплуатировали уязвимости в бинарниках без исходников в реальном времени, на скорости соревнования. Сегодня к бинарному фаззингу и символьному исполнению добавились языковые модели, работающие с декомпилированным кодом.

Для схемы выше это значит, что закрытые слои — прошивка NIC, management-движок, виртуальное устройство гипервизора — не «безопаснее по умолчанию», а всего лишь хуже освещены: там меньше публичных CVE, а не меньше ошибок. Broadpwn в прошивке Broadcom (CVE-2017-9417) и обход аутентификации в Intel AMT (CVE-2017-5689) добыли ручным реверсом закрытых бинарников ещё до всякого AI — просто это стоило месяцев работы редкого специалиста. Теперь дешевле и дешевеет дальше, а значит, ставка «нас не будут ковырять, там же нет исходников» перестала быть ставкой на что-либо. Не зависит от стоимости анализа только одно: если байты до слоя не доходят, разбирать его нечем и незачем.

Лаг обновлений — вторая половина проблемы. Против 0-day патч-менеджмент не работает по определению, но и после выхода патча гонка не заканчивается:

  • цепочку в Ivanti Connect Secure эксплуатировали с начала декабря 2023-го, патчи начали выходить в конце января 2024-го — окно в недели;

  • патч ядра или гипервизора — это перезагрузка или миграция ВМ, а окно обслуживания в 24/7-инфраструктуре планируют неделями;

  • прошивки сетевых карт и BMC на практике не обновляются годами;

  • ESXiArgs в феврале 2023-го массово шифровал серверы через CVE-2021-21974 — патч к ней вышел двумя годами ранее.

Что здесь меняет разрыв. У всей цепочки — от прошивки NIC до парсера прикладного протокола — одно общее условие эксплуатации: атакующий должен доставить свой пакет к машине. В контуре с разрывом у машин доверенного сегмента такой достижимости нет: маршрута из недоверенной зоны к ним не существует, исходящее соединение к шлюзу открывает сам исполнитель. Цепочка разбора перестаёт быть pre-auth-поверхностью — не потому, что уязвимости кончились, а потому, что байты до них не долетают. Гонка «эксплойт против патча» остаётся только на шлюзе — узле, спроектированном как расходный: минимальная поставка, нет секретов, нет маршрута внутрь.

Да, конечно, остаётся ещё клиентский код исполнителя, и там тоже могут быть уязвимости — об этом ниже, в разделе про остаточный риск.

Модель угроз

Что защищаем. Доверенный сегмент: внутренние API, базы данных, технологические сети АСУ ТП, системы с персональными данными или гостайной — всё, компрометация чего стоит несоизмеримо дороже недоступности.

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

Противник

Возможности

Цель

Внешний атакующий

Сканирование, эксплуатация периметра, 0-day в сетевом стеке и гипервизоре, переиспользование одного эксплойта на однотипных узлах пути

Первичный доступ и продвижение хоп за хопом

Закрепившийся в DMZ

Полный контроль над узлом DMZ, произвольный трафик и украденные креды из него

Продвижение внутрь

Внутри доверенного сегмента

Контроль над внутренним узлом

Канал C2, эксфильтрация

Цепочка поставок

Закладка в периметровом ПО или устройстве

Скрытый доступ через «доверенный» компонент

Допущения. Сетевые стеки, гипервизоры и контейнерная сеть содержат неизвестные уязвимости — это статистика, а не пессимизм. Конфигурации содержат ошибки. Сигнатурные средства не детектируют то, чего ещё нет в сигнатурах.

Если этих допущений в вашей модели нет — например, вы готовы принять риск 0-day в сетевом стеке доверенной машины — разрыв вам, скорее всего, не нужен. И это нормальный ответ, а не ересь.

Три вектора, которые закрывает только разрыв

1. RCE в сетевом стеке ОС — до всякой аутентификации

Как выполняется. Атакующий отправляет серию специально сформированных пакетов на IP-адрес машины. Пример — CVE-2024-38063 в tcpip.sys: ошибка обработки IPv6-фрагментов срабатывает в коде ядра, разбирающем заголовки. Ни рукопожатия, ни аутентификации, ни даже открытого порта не требуется — обработчик в ядре читает заголовки раньше, чем решается, кому отдать пакет. Того же класса SACK Panic и Bad Neighbor (CVE-2020-16898).

Что даёт. Исполнение кода в контексте ядра, то есть полный контроль над машиной доверенного сегмента, минуя все прикладные проверки. Логи приложения при этом пусты: приложение запроса не видело.

Почему периметр не спасает. Firewall обязан пропускать трафик к опубликованному сервису — пакет доходит до интерфейса, а этого достаточно. WAF разбирает L7 и до L3-эксплойта не доходит. Host-based фильтр в этой цепочке — ещё один потребитель тех же байтов, стоящий над уязвимым кодом.

Как защищает разрыв. Условие эксплуатации — доставить пакет к интерфейсу машины. В контуре с разрывом машины доверенного сегмента нет ни в одной таблице маршрутизации со стороны недоверенной зоны: отдельная адресация, отсутствие маршрута и трансляции, интерфейс не смотрит наружу. Единственный сетевой обмен — исходящая TCP-сессия исполнителя к шлюзу; ответные пакеты принимаются только в рамках уже установленной сессии (conntrack ESTABLISHED), инициированной изнутри. Эксплойт остаётся рабочим, но недоставляемым: у атакующего нет ни одного пути отправить байты в стек ядра доверенной машины.

2. Разведка и lateral movement по разрешённому правилу

Как выполняется. Атакующий на узле DMZ пользуется правилом, которое там есть по определению: DMZ → internal-api:443. Через него он сканирует внутренний диапазон, снимает баннеры и версии, эксплуатирует внутренние сервисы (патчатся реже периметра) или переиспользует украденный у прокси токен и клиентский сертификат mTLS. Для файрвола весь этот трафик легитимен — он соответствует разрешающему правилу.

Что даёт. Точку входа в доверенный сегмент и карту целей внутри него.

Почему периметр не спасает. Правило нужно приложению, поэтому его нельзя убрать — можно только сузить. Микросегментация уменьшает список целей, но разрешённые ей потоки остаются сквозными путями. IDS замечает аномалию постфактум и не всегда.

Как защищает разрыв. Pull-модель: в доверенном сегменте нет ни одного слушающего порта, доступного из недоверенной зоны, а inbound-политика — deny all без исключения «для приложения». Исполнитель сам держит исходящий long-poll к шлюзу и получает задачи в ответах этой сессии. Для узла DMZ это означает: SYN в сторону сегмента не имеет маршрута, сканирование не возвращает ни одного открытого порта, украденный токен бесполезен — им нельзя инициировать соединение туда, куда нет пути. Класс атак «нашёл порт → определил версию → проэксплуатировал» не затрудняется, а исчезает: у него нет первого шага.

3. Цепочка hop-by-hop: одна уязвимость доводит до доверенного сегмента

Как выполняется. У атакующего есть 0-day в компоненте, который стоит на каждой машине контура: сетевой стек ядра, TLS-библиотека, реализация HTTP/2. Это типично — узлы разворачивают из одного образа, с одной сборкой ядра и одним base image. Дальше он идёт по цепочке достижимости, и каждый шаг разрешён потому, что предыдущий узел обязан общаться с последующим:

  1. эксплойт против внешнего балансировщика — получен код на узле, смотрящем в интернет;

  2. с него тот же эксплойт против прокси/WAF в DMZ — узел достижим, балансировщик обязан к нему обращаться;

  3. с прокси тот же эксплойт против внутреннего сервиса — правило DMZ → internal-api:443 существует по построению.

Что даёт. Три хопа, один эксплойт, ни одного украденного пароля — и RCE на машине доверенного сегмента. Ни на одном шаге не понадобилось обходить аутентификацию: уязвимый код лежит ниже неё.

Почему периметр не спасает. Каждый рубеж — это ещё одна машина с тем же уязвимым слоем, то есть ещё один хоп, а не барьер. Разные вендоры помогают только если уязвимость не в общем слое; ядро, libc и TLS общие почти всегда. Правила не мешают: атакующий использует ровно те потоки, которые разрешены для работы приложения.

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

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

Остаточный риск: клиентский код исполнителя

Ноля здесь нет, и делать вид, что он есть, — маркетинг, от которого я и пытаюсь отгородиться. Захватив шлюз, атакующий контролирует ответы, которые получает исполнитель, и может атаковать его клиентскую часть — TLS-клиент, HTTP-клиент, десериализатор JSON или protobuf. Но риск не тот же самый, и разница измеримая:

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

  • Поверхность у клиента меньше. Сервер разбирает запрос произвольной формы от произвольного отправителя: слушающий сокет и accept, конкурентные соединения, маршрутизацию, состояние сессий, таблицы HPACK и стримы, апгрейды протокола. Отсюда и класс серверных ошибок — request smuggling, CONTINUATION flood, десинхронизация. Клиент разбирает ответ на собственный запрос: один известный эндпоинт, ожидаемый content-type, лимиты длины и таймаута, схема, по которой ответ можно провалидировать до использования.

  • Серверный код исследуют несравнимо активнее. Его может фаззить любой в интернете, поэтому туда направлены сканеры, bug bounty и массовая эксплуатация, и подавляющая часть pre-auth RCE — про серверные компоненты. Клиентскую часть исполнителя в этой позиции способен атаковать только тот, кто уже захватил шлюз, — а значит, и цена, и заметность атаки другие.

Поэтому pull не убирает риск, а меняет его класс: вместо pre-auth RCE, доступного любому в интернете и продолжаемого хоп за хопом, остаётся post-compromise атака на узкий клиентский парсер, доступная только тому, кто уже потратил 0-day на расходный шлюз. Снижается она обычными средствами: минимальный клиент, жёсткие лимиты на размер и время ответа, строгая валидация ответа по схеме.

От чего разрыв не защищает

Список короткий, но каждый пункт в нём регулярно всплывает в спорах как «убийственный аргумент против» — хотя на деле это просто границы применимости:

  • Атаки через валидный запрос. Сам по себе разрыв контролирует форму обмена, а не смысл содержимого: SQL-инъекция, укладывающаяся в схему моста, будет доставлена и выполнена. Если в точке разрыва вся полезная нагрузка разобрана в структуру и её можно проверить до попадания внутрь, если в этой точке реализован строгий форматно-логический контроль (ФЛК) — allow-list операций и полей, типы, длины, диапазоны, регулярные выражения и перечисления значений, отказ по умолчанию для всего неописанного, — то запросы с инъекциями, обходом путей и неожиданными полями не проходят границу вообще. Но ФЛК не спасает от нагрузки, валидной по схеме и опасной по смыслу (например, легитимного по формату идентификатора чужого объекта): это уровень авторизации в приложении.

  • Компрометацию доверенного сегмента изнутри. Инсайдер, флешка, закладка в зависимости — разрыв сузит каналы наружу, но не предотвратит компрометацию.

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

  • Уязвимости посредника и средств защиты. Шлюз в недоверенной зоне — такая же программа, разбирающая недоверенный ввод, как firewall или WAF, и у него есть свой интерфейс управления. Разрыв не делает его неуязвимым; он лишь ограничивает, что даёт его захват.

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

  • Социальную инженерию и всё, что не проходит через сетевую границу.

Когда разрыв оправдан, а когда избыточен

Признак контура

Разрыв оправдан

Разрыв избыточен

Цена компрометации

КИИ, АСУ ТП, гостайна, финансовое ядро, ПДн

обычное веб-приложение, потери восполнимы

Модель угроз

0-day, закрепление в DMZ

массовые автоматические атаки

Главный риск

достижимость сегмента извне

уязвимости самого приложения

Комплаенс

требуется отсутствие прямой связности сегментов

достаточно стандартной сегментации

Характер обмена

конечный набор операций, описываемых схемой

произвольный трафик, WebSocket, стриминг

Требования к задержке

десятки миллисекунд допустимы

критична минимальная задержка

Приоритет

изоляция важнее доступности

доступность важнее изоляции

Практический маркер в пользу разрыва — регуляторное требование об отсутствии прямой связности: разрыв закрывает его архитектурно, а не компенсирующими мерами. Против — если основной риск лежит в коде приложения: тогда бюджет разумнее вложить в безопасную разработку, а разрыв только добавит задержку и эксплуатационную сложность (её я измерял в исследовании MVP — про миллисекунды и RPS будет отдельная статья).

Итог

  1. Атакуемая поверхность опубликованного сервиса — не его код, а девять слоёв разбора недоверенных байтов под ним: прошивка NIC, драйверы, стек ОС, гипервизор, контейнерная сеть, TLS, парсер прикладного протокола (HTTP/1.1, HTTP/2, gRPC, WebSocket, QUIC). Все они pre-auth, во всех регулярно находят RCE, патчи доходят с лагом от недель до лет.

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

  3. Поэтому разрыв закрывает то, до чего фильтры структурно не дотягиваются: 0-day в сетевом стеке и гипервизоре, разведку, lateral movement из DMZ. Главное следствие — цепочка «хоп за хопом одним эксплойтом» обрывается на шлюзе: дальше нет ни маршрута, ни слушающего порта. Остаточный риск смещается в клиентский код исполнителя — поверхность меньше, инициатива не у атакующего, и добраться до неё можно только после захвата шлюза.

  4. Всё остальное — WAF, патчи, сегментация, безопасная разработка — по-прежнему нужно. Разрыв не заменяет эти меры, а закрывает тот класс атак, до которого они не дотягиваются.

Как этот паттерн реализуется и во что обходится — в карте вариантов.

А теперь интересен ваш опыт: у кого модель угроз честно включает 0-day в сетевом стеке — и что вы с этим делаете?

ссылка на оригинал статьи https://habr.com/ru/articles/1067520/