Блог / Разборы CVE

CVE-2026-104935: Ivanti доверяет заголовку, не URL

CVE-2026-104935 даёт выполнение кода на Ivanti Connect Secure через заголовок и гонку загрузки. Для root нужен третий, пока не исправленный баг.

Аспирантура по информационной безопасности завершенаВнутривузовская защита пройдена · Итоговая защита диссертации впереди

Полностью пропатченный VPN-шлюз — и root всё равно

Вот и весь первый шаг: POST на публичную страницу входа Ivanti Connect Secure, welcome.cgi, которой не нужна сессия вообще, с одним лишним заголовком — X-DSCGI-PATH, указывающим на путь другого, обычно защищённого аутентификацией CGI-файла. Проверка аутентификации читает URL. Заголовок решает, куда запрос реально пойдёт. [1]

Volexity отслеживает эту группу под именем UTA0576, и к моменту публикации технического разбора, 13 октября 2026 года, группа уже использовала этот трюк с заголовком, сцепленный со вторым багом, чтобы получить выполнение кода от имени непривилегированного пользователя cgi-server — на шлюзе с полностью пропатченной сборкой 22.8R2.4. [1] Эти два бага вместе отслеживаются как CVE-2026-104935: CVSS 8.6, CWE-290 (обход аутентификации через подмену), затрагивает Ivanti Connect Secure 25.1.0.0 до 25.1.1.2, 25.1.2.0 до 25.1.3.0 и все сборки 22.8R2 до 22.8R2.4 включительно, исправлено в 25.1.1.2, 25.1.3.0, 25.1.3.1 и 22.8R2.5 13 октября 2026 года. [2] CISA добавила уязвимость в каталог известных эксплуатируемых уязвимостей в тот же день, с трёхдневным сроком для федеральных агентств, который уже прошёл. [2][3]

Выполнение кода от имени cgi-server — не конец истории. Volexity также восстановила третий этап, до сих пор без номера CVE и без патча, который ведёт отсюда к root — без backdoor и без веб-шелла. Я сам ничего из этого не запускал; дальше — разбор технического материала Volexity и записей CVE и KEV, прочитанных 18 октября 2026 года. [1][2][3] Важно не то, что VPN-шлюз взломали. Важно, что два из трёх багов — именно то, что ловит ревьюер кода, а не то, что обнаруживает специалист по реагированию на инциденты постфактум.

POST /dana-na/auth/url_default/welcome.cgi HTTP/1.1
Host: vpn.example.com
X-DSCGI-PATH: /home/webserver/htdocs/dana/fb/smb/wu.cgi
Content-Type: application/x-www-form-urlencoded

(welcome.cgi needs no session at all)

// Host and paths are illustrative, not turnkey.
// The URL says "public login page" -> the auth check reads the URL.
// The header says "run wu.cgi instead" -> routing reads the header.
// Nothing checks that the two agree.

Страница входа проверяет URL. Заголовок решает, какой скрипт запустится

Любой диспетчер вроде этого отвечает на два отдельных вопроса для каждого запроса: допущен ли этот вызывающий сюда, и какой файл реально его обработает. ICS отвечает на первый вопрос, проверяя путь URL — один ли это из публичных, неаутентифицированных эндпоинтов, — а на второй, проверяя маршрутизирующий заголовок X-DSCGI-PATH, который называет CGI-скрипт для вызова. [1] Разделить это на два вопроса — разумная инженерия. Само по себе такое решение не беспечно.

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

Обработчик загрузки доверяет не содержимому, а времени

Получить маршрутизацию на wu.cgi — первый шаг. Второй превращает эту маршрутизацию в выполнение кода, и это ошибка времени, а не содержимого. wu.cgi записывает загруженный файл в /home/runtime/tmp/<MD5 от выбранного вами имени файла>, и только после этого обработчик проверяет, действительно ли содержимое — валидный ZIP-архив. Если нет — файл удаляется. [1] Назовите Perl-скрипт с расширением .zip, и на короткое окно скрипт, который охотно выполнится, лежит по пути, который вы уже знаете, потому что имя файла выбрали вы, а предсказуемый хэш превратил его в этот путь.

Volexity описывает, как атакующий запускает гонку параллельных запросов именно за это окно: достаточно запросов, направленных через трюк с заголовком из первого этапа, на этот временный путь, чтобы успеть выполнить Perl-скрипт до того, как удаление выиграет гонку. Достаточно, чтобы в окно попал хотя бы один запрос. [1]

