PHP exec() Called With Unsanitized Attack Data — Shell Metacharacters Run as Commands
- CVE
- CVE-2026-48687
- CVSS
- 8.1 (High)
- CWE
- CWE-78 (OS Command Injection)
- Affected
- FastNetMon Community Edition <= 1.2.9
- Component
- src/juniper_plugin/fastnetmon_juniper.php, function
_log(), lines 115-119 - Attack Vector
- Indirect remote (via the attack notification pipeline)
- Discovered by
- Lorikeet Security
FastNetMon ships with a number of "notification scripts" that integrate the detector with router platforms for automated mitigation. The Juniper plugin (src/juniper_plugin/fastnetmon_juniper.php) is a PHP script that FastNetMon invokes when an attack is detected, with command-line arguments containing the attacker's IP address, the direction of the attack, and the traffic rate. The script then pushes a Juniper NETCONF configuration change to add a discard route for the attacking IP, plus logs a message recording the action.
The logging function, _log(), accepts a message string and writes it to a temporary log file using exec("echo `date` \"- [FASTNETMON] - $msg \" >> $FILE_LOG_TMP"). The $msg parameter is built by interpolating the script's command-line arguments — the attack IP, direction, and power values — into a status string. Those arguments are not sanitized, and PHP's exec() runs the entire concatenated string through /bin/sh. Any shell metacharacter in the attack data turns into command execution.
Disclosure status: Lorikeet Security notified FastNetMon LTD on April 25, 2026. CVE-2026-48687 was assigned by MITRE. No vendor response or fix as of May 23, 2026.
The vulnerable code
// src/juniper_plugin/fastnetmon_juniper.php, lines 115-119
function _log( $msg ) {
$FILE_LOG_TMP = "/tmp/fastnetmon_api_juniper.log";
exec( "echo `date` \"- [FASTNETMON] - " . $msg . " \" >> " . $FILE_LOG_TMP );
}
// Earlier in the same file:
$IP_ATTACK = $argv[1];
$DIRECTION_ATTACK = $argv[2];
$POWER_ATTACK = $argv[3];
// ...
_log("Ban: IP=$IP_ATTACK direction=$DIRECTION_ATTACK power=$POWER_ATTACK");
The pattern is exactly the textbook shell injection: untrusted input concatenated into a shell command string, passed to exec() without escaping. If any of $IP_ATTACK, $DIRECTION_ATTACK, or $POWER_ATTACK contain backticks, $( ), semicolons, or newlines, the resulting shell command parses and executes them.
Why $IP_ATTACK is attacker-influenceable
You might object that $IP_ATTACK comes from FastNetMon's C++ core, which currently passes IP addresses via inet_ntoa() — a function that only produces well-formed dotted-decimal output. That's true today. It is not true in general:
- Future code changes could introduce string-sourced IPs, IPv6 with brackets, or hostnames into the attack notification pipeline. Any such change would silently activate this CVE without any other code change.
- Direct invocation. The script is a standalone PHP file. Anyone with shell access (including a low-privilege user on the host) can invoke it directly with arbitrary argv, and the injection runs as the user who ran it. If the script is invoked via a wrapper that runs it as root or as a privileged service account, the impact is correspondingly elevated.
- Alternative invocation paths. Orchestration systems, monitoring tools, custom wrappers, and CI scripts sometimes invoke these notify scripts directly to test integrations. Each of those invocation paths is a potential injection vector.
- The gRPC API (CVE-2026-48692) allows unauthenticated callers to trigger the notify pipeline. That pipeline ultimately invokes scripts like this one. If the gRPC API ever takes a string-sourced IP, the chain becomes remote-unauthenticated.
The current FastNetMon code happens to pass safe values today, but the vulnerability is in the script, not in the caller. Defensive coding requires the script to validate or escape its own inputs — the surrounding system's invariants can change, and they will.
Exploit example
If an attacker can supply a value for $IP_ATTACK that contains shell metacharacters — say, 1.2.3.4`id>/tmp/owned` — the log line becomes:
echo `date` "- [FASTNETMON] - Ban: IP=1.2.3.4`id>/tmp/owned` direction=in power=1000 " >> /tmp/fastnetmon_api_juniper.log
The shell sees the backticks, runs id, captures its output, and substitutes it into the echo string. As a side effect, id's output is written to /tmp/owned. Substitute any command you want; the same mechanism executes it.
The impact runs as whoever executes the PHP script. In production deployments, this is typically the FastNetMon process user, which is often root because of the raw-packet-capture requirement. Even on hardened deployments where FastNetMon runs as a low-privilege user, the notify script frequently runs as root via sudo (because it needs to push NETCONF configuration to a router), which makes the injection a privilege escalation.
How a fix should look
There are two correct fixes. The first replaces exec() with PHP's native file API, which has no shell involvement:
function _log( $msg ) {
$FILE_LOG_TMP = "/tmp/fastnetmon_api_juniper.log";
$timestamp = date("Y-m-d H:i:s");
file_put_contents(
$FILE_LOG_TMP,
"$timestamp - [FASTNETMON] - $msg\n",
FILE_APPEND | LOCK_EX
);
}
No shell, no exec, no metacharacter problem. This is the recommended fix — you don't need a shell to append a line to a file. The original code probably used exec("echo ... >> file") because date was easier to reach via shell substitution than via PHP's date function. file_put_contents() with a PHP-native timestamp is the modern idiom.
The second correct fix, if for some reason you must use exec, is to escape every interpolated value:
function _log( $msg ) {
$FILE_LOG_TMP = "/tmp/fastnetmon_api_juniper.log";
$escaped = escapeshellarg($msg);
exec( "echo `date` " . $escaped . " >> " . escapeshellarg($FILE_LOG_TMP) );
}
escapeshellarg() wraps the value in single quotes and escapes any embedded single quotes, neutralizing shell metacharacters. This is more brittle than the file API because every concatenation has to remember to escape; one missed call site reintroduces the bug. Prefer the file-API form.
Compensating controls
- Don't use the Juniper plugin if you don't need it. Plugin invocation is configured in
fastnetmon.conf; if you don't push Juniper configurations, comment the plugin out. The script never runs, the bug is never reachable. - Audit the IP normalization path from the C++ core through to the script. As long as IP addresses are emitted by
inet_ntoa()(which only produces digits and dots), the input is safe; but if anyone has modified the chain to pass string-sourced IPs, the injection is live. - Run the script as a low-privilege user. If you must use it, run it under a dedicated account with only the privileges needed to call NETCONF (a network user account with the right SSH keys, not root). Injection still produces unwanted shell commands, but the impact is constrained.
- Restrict shell access on the FastNetMon host. If the only invocation path is FastNetMon's own attack pipeline, the input source is controlled. Don't let unrelated users or processes invoke notify scripts directly.
The pattern: notify scripts are security-relevant
This is the third command-injection bug we've seen in this disclosure series (alongside CVE-2026-48694 and CVE-2026-48695), all in the notify-script family. There is a recurring assumption in DDoS-mitigation tooling that notify scripts are "internal" and therefore don't need the same input validation as user-facing code. That assumption is wrong for at least three reasons:
- Scripts that take input from network observations are taking attacker input. The whole point of the notify script is to react to an external attack. The data being reacted to comes from the attacker. There is no internal vs. external boundary at the point where the script reads its argv.
- Scripts get invoked from places nobody anticipated. Operators write wrappers, cron jobs invoke the binaries with parsed parameters, monitoring systems trigger test runs. Every additional invocation path is a new input source.
- Scripts often run with elevated privileges. They push configuration to routers, modify firewall rules, restart services. Sudo wrappers and setuid wrappers are common. The privilege concentration in notify scripts is high; the security review of them is usually low.
The fix for the class is to treat notify scripts as exposed surface and write them defensively from the start: validate inputs against strict formats (IP regex for IPs, integer regex for rates, whitelisted enums for directions), use language-native APIs instead of shelling out, and run them as low-privilege users. Treat them like CGI scripts — because that's effectively what they are.
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-48687 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.