Junos Configuration Strings Interpolate Attacker Data — Full Router Compromise
- CVE
- CVE-2026-48694
- CVSS
- 8.1 (High)
- CWE
- CWE-77 (Improper Neutralization of Special Elements used in a Command)
- Affected
- FastNetMon Community Edition <= 1.2.9
- Component
- src/juniper_plugin/fastnetmon_juniper.php, lines 69 (ban) and 90 (unban)
- Attack Vector
- Indirect remote (via the attack notification pipeline)
- Discovered by
- Lorikeet Security
The Juniper integration plugin (src/juniper_plugin/fastnetmon_juniper.php) sends configuration changes to a Juniper router over NETCONF when FastNetMon decides to ban or unban an IP. The configuration string includes the attacker's IP, interpolated directly into a Junos CLI command. There is no validation, no escaping, no allowlist for the IP value's format.
This is distinct from CVE-2026-48687, which is a shell-level command injection in the plugin's logging function. CVE-2026-48694 is a different layer of injection — into Junos configuration syntax, not into /bin/sh — with a different impact ceiling (full router compromise versus arbitrary shell on the host).
Disclosure status: Lorikeet Security notified FastNetMon LTD on April 25, 2026. CVE-2026-48694 was assigned by MITRE. No vendor response or fix as of May 23, 2026.
The vulnerable code
// src/juniper_plugin/fastnetmon_juniper.php, line 69 (ban action)
$conn->load_set_configuration(
"set routing-options static route " . "{$IP_ATTACK} community 65535:666 discard"
);
// No validation of $IP_ATTACK format.
// Line 90 (unban action)
$conn->load_set_configuration(
"delete routing-options static route {$IP_ATTACK}/32"
);
// Same problem.
Junos's load_set_configuration accepts a series of set / delete commands, one per line. The function does not perform any quoting or escaping on its argument string; it treats the entire input as a sequence of configuration mode commands. A newline in the argument starts a new command.
If the attacker can influence $IP_ATTACK to contain a newline character, they can inject additional Junos commands after the original set routing-options line. Those injected commands run in the same NETCONF session, with the same privileges as whatever account the plugin is configured to use — typically a privileged operator or super-user account, because the plugin needs to modify the routing table.
What an attacker can inject
The full Junos CLI is available to the attacker. Some particularly damaging examples:
// Inject a backdoor user with full configure access:
1.2.3.4 community 65535:666 discard
set system login user attacker class super-user authentication plain-text-password Attack3r!
// Disable a firewall filter:
1.2.3.4 community 65535:666 discard
delete firewall filter PROTECT_MGMT term DROP_ALL
// Enable SNMP write community for further reconnaissance and modification:
1.2.3.4 community 65535:666 discard
set snmp community public authorization read-write
// Modify BGP policy to leak prefixes to unintended peers:
1.2.3.4 community 65535:666 discard
delete policy-options policy-statement BGP_OUT term DENY_PRIVATE
// Hijack BGP sessions by changing the peer-as on existing sessions:
1.2.3.4 community 65535:666 discard
set protocols bgp group EXTERNAL neighbor 203.0.113.1 peer-as 65501
Every one of those is a Junos one-liner the attacker can inject by including the appropriate string in $IP_ATTACK. Combined, they convert "the attacker can blackhole one IP" into "the attacker has root-equivalent control of the router."
The injection format
Junos set commands are line-delimited. The newline character (\n) is the command separator. An attacker who places 1.2.3.4 community 65535:666 discard\nset system login user attacker class super-user authentication plain-text-password Attack3r! in $IP_ATTACK produces this NETCONF payload:
set routing-options static route 1.2.3.4 community 65535:666 discard
set system login user attacker class super-user authentication plain-text-password Attack3r! community 65535:666 discard
The first line is the intended command. The second line is the injected command. Junos parses both lines as separate set statements. The trailing community 65535:666 discard on the injected line is junk that Junos will likely reject — but it might be possible to craft an injection where the trailing tokens form valid Junos syntax for the second statement, depending on which configuration element is being targeted. Even when the trailing tokens cause an error, the injected line may execute before the error stops processing — this depends on Junos's transactional semantics for load_set_configuration.
How an attacker reaches this
The same chain as CVE-2026-48687: any path that lets an attacker influence the $IP_ATTACK argument reaches the bug. In the current code, $IP_ATTACK is sourced from argv[1], which FastNetMon's C++ core populates via inet_ntoa() (currently safe). The vulnerability becomes live the moment any of these conditions changes:
- The plugin is invoked from a different path (orchestration system, monitoring tool, custom wrapper) that passes string-sourced IPs.
- FastNetMon adopts IPv6 ban support that doesn't go through
inet_ntoa()(which is IPv4-only). IPv6 address printing in C usesinet_ntop(), which still produces safe output, but a hand-rolled formatter or a stringified parsing result could introduce unexpected characters. - An attacker exploits another vulnerability (e.g., the unauthenticated gRPC API) to invoke the notify pipeline with a controlled IP value.
- An operator's custom wrapper script invokes the plugin with attacker-influenced data.
The vulnerability is in the plugin's input handling, not in the calling chain. The safe defaults of the calling chain reduce current exposure but do not eliminate the bug.
How a fix should look
// At the top of fastnetmon_juniper.php
function validate_ipv4_or_die($candidate) {
if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $candidate, $m)) {
fwrite(STDERR, "FATAL: invalid IPv4 in argv[1]: " . var_export($candidate, true) . "\n");
exit(1);
}
for ($i = 1; $i <= 4; $i++) {
if ((int)$m[$i] > 255) {
fwrite(STDERR, "FATAL: IPv4 octet out of range\n");
exit(1);
}
}
return $candidate;
}
$IP_ATTACK = validate_ipv4_or_die($argv[1]);
// From here, $IP_ATTACK is guaranteed to be a syntactically-valid dotted-quad with
// no newlines, no spaces, no Junos CLI metacharacters.
// Then the existing code can interpolate $IP_ATTACK with confidence:
$conn->load_set_configuration(
"set routing-options static route {$IP_ATTACK} community 65535:666 discard"
);
The fix is a strict regex against a known-safe IPv4 dotted-quad format, applied at script entry. Any other format (IPv6, hostnames, anything with newlines or special characters) is rejected at the boundary with a clear error.
For IPv6 support, add a separate regex for IPv6 string format and validate against that path. Do not try to merge the validation into a single permissive pattern; keep IPv4 and IPv6 cases explicit. Junos's NETCONF interface accepts both formats; the script just needs to know which one it's about to send.
A defensive secondary improvement is to use Junos's structured configuration format (XML over NETCONF) instead of string-formatted set commands. The XML format requires field names and values to be passed as separate XML elements; there is no way for a value to inject a new field. This eliminates the entire class of injection bug. The cost is migrating the plugin to use the NETCONF XML API, which is more verbose but structurally safer.
Compensating controls
- Add input validation in a wrapper script. If you can't modify the plugin itself, wrap it in a shell script or a separate PHP entrypoint that validates
argv[1]with a strict IPv4 regex before calling the original. Configure FastNetMon to invoke your wrapper instead of the original plugin. - Restrict the NETCONF account's privileges. The plugin authenticates to the router as a Junos user. If that user has
super-userclass, injection allows full router compromise. Configure the user with a custom class that only permitsconfigure private-level access torouting-options static— the user can ban IPs but cannot, for example, create new user accounts or modify firewall filters. Junos'spermissionsystem supports this; use it. - Audit Junos commit logs for unexpected configuration changes from the FastNetMon NETCONF user. Any change outside
routing-options staticshould be a high-priority alert. - Disable the plugin if you don't use it. Plugin invocation is configured in
fastnetmon.conf. If you don't push Juniper configurations, comment the plugin out.
The pattern: interpolating attacker input into anyone's CLI
This bug is in the same family as SQL injection, shell injection, LDAP injection, and Junos's older friend — XPath injection. The structural pattern is the same in all cases: a string is built by concatenating literal command syntax with attacker-influenceable values, and the resulting string is sent to a parser that treats some characters as syntax separators. Newlines in Junos configuration commands play the same role as quotes in SQL queries: they let attacker-supplied data escape from "value" context into "command" context.
The defensive habit applies everywhere: never interpolate attacker-influenceable values into command syntax. Use parameterized APIs where they exist (NETCONF XML for Junos, prepared statements for SQL, execve with separate argv for shell), and where they don't exist, use language-native escaping or strict allowlists at the input boundary. The plugin needs to know what an IP address looks like; reject anything that isn't one.
For network-device integrations specifically: parameterized configuration APIs exist on every major platform (NETCONF XML, RESTCONF, gNMI, Cisco YANG). They're verbose, but they're structurally safe. The string-concat load_set_configuration idiom is fast and convenient, and it produces this bug every time it's combined with non-strictly-validated input. Pick the structurally safe form unless you have a measured reason not to.
Disclosure timeline
| Date | Event |
|---|---|
| 2026-04-25 | Vulnerability identified during Lorikeet Security source code audit of FastNetMon Community Edition 1.2.9 |
| 2026-04-25 | CVE ID requested from MITRE |
| 2026-04-25 | Vendor (Pavel Odintsov / FastNetMon LTD) notified at the contact published in SECURITY.md |
| 2026-05-22 | CVE-2026-48694 assigned by MITRE |
| TBD | Vendor response |
| TBD | Fix release |
| 2026-05-23 | Lorikeet Security publishes responsible disclosure report |
Full Responsible Disclosure Report (PDF)
Complete writeup of all 16 FastNetMon Community Edition vulnerabilities Lorikeet Security identified, including vulnerable-code excerpts, impact analysis, and remediation guidance for each CVE.
Auditing the network infrastructure your business depends on
Lorikeet Security finds the parser bugs, protocol confusion, and unauthenticated-control-plane issues that move your DDoS detector, your BGP speaker, and your edge devices from "running" to "exploitable." Source code review, fuzz harness development, and adversarial protocol testing.