Нагрузка, которую восстановила Volexity, не остановилась на Perl. Она переходила в Python-скрипт, доставленный сжатым Base64 и zlib и выполненный в памяти через exec(), а не записанный отдельным файлом. [1] Цель на всех этапах одна: оставить на диске как можно меньше того, что можно найти потом.

// wu.cgi's order of operations, reconstructed from Volexity's writeup.
// This is the sequence, not a working exploit.

1. Attacker uploads "payload.zip" containing a Perl script, not a real ZIP.
2. wu.cgi writes it to /home/runtime/tmp/<MD5("payload.zip")> immediately.
3. wu.cgi then checks: is this actually a valid ZIP archive?
4. Check fails -> wu.cgi deletes the file.

// The gap between step 2 and step 4 is the whole bug.
// The attacker fires repeated requests (via the stage-one header trick)
// at the path from step 2, racing to execute it before step 4 wins.
// One request landing inside that window is enough.

Два этапа закрыты. Третий — нет, и он самый интересный

Исправление Ivanti для CVE-2026-104935, выпущенное 13 октября 2026 года, закрывает первый и второй этапы: 25.1.1.2, 25.1.3.0, 25.1.3.1 и 22.8R2.5 — версии, на которых именно эта цепочка перестаёт работать. [2] Если у вас более ранняя сборка на любой из этих трёх линий, это не пункт «запланируем на следующий спринт». Собственные данные записи CVE по эксплуатации уже читаются как «активная», с «полным» техническим воздействием, а трёхдневный федеральный срок CISA уже прошёл. [2][3]

На третьем этапе описания патча заканчиваются. Получив выполнение кода от имени непривилегированного пользователя cgi-server, Volexity установила, что UTA0576 переназначил dsinvoked, собственный процесс обслуживания ICS с правами root, на выполнение выбранного атакующим кода от root — переназначив цель существующей задачи целиком в памяти уже работающего процесса. [1] Я не буду разбирать конкретные детали библиотеки или конфигурации, которые назвала Volexity. Механизм публичен, патча для него нет, и повторять детали уровня эксплойта для открытой эскалации — не то, что я делаю здесь, независимо от того, кто уже это опубликовал.

Что важно независимо от статуса патча: эскалация откатывает себя сама. Она восстанавливает запись конфигурации и байты библиотеки в памяти, подтвердив root через проверку доступности /etc/shadow, так что устройство после этого выглядит нетронутым. Volexity прямо утверждает, что собственный Integrity Checker Tool от Ivanti этого не ловит, потому что ICT проверяет то, что на диске, а здесь диск вообще не затрагивается. [1] Чистый прогон ICT сегодня ничего не говорит о том, затронул ли устройство третий этап — ни до 13 октября, ни сейчас, ведь патча для него до сих пор нет.

Как я искал бы это в коде, не имеющем отношения к Ivanti

Первый и второй этапы — не ошибки, специфичные для ICS. Это два паттерна, которые я проверяю в любом реверс-прокси, API-шлюзе или CGI-подобном диспетчере, на любом стеке. Первый: читает ли компонент, решающий, допущены ли вы, то же поле, что и компонент, решающий, какой обработчик запустится? Если авторизация опирается на URL, а маршрутизация — на заголовок, куки или параметр запроса, который слой авторизации никогда не смотрит, этот баг есть и здесь, просто с другими именами полей.

Второй: существует ли то, что ваш сервис пишет по доступному пути, хотя бы кратко, до того как это проверено? Обработчики загрузки — классический дом для этого, но встречается везде, где сервис сначала сохраняет, а потом проверяет: запись в кэш, временный файл, запись в очередь, которую воркер может взять раньше, чем отработает валидатор. Два вопроса ниже — те, что я реально задал бы, читая такой обработчик, как wu.cgi, в первый раз, и для их применения исходники Ivanti не нужны.

// Two questions I ask reading any dispatcher or upload handler,
// independent of what language or framework it's written in.

// 1. Auth-vs-routing mismatch
//    - What field decides "is this caller allowed in"? (often the URL)
//    - What field decides "which handler actually runs"? (sometimes a
//      header, a cookie, a query parameter)
//    - If those are two different fields, does anything enforce that
//      they agree?

// 2. Persist-before-validate
//    - Where does a handler write attacker-influenced content to a
//      referenceable path (disk, cache, queue) before fully validating it?
//    - Is there a window, however small, where something worse can
//      reach what was just written before cleanup or validation runs?

