Back to Blog
Juniper's Anomaly Detector Was Listening on Every Interface With No Auth, and That Was the Whole Bug

Juniper's Anomaly Detector Was Listening on Every Interface With No Auth, and That Was the Whole Bug

September 15, 2026
11 min read
0 views
Share:

CVE-2026-21902 (CVSS 9.8) hands root on Juniper PTX routers to anyone who can reach port 8160 — because a service meant to stay internal was bound to 0.0.0.0 with zero authentication, no memory corruption required.

Port 8160. No TLS, no API key, no session cookie, nothing. Just an open REST API sitting on 0.0.0.0:8160 on a Juniper PTX core router, happily accepting POST requests from anyone who could route a packet to it. That single fact is CVE-2026-21902 in its entirety, and it's the kind of bug that makes fifteen years of "we'll firewall it later" flash before your eyes.

I've racked and cabled PTX-class gear. These aren't edge boxes sitting in a branch closet — they're the ones service providers put in their core, their peering points, their data center interconnects. The blast radius on a box like that isn't "one office loses internet," it's "a chunk of somebody's backbone goes dark or gets quietly MITM'd." So when watchTowr Labs published a writeup titled, with their usual dry humor, "Sometimes, You Can Just Feel The Security In The Design," I went and read the whole thing twice.


What CVE-2026-21902 Actually Is

Juniper's own advisory, released as an out-of-cycle security bulletin, classifies this as an Incorrect Permission Assignment for Critical Resource vulnerability in the On-Box Anomaly Detection framework of Junos OS Evolved, affecting PTX Series routers only. The advisory text is worth quoting directly, because it tells you everything in two sentences:

An Incorrect Permission Assignment for Critical Resource vulnerability in the On-Box Anomaly detection framework of Juniper Networks Junos OS Evolved on PTX Series allows an unauthenticated, network-based attacker to execute code as root.

The On-Box Anomaly detection framework should only be reachable by other internal processes over the internal routing instance, but not over an externally exposed port. With the ability to access and manipulate the service to execute code as root a remote attacker can take complete control of the device. Please note that this service is enabled by default as no specific configuration is required.

Translated out of advisory-speak: there's a Python REST API on the box, it's supposed to be reachable only by other internal processes, it runs as root, and instead of being bound to an internal loopback or a routing-instance-scoped address, it's bound to every interface on the router. No configuration is required to be vulnerable — it ships this way. The CVSS score is 9.8, which is about as close to maximum as scoring conventions allow for something that isn't also wormable.

Junos OS Evolved, Briefly

If you've only worked with classic Junos, Junos OS Evolved is worth a sentence of context. Classic Junos runs on a FreeBSD base; Evolved is Juniper's re-platforming onto Linux, with a more modular, containerized internal architecture. It's the newer, more "modern" OS variant Juniper ships on higher-end platforms like the PTX series — routers built for core, peering, and hyperscale interconnect roles where throughput and port density matter more than anything else. The modernization is generally a good thing. It also means more moving parts, more daemons, more surface — and in this case, one of those daemons is the entire vulnerability.

Affected Versions

Junos OS Evolved VersionStatus
Before 25.4R1-EVONot vulnerable
25.4 versions before 25.4R1-S1-EVOVulnerable
Before 25.4R2-EVOVulnerable
25.4R1-S1-EVO, 25.4R2-EVO, and laterFixed

Per follow-on analysis, the affected hardware list includes the PTX10001-36MR, PTX10002-36QDD, PTX10003, PTX10004, PTX10008, and PTX10016 — in other words, the current PTX10000 family across the board.


The Anomaly Detection Framework: What It's Supposed to Do

The On-Box Anomaly Detection framework is a legitimate, useful idea badly exposed. It's meant to let the router run automated diagnostic and monitoring routines internally — detect hardware faults, traffic anomalies, protocol errors — and react to them without needing an external monitoring system bolted on. Juniper built it around four concepts:

  • Command — an actual shell command to be executed on the device
  • Handler — processes the output data from a command
  • DAG (Directed Acyclic Graph) — a workflow chaining commands, handlers, or sub-DAGs together
  • DAG Instance — a specific, scheduled execution of a DAG

