Инструмент поиска аномалий не заметил, как сдал устройство хакерам.
В маршрутизаторах Juniper PTX на базе Junos OS Evolved раскрыли критическую уязвимость CVE-2026-21902 , которая позволяет удалённо выполнять код с правами root без аутентификации. Juniper описывает проблему как неправильное назначение прав доступа к критическому ресурсу в механизме On-Box Anomaly Detection Framework. По замыслу этот сервис должен быть доступен только внутренним процессам через внутренний маршрутизирующий экземпляр, а не через внешний сетевой порт. Если до службы всё же можно достучаться по сети, атакующий получает возможность управлять сервисом и в итоге полностью захватить устройство. Компонент включён по умолчанию и не требует отдельной настройки.
Уязвимость затрагивает только устройства серии PTX. В бюллетене Juniper говорится, что под удар попали выпуски Junos OS Evolved 25.4 до версий 25.4R1-S1-EVO и 25.4R2-EVO, тогда как сборки до 25.4R1-EVO уязвимыми не считаются. Сама серия PTX применяется в магистральных сетях операторов связи, на узлах пиринга и в крупных межцентровых соединениях. Такие маршрутизаторы проектируют под очень высокую пропускную способность, низкие задержки и большую плотность портов. Классический Junos OS исторически строился на FreeBSD, а Junos OS Evolved Juniper перевела на Linux и более модульную, контейнеризированную архитектуру.
Проверка показала, что внутри системы действительно работает сетевой сервис, связанный с On-Box Anomaly Detection Framework. При просмотре сокетов исследователи увидели следующую картину:
Служба аномалий слушает порт 8160/TCP и, судя по выводу, привязана к 0.0.0.0, то есть ко всем IPv4-интерфейсам. Подозрения усиливает и код инициализации HTTP-сервера, найденный в системе:
Внутренняя модель сервиса строится вокруг четырёх сущностей. Первая - Command, то есть команда, которая будет выполнена на устройстве. Вторая - Handler, обработчик, разбирающий вывод команды. Третья - DAG, ориентированный ациклический граф, который описывает последовательность действий: команд, обработчиков или вложенных графов. Четвёртая - DAG Instance, конкретный экземпляр графа, привязанный к расписанию. Уже из этой схемы видно, что система сама по себе умеет запускать команды на маршрутизаторе. Вопрос только в том, можно ли управлять этой функцией извне. Ответ, судя по опубликованному разбору, оказался положительным.
Все основные файлы лежат прямо в файловой системе устройства, в каталоге /usr/sbin/monitor/. Исследователи выделили четыре ключевых компонента:
python3.10 /usr/sbin/monitor/ anomaly_detector_main.py - The initial Python script that ensures the sub Python scripts stay alive. python3.10 /usr/sbin/monitor/ api_server.py - The HTTP API server which stores request data in files on the server. python3.10 /usr/sbin/monitor/ intent_monitor.py - Periodically checks for updates to definitions and updates API server definitions. python3.10 /usr/sbin/monitor/ schedule_enforcer.py - Executes scheduled DAG instances periodically.
anomaly_detector_main.py следит, чтобы остальные процессы не падали. api_server.py обслуживает HTTP-запросы и сохраняет полученные данные в файлах на устройстве. intent_monitor.py периодически отслеживает обновления определений и синхронизирует их с сервером API. schedule_enforcer.py отвечает за периодический запуск экземпляров DAG по расписанию.
Набор HTTP-методов и конечных точек у сервиса выглядит как вполне обычный интерфейс управления конфигурацией. В опубликованном разборе приведён такой список:
MethodPathDescription GET/anomalyRetrieves all registered Anomalies. GET/config/schedule/ Get new DAG INSTANCEs to execute on the component. GET / POST / PUT / DELETE/config/dag/Retrieves, creates, updates ,or deletes a DAG configuration. GET / POST / PUT / DELETE/config/command/ Retrieves, creates, updates ,or deletes a COMMAND configuration. GET / POST / PUT / DELETE/config/handler/ Retrieves, creates, updates ,or deletes a HANDLER configuration. GET / POST / PUT / DELETE/config/dag-instance/ Retrieves, creates, updates ,or deletes a DAG INSTANCE configuration. GET / POST/config/commitValidates the union of the Workspace config and the Existing Config. Saves the Workspace Config if it is valid on POST. GET / POST/output/dag-instance/ /iteration/ /component/reRetrieves or stores the output of a specific DAG INSTANCE run for an ITERATION on the RE. GET / POST / DELETE/alarm/dag-instance/ / component/reGets, stores or deletes alarms raised by the DAG INSTANCE run on the RE. POST/anomaly/dag-instance/ /iteration/ /component/reRegisters anomalies raised by the DAG INSTANCE run on an RE.
Критически важная часть находится в конфигурации команды. Сервис позволяет создать объект command, а в поле syntax передать строку, которую система позже запустит. Для удалённого выполнения кода в опубликованном примере используется простая команда id > /var/home/admin/watchTowr.txt. Тип RE-SHELL подсказывает сервису, что строку нужно выполнить как обычную команду оболочки на самом устройстве.
Пример HTTP-запроса для создания такой команды в оригинале выглядит так:
После создания команды атакующему нужен DAG, который определит порядок выполнения действий. В простейшем случае граф состоит из одного действия и просто ссылается на ранее созданную команду. Обработчики, дополнительные входные параметры и переходы между узлами не нужны. В разборе приведён следующий запрос:
Дальше создаётся экземпляр DAG, который говорит сервису, когда именно нужно выполнить граф. В опубликованной цепочке запуск назначают немедленно, без задержки. Для этого используется такой запрос:
На последнем шаге клиент отправляет запрос фиксации конфигурации. После этого прежние объекты сохраняются в файле, который затем обрабатывает планировщик schedule_enforcer:
POST /config/config/commit HTTP/1.1 Host: Content-Type: application/json Content-Length: 0
Дальше в дело вступает внутренняя логика сервиса. Главная функция получает расписание, заданное для экземпляра DAG. После проверки времени она вызывает execute_dag_instance. Затем запускается execute_dag, далее - run_bfs_on_dag_actions, а уже там вызывается execute_command. Именно в этой функции из описания команды извлекается поле syntax, и его содержимое без фильтрации передаётся в subprocess.run(...). В опубликованном разборе цепочка показана прямо по исходному коду:
def main(): # [1]...schedule = api_client.get_config_schedule(component_name=f'{COMPONENT}{FPC_SLOT}')...thread = threading.Thread(target=execute_dag_instance, args=(...)) # [2]... def execute_dag_instance(api_client, ...): # [2]...dag_executor = Executor(...)dag_executor.execute_dag() # [3]... class Executor:def execute_dag(self): # [3] self.run_bfs_on_dag_actions(...) # [4] def run_bfs_on_dag_actions(self, ...): # [4] ... if 'command' in dag_def['actions'][current_node]: action_outputs = self.execute_command(command_id=current_node, ...) # [5] ... ## COMMAND Execution Function#def execute_command(self, command_id, ...): # [5] command_name = dag_def['actions'][command_id]['command'] ... # # Build Command by substituting in Inputs # syntax = command_def['syntax'] # [6] ... if self.target['type'] == 'RE': # # If the DAG INSTANCE is executing on the RE, # and if the command type is an RE CLI command, # we need to run the command on the RE # if command_def['type'] == 'RE':component_command_mapping['re'] = f'cli -c "{syntax}"' elif command_def['type'] == 'RE-SHELL':component_command_mapping['re'] = syntax raw_output_mapping = dict() for component_name, command in component_command_mapping.items(): try:completed_subprocess = subprocess.run( # [7] command, shell=True, check=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE)if completed_subprocess.returncode != 0: raw_output = completed_subprocess.stderr.decode('utf-8')else: raw_output = completed_subprocess.stdout.decode('utf-8')raw_output_mapping[component_name] = raw_output except subprocess.CalledProcessError as e:logging.error(f'Error executing command - ...')
Здесь важны сразу несколько деталей. Во-первых, логика сама строит строку команды из конфигурации DAG. Во-вторых, для типа RE-SHELL никакого дополнительного обрамления не происходит: строка из syntax просто попадает в component_command_mapping['re']. В-третьих, вызов subprocess.run использует параметр shell=True, а значит, строка обрабатывается оболочкой напрямую. Для атакующего это уже готовое удалённое выполнение кода , причём с правами root, потому что сам сервис работает от имени суперпользователя.
Вся цепочка в итоге выглядит почти слишком прямолинейно для критической уязвимости такого уровня. Удалённый пользователь без логина и пароля отправляет запрос на создание команды, затем описывает DAG, регистрирует экземпляр DAG, фиксирует конфигурацию и ждёт, пока планировщик выполнит задание. После этого команда срабатывает на маршрутизаторе. Никакой отдельной уязвимости для обхода аутентификации здесь не требуется, потому что входной интерфейс и так оказывается доступен извне.
Именно поэтому CVE-2026-21902 получила почти максимальную оценку по шкале CVSS. При успешной эксплуатации атакующий получает полный контроль над магистральным маршрутизатором, который может стоять в критическом сегменте сети оператора, интернет-обменника или гипермасштабной инфраструктуры. Для таких устройств компрометация означает уже не локальный сбой одного сервиса, а риск полного контроля над сетевым узлом, через который проходит большой объём трафика.