Это ваша авария? Если у вас Connect Secure — да

Честно о том, кого это касается. Если у вас нет Ivanti Connect Secure, идите дальше — это не про вас. Если есть, мягкой посадки здесь нет, как иногда бывает с малозначимой находкой, раздутой до срочности. CVE-2026-104935 — доаутентификационная, собственные данные об эксплуатации уже читаются как «активные», а по криминалистической реконструкции Volexity это ударило по шлюзу со всеми доступными патчами уже установленными. [1][2] «Мы пропатчены» было правдой для того клиента, и это не спасло.

Три вещи задают уровень тревоги. Первое — сборка, реально работающая на устройстве прямо сейчас, а не та, до которой вы собирались дойти по журналу изменений: 25.1.1.1 и ранее, 25.1.2.2 и ранее, 22.8R2.4 и ранее — всё это ещё открыто для первого и второго этапов. [2] Второе — является ли патч CVE-2026-104935 концом вашей оценки риска или её началом. Он закрывает дверь, через которую прошёл UTA0576, а не ту, что Volexity нашла уже использованной внутри. Третье — не выдаёт ли чистый результат Integrity Checker Tool за доказательство отсутствия компрометации, хотя собственный материал Volexity прямо говорит, что для этой цепочки это не так. [1]

Оценка 8.6 честна в отношении первого и второго этапов. Она ничего не говорит о третьем, потому что у третьего пока нет оценки — а атакующий, сцепляющий все три, не перестаёт беспокоиться об этом только потому, что перестал CVSS.

Что проверить, прежде чем закрыть инцидент

Сначала патч, очевидно. Но патч постфактум отвечает на вопрос «безопасны ли мы теперь», а не «ударило ли это по нам уже», а окно эксплуатации этой цепочки не требовало от вас быть непропатченными хоть в чём-то заметном на тот момент. [1]

Volexity прослеживает инфраструктуру атакующего до 30 сентября 2026 года, а эксплуатацию — с 3 октября. [1] Если шлюз смотрел в интернет на уязвимой сборке в любой момент начиная с конца сентября 2026 года, считайте, что компрометация возможна, и ищите следы, прежде чем закрывать инцидент. Список ниже — порядок, в котором я бы работал.

Before you call this closed:
  [ ] Confirm the build actually RUNNING on the appliance, not the one
      your change log says. 25.1.1.2 / 25.1.3.0 / 25.1.3.1 / 22.8R2.5 or
      later closes CVE-2026-104935 (stages one and two only).
  [ ] Pull logs for requests to welcome.cgi where X-DSCGI-PATH does not
      match the URL path. That mismatch is this chain's fingerprint.
  [ ] Do not close the incident on a clean Integrity Checker Tool run
      alone. ICT checks on-disk state; stage three never touches disk.
  [ ] If the appliance was internet-facing on a vulnerable build any time
      from late September 2026 on, treat it as possibly compromised, not
      just unpatched.
  [ ] Track Ivanti's advisory for the still-missing privilege-escalation
      fix and apply it the day it ships. There is no CVE for it yet.

CVE — это самая простая часть

CVE-2026-104935 сама скоро выйдет из новостного цикла. Два класса багов за ней — нет, потому что они никогда не были особенностью только Ivanti. Проверка авторизации и решение о маршрутизации, незаметно доверяющие разным полям одного запроса, и путь записи, сохраняющий данные раньше, чем проверяет их, — я нахожу и то, и другое в веб- и API-коде, никогда не касавшемся VPN-шлюза, обычно в местах, на которые никто не посмотрел второй раз, потому что очевидная проверка — аутентифицирован ли эндпоинт, проверен ли тип загрузки — уже прошла.

Это и есть граница, которую трассирует аудит кода на безопасность: не дошёл ли запрос до функции, а читает ли каждый шлюз на пути одни и те же факты об этом запросе. Если хотите, чтобы кто-то прочитал ваш слой авторизации и маршрутизации с этим вопросом в голове, прежде чем он станет чьим-то разбором нового zero-day, это именно та работа, которую я делаю. Напишите, что вы запускаете, и мы определим объём.

Источники

Нужна такая проверка для вашего продукта?

Аудит безопасности кода
Все статьи

08Контакт

Выпускаете продукт с данными пользователей?

Пришлите короткое описание. Я отвечу в течение трёх дней вопросами или наброском предложения, а стоимость согласуем письменно до начала работ.