Read that list again with an attacker's eye and the design problem jumps out immediately: the first-class primitive in this framework is an arbitrary shell command. That's not a side effect of some other feature being abused — command execution is the literal product. The entire security model for something like that has to rest on "only trusted internal processes can reach this API." Which is exactly the assumption that turned out to be false.

watchTowr's researchers ran ss on a live device to check Juniper's claim that the framework binds only internally, and got back exactly the socket table you'd hope never to see on a router that's supposed to have an internal-only service:

ProtocolBinding IPPortApplication
TCP0.0.0.022SSH (xinetd)
TCP0.0.0.053DNS (dnsmasq)
TCP0.0.0.0830NETCONF over SSH (xinetd)
TCP0.0.0.08160On-Box Anomaly Detection (api_server.py)

And the code backs it up — the service's own startup logic binds to an empty host string, which in Python's socket semantics means "all interfaces":

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

server_address = ('', port) is the whole bug. Not a buffer overflow, not a deserialization gadget chain, not a race condition. One tuple, in one file, that should have read something like ('127.0.0.1', port) or been scoped to the internal routing-instance address, and wasn't.


How the Exploit Chain Actually Works

The API exposes a fairly ordinary set of CRUD endpoints for managing anomalies, DAGs, commands, and handlers:

MethodPathDescription
GET/anomalyRetrieves all registered anomalies
GET / POST / PUT / DELETE/config/dag/<dag-name>Manages a DAG configuration
GET / POST / PUT / DELETE/config/command/<command-name>Manages a COMMAND configuration
GET / POST / PUT / DELETE/config/dag-instance/<dag-instance-name>Manages a DAG INSTANCE configuration
GET / POST/config/commitValidates and commits the pending workspace configuration

Chaining those into root code execution takes exactly four unauthenticated HTTP requests. Here's the kill chain, step by step:

  1. Create a command. POST /config/command/<name> with a JSON body whose type is set to RE-SHELL and whose syntax field is the shell command to run — in watchTowr's demo, simply id > /var/home/admin/watchTowr.txt. The RE-SHELL type tells the framework this string should be executed directly as a shell command, no CLI wrapping involved.
  2. Create a DAG. POST /config/dag/<name> defining a workflow whose only action is to invoke the command just created — no inputs, no handlers, nothing fancy needed.
  3. Create a DAG instance. POST /config/dag-instance/<name> scheduling that DAG to run against the Routing Engine ("target": {"type": "RE"}), with "delay": 0 so it fires immediately rather than waiting on a schedule.
  4. Commit. POST /config/commit persists the command, DAG, and DAG instance to disk so the scheduler daemon can pick them up.

From there, a separate always-running process called schedule_enforcer checks for due DAG instances roughly every 30 seconds, walks the DAG, and calls execute_command(). That function pulls the attacker-supplied syntax string straight out of the command definition and hands it to subprocess.run(command, shell=True, ...) with no sanitization, no allow-list, no privilege drop. Since the whole framework runs as root to begin with, so does the resulting shell.

Figure 1: The full four-request exploitation flow — create command, create DAG, create DAG instance, commit — as mapped by watchTowr Labs (Source: watchTowr Labs).

And the payoff is about as blunt as it gets. Thirty seconds after the commit request lands, the scheduled command executes on the router as root:

Figure 2: Reading back the output file after exploitation confirms full root execution on the device (Source: watchTowr Labs).

uid=0(root) gid=0(root) groups=0(root). No credentials were ever presented. There's no authentication bypass to find here because there was never any authentication to bypass — the entire vulnerability class this falls under is missing authentication for a critical resource, wrapped inside a permission/binding mistake. watchTowr also published a working proof-of-concept and a detection tool on GitHub (watchtowrlabs/watchTowr-vs-JunosEvolved-CVE-2026-21902), so this is not a theoretical bug — anyone with the repo and a routable IP to a vulnerable box has what they need.


My Take

I want to push back gently on the instinct to file this one under "lol Juniper messed up." I've spent enough years staring at service configs — dnsmasq, xinetd, homegrown Python daemons wedged into vendor firmware — to know exactly how this class of bug is born. Somebody wrote api_server.py assuming it would only ever be reached over an internal routing instance, because that's how it's documented to work, and because in the dev/lab environment it probably only ever was reached that way. Nobody circled back and asked "what happens if this binds to every interface instead of just the internal one, in a build meant to ship to customers?" That question is cheap to ask in a design review and brutally expensive to answer after the fact, which is exactly the shape of most of the worst vulnerabilities I've dealt with professionally — not clever, just unchecked.

What actually worries me about this one isn't the exploit chain — it's genuinely simple once you see it, four requests and a thirty-second wait — it's the trust model underneath it. The framework's core primitive is "run this arbitrary shell string as root, on request." That's an enormous amount of power to hand to a component whose only real protection is "attackers shouldn't be able to reach this network segment." Defense in depth isn't a slide in a vendor pitch deck; it's supposed to mean that when your one assumption fails — and assumptions about network reachability fail constantly, through misconfiguration, through a bridged management VLAN, through someone's temporary NAT rule that never got removed — there's a second control behind it. Here there wasn't one. No auth token, no mTLS, no allow-list, nothing. The bind address was the only thing standing between "internal telemetry service" and "unauthenticated root shell," and it was wrong from day one.

I'd also flag the CVSS 9.8 and the "no configuration required" line together, because that combination is what makes this genuinely dangerous rather than just embarrassing. A lot of critical CVEs need a non-default setting, an enabled feature flag, or a specific deployment pattern to be reachable. This one doesn't. If you're running an affected Junos OS Evolved build on PTX hardware, you were exposed the moment the box came up, with zero action on your part. That's the exact profile that shows up in mass-scanning campaigns within days of a PoC going public — and a PoC is already public.


What You Should Do Right Now

  • Patch first, argue about root cause later. Upgrade any PTX Series device running Junos OS Evolved 25.4 to 25.4R1-S1-EVO, 25.4R2-EVO, or later. If you're on a version before 25.4R1-EVO, you're not affected by this specific CVE, but don't take that as a reason to stay behind — check Juniper's advisory for your exact train.
  • If you can't patch immediately, disable the service. Juniper's guidance is to run request pfe anomalies disable as an interim mitigation. Treat this as a stopgap, not a fix — verify it actually closes the listening socket on 8160 in your environment before you trust it.
  • Check what's actually listening on your edge. Don't take a vendor's "internal only" claim at face value — I never do anymore, and this writeup is a good reason why. Run ss -tulnp (or your platform's equivalent) against your core and edge routers and specifically look for anything bound to 0.0.0.0 or [::] that you can't immediately justify. Port 8160 is the specific IOC here, but the habit of auditing bound sockets on routing infrastructure should be routine, not incident-driven.
  • Get management-plane traffic off routable interfaces entirely. If your PTX boxes have any path where 8160 (or NETCONF on 830, or SSH on 22) is reachable from anything other than a locked-down out-of-band management network, that's a standing architectural risk independent of this specific CVE. Use a dedicated management VRF or out-of-band network, and firewall filter inbound traffic to the Routing Engine (lo0 filters in Junos terms) as a baseline, not an afterthought.
  • Watch for the IOCs. The GitHub PoC from watchTowr Labs doubles as a detection tool — use it to check your own fleet before someone else does. Also check logs and any file artifacts on affected devices for unexpected files under /var/home/admin/ or similar admin home paths, which is where the framework's command output tends to land by default in this exploitation pattern.
  • If you run any external attack surface management or exposure scanning, add port 8160 to your PTX fleet's watchlist even after patching. A regression here — a config template that re-enables the old bind, a downgrade during a rollback — should trip an alert, not wait for the next audit cycle.

This article was researched and drafted with AI assistance as part of an experiment in building a faster tech-writing workflow, then reviewed and edited before publishing.


Primary source: Sometimes, You Can Just Feel The Security In The Design (Juniper Junos Evolved CVE-2026-21902 Pre-Auth RCE) — watchTowr Labs, March 3, 2026

Secondary source: CVE-2026-21902: Junos OS Evolved Privilege Escalation Flaw — SentinelOne Vulnerability Database

Secondary source: Juniper JunOS OS Evolved Pre-authenticated Remote Code Execution (CVE-2026-21902) — SonicWall

Keep Reading

Comments