local comm = require "comm" local string = require "string" local table = require "table" local shortport = require "shortport" local nmap = require "nmap" local stdnse = require "stdnse" local U = require "lpeg-utility" local http = require "http" local snmp = require "snmp" local sslcert = require "sslcert" local tls = require "tls" local url = require "url" local json = require "json" description = [[ Search SD-WAN products from SDWAN NewHope research project database by - server name - http titles - snmp descriptions - ssl certificates The search database is based on census.md document with SD-WAN products search queries. Also this script is based on: - http-server-header NSE script by Daniel Miller - http-title NSE script by Diman Todorov - snmp-sysdescr NSE script by Thomas Buchanan - ssl-cert NSE script by David Fifield Installation $ git clone https://github.com/sdnewhop/sdwan-infiltrator $ cd /sdwan-infiltrator/ $ sudo cp * /usr/share/nmap/scripts/ $ sudo nmap --script infiltrator --script-args infiltrator.version=true -sS -sU -p U:161,T:80,443,8008,8080,8443 or -iL ]] -- -- @usage -- nmap --script=infiltrator.nse -sS -sU -p U:161,T:80,443,8008,8080,8443 or -iL -- -- @output -- | infiltrator: -- | status: success -- | method: server -- | product: -- | host_addr: ... -- | host_port: 443 -- |_ version: ... -- ... -- | infiltrator: -- | status: success -- | method: title -- | product: -- | host_addr: ... -- | host_port: 443 -- |_ version: ... -- ... -- | infiltrator: -- | status: success -- | method: snmp -- | product: -- | host_addr: ... -- | host_port: 161 -- |_ version: ... -- ... -- | infiltrator: -- | status: success -- | method: SSL certificate -- | product: -- | host_addr: ... -- | host_port: 443 -- |_ version: ... author = "sdnewhop" license = "Same as Nmap--See https://nmap.org/book/man-legal.html" categories = {"default", "discovery", "safe"} portrule = shortport.portnumber({80, 161, 443, 8008, 8080, 8443}, {"tcp", "udp"}, {"open"}) SDWANS_BY_SSL_TABLE = { ["Cisco SD-WAN"] = {"Viptela Inc"}, ["Versa Analytics"] = {"versa%-analytics"}, ["Versa Director"] = {"director%-1", "versa%-director"}, ["Riverbed SteelHead"] = {"Riverbed Technology"}, ["Silver Peak Unity Orchestrator"] = {"Silverpeak GMS"}, ["Silver Peak Unity EdgeConnect"] = {"silver%-peak", "Silver Peak Systems Inc"}, ["CloudGenix SD-WAN"] = {"CloudGenix Inc."}, ["Talari SD-WAN"] = {"Talari", "Talari Networks"}, ["InfoVista SALSA"] = {"SALSA Portal"}, ["Barracuda CloudGen Firewall"] = {"Barracuda CloudGen Firewall", "Barracuda Networks"}, ["Viprinet Virtual VPN Hub"] = {"Viprinet"}, ["Citrix Netscaler SD-WAN"] = {"Citrix Systems"}, ["Fortinet FortiGate SD-WAN"] = {"FGT%-", "FortiGate"} } SDWANS_BY_SNMP_TABLE = { ["Fatpipe SYMPHONY SD-WAN"] = {"Linux Fatpipe"}, ["Versa Analytics"] = {"Linux versa%-analytics"}, ["Juniper Networks Contrail SD-WAN"] = {"Juniper Networks, Inc. srx"}, ["Aryaka Network Access Point"] = {"Aryaka Networks Access Point"}, ["Arista Networks EOS"] = {"Arista Networks EOS"}, ["Viprinet Virtual VPN Hub"]= {"Viprinet VPN Router"} } SDWANS_BY_TITLE_TABLE = { ["VMWare NSX SD-WAN"] = {"VeloCloud", "VeloCloud Orchestrator"}, ["TELoIP VINO SD-WAN"] = {"Teloip Orchestrator API"}, ["Fatpipe SYMPHONY SD-WAN"] = {"WARP"}, ["Cisco SD-WAN"] = {"Viptela vManage", "Cisco vManage"}, ["Versa Flex VNF"] = {"Flex VNF"}, ["Versa Director"] = {"Versa Director Login"}, ["Riverbed SteelConnect"] = {"SteelConnect Manager", "Riverbed AWS Appliance"}, ["Riverbed SteelHead"] = {"amnesiac Sign in"}, ["Citrix NetScaler SD-WAN VPX"] = {"Citrix NetScaler SD%-WAN %- Login"}, ["Citrix NetScaler SD-WAN Center"] = {"SD%-WAN Center | Login"}, ["Citrix Netscaler SD-WAN"] = {"DC | Login"}, ["Silver Peak Unity Orchestrator"] = {"Welcome to Unity Orchestrator"}, ["Silver Peak Unity EdgeConnect"] = {"Silver Peak Appliance Management Console"}, ["Ecessa WANworX SD-WAN"] = {"Ecessa"}, ["Nuage Networks SD-WAN (VNS)"] = {"SD%-WAN Portal", "Architect", "VNS portal"}, ["Juniper Networks Contrail SD-WAN"] = {"Log In %- Juniper Networks Web Management"}, ["Talari SD-WAN"] = {"AWS"}, ["Aryaka Network Access Point"] = {"Aryaka Networks", "Aryaka, Welcome"}, ["InfoVista SALSA"] = {"SALSA Login"}, ["Huawei SD-WAN"] = {"Agile Controller"}, ["Sonus SBC Management Application"] = {"SBC Management Application"}, ["Sonus SBC Edge"] = {"Sonus SBC Edge Web Interface"}, ["Arista Networks EOS"] = {"Arista Networks EOS"}, ["128 Technology Networking Platform"] = {"128T Networking Platform"}, ["Gluware Control"] = {"Gluware Control"}, ["Barracuda CloudGen Firewall"] = {"Barracuda CloudGen Firewall"}, ["Viprinet Virtual VPN Hub"] = {"Viprinet %- AdminDesk %- Login"}, ["Viprinet Traffic Tools"] = {"Viprinet traffic tools"}, ["Cradlepoint SD-WAN"] = {"Login :: CR4250%-PoE", "Login :: AER2200%-600M"}, ["Brain4Net Orchestrator"] = {"B4N ORC"}, ["Fortinet FortiManager"] = {"FortiManager%-VM64"} } SDWANS_BY_SERVER_TABLE = { ["Versa Director"] = {"Versa Director"}, ["Versa Analytics"] = {"Versa%-Analytics%-Server"}, ["Barracuda CloudGen Firewall"] = {"Barracuda CloudGen Firewall"}, ["Viprinet Virtual VPN Hub"] = {"ViprinetHubReplacement", "Viprinet"} } ------------------------------------------------------------------------------- -- version gathering block ------------------------------------------------------------------------------- local function vbrain(host, port) local path = stdnse.get_script_args(SCRIPT_NAME .. ".path") or "/api/version" local response local output_info = {} local vsdwan = "" local urlp = path response = http.generic_request(host, port, "GET", path) if response.status == 301 or response.status == 302 then local url_parse_res = url.parse(response.header.location) urlp = url_parse_res.path stdnse.print_debug("Status code: " .. response.status) response = http.generic_request(host,port,"GET", urlp) end output_info = stdnse.output_table() if response == nil then return fail("Request failed") end local try_counter = 1 while try_counter < 6 and response.status ~= 200 do response = http.generic_request(host, port, "GET", urlp) found, matches = http.response_contains(response, '0;url%=(.*)"%/%>') if found == true then local urltmp = url.parse(matches[1]) urlp = urltmp.path response = http.generic_request(host, port, "GET", urlp) try_counter = 1 end try_counter = try_counter + 1 end if response.status == 200 then found, matches = http.response_contains(response, '"build":"(.+)",', false) if found == true then vsdwan = matches[1] else return nil end output_info.vsdwan_version = {} table.insert(output_info.vsdwan_version, "Brain4Net Orchestrator Version: " .. vsdwan) end return output_info, stdnse.format_output(true, output_info) end local function vcradlepoint(host, port) local path = stdnse.get_script_args(SCRIPT_NAME .. ".path") or "/login/?referer=/admin/" local response local output_info = {} local vsdwan = "" local urlp = path response = http.generic_request(host, port, "GET", path) if response.status == 301 or response.status == 302 then local url_parse_res = url.parse(response.header.location) urlp = url_parse_res.path stdnse.print_debug("Status code: " .. response.status) response = http.generic_request(host,port,"GET", urlp) end output_info = stdnse.output_table() if response == nil then return fail("Request failed") end local try_counter = 1 while try_counter < 6 and response.status ~= 200 do response = http.generic_request(host, port, "GET", urlp) try_counter = try_counter + 1 end if response.status == 200 then found, matches = http.response_contains(response, "([0-9.]+[0-9]) .[a-zA-Z]+.[a-zA-Z]+.[0-9]+.[0-9]+:[0-9]+:[0-9]+", false) if found == true then vsdwan = matches[1] else return nil end output_info.vsdwan_version = {} table.insert(output_info.vsdwan_version, "Cradlepoint App Version: " .. vsdwan) end return output_info, stdnse.format_output(true, output_info) end local function vcitrix(host, port) local path = stdnse.get_script_args(SCRIPT_NAME .. ".path") or "/" local response local output_info = {} local vsdwan = "" local urlp = path response = http.generic_request(host, port, "GET", path) if response.status == 301 or response.status == 302 then local url_parse_res = url.parse(response.header.location) urlp = url_parse_res.path -- stdnse.print_debug("Status code: " .. response.status) response = http.generic_request(host,port,"GET", urlp) end output_info = stdnse.output_table() if response == nil then return fail("Request failed") end local try_counter = 1 while try_counter < 30 and response.status ~= 200 do response = http.generic_request(host, port, "GET", urlp) try_counter = try_counter + 1 end if response.status == 200 then found, matches = http.response_contains(response, "css%?v%=([.0-9]+)", false) if found == true then vsdwan = matches[1] else return nil end output_info.vsdwan_version = {} table.insert(output_info.vsdwan_version, "Citrix NetScaler Version: " .. vsdwan) end return output_info, stdnse.format_output(true, output_info) end local function vfatpipe(host, port) local path = stdnse.get_script_args(SCRIPT_NAME .. ".path") or "/" local response local output_info = {} local vsdwan = "" local urlp = path response = http.generic_request(host, port, "GET", path) if response.status == 301 or response.status == 302 then local url_parse_res = url.parse(response.header.location) urlp = url_parse_res.path stdnse.print_debug("Status code: " .. response.status) response = http.generic_request(host,port,"GET", urlp) end output_info = stdnse.output_table() if response == nil then return fail("Request failed") end local try_counter = 1 while try_counter < 6 and response.status ~= 200 do response = http.generic_request(host, port, "GET", urlp) try_counter = try_counter + 1 end if response.status == 200 then found, matches = http.response_contains(response, "
([r.0-9]+)
", false) if found == true then vsdwan = matches[1] else return nil end output_info.vsdwan_version = {} table.insert(output_info.vsdwan_version, "Fatpipe Version: " .. vsdwan) end return output_info, stdnse.format_output(true, output_info) end local function vnuage(host, port) local path = stdnse.get_script_args(SCRIPT_NAME .. ".path") or "/" local response local output_info = {} local vsdwan = "" local urlp = path response = http.generic_request(host, port, "GET", path) if response.status == 301 or response.status == 302 then local url_parse_res = url.parse(response.header.location) urlp = url_parse_res.path stdnse.print_debug("Status code: " .. response.status) response = http.generic_request(host,port,"GET", urlp) end output_info = stdnse.output_table() if response == nil then return fail("Request failed") end local try_counter = 1 while try_counter < 6 and response.status ~= 200 do response = http.generic_request(host, port, "GET", urlp) try_counter = try_counter + 1 end if response.status == 200 then found, matches = http.response_contains(response, 'ng%-version="([.0-9]+)"', false) if found == true then vsdwan = matches[1] else return nil end output_info.vsdwan_version = {} table.insert(output_info.vsdwan_version, "Nuage Version: " .. vsdwan) end return output_info, stdnse.format_output(true, output_info) end local function vriverbed(host, port) local path = stdnse.get_script_args(SCRIPT_NAME .. ".path") or "/" local response local output_info = {} local vsdwan = "" local urlp = path response = http.generic_request(host, port, "GET", path) if response.status == 301 or response.status == 302 then local url_parse_res = url.parse(response.header.location) urlp = url_parse_res.path stdnse.print_debug("Status code: " .. response.status) response = http.generic_request(host,port,"GET", urlp) end output_info = stdnse.output_table() if response == nil then return fail("Request failed") end local try_counter = 1 while try_counter < 6 and response.status ~= 200 do response = http.generic_request(host, port, "GET", urlp) try_counter = try_counter + 1 end if response.status == 200 then found, matches = http.response_contains(response, "web3 v([.0-9]+)", false) if found == true then vsdwan = matches[1] else return nil end output_info.vsdwan_version = {} table.insert(output_info.vsdwan_version, "Riverbed Version: " .. vsdwan) end return output_info, stdnse.format_output(true, output_info) end local function vsilverpeak(host, port) local path = stdnse.get_script_args(SCRIPT_NAME .. ".path") or "/" local response local output_info = {} local vsdwan = "" local urlp = path response = http.generic_request(host, port, "GET", path) output_info = stdnse.output_table() if response == nil then return fail("Request failed") end if response.status == 302 then found, matches = http.response_contains(response, "http.*/([.0-9]+)/", false) if found == true then vsdwan = matches[1] else return nil end output_info.vsdwan_version = {} table.insert(output_info.vsdwan_version, "SilverPeak Version: " .. vsdwan) end return output_info, stdnse.format_output(true, output_info) end local function vsilverpeak_login(host, port) local output_info = {} output_info.login = {} local monitor_check = "/rest/json/login?user=monitor&password=monitor" local admin_check = "/rest/json/login?user=admin&password=admin" local resp_monitor = http.get(host, port, monitor_check) if not resp_monitor.status then -- force check on 80 port if empty response from 443 (by default) resp_monitor = http.get(host, 80, monitor_check) end if resp_monitor.status == 200 then table.insert(output_info.login, "Authentication successful (monitor:monitor)") end local resp_admin = http.get(host, port, admin_check) if not resp_admin.status then -- force check on 80 port if empty response from 443 (by default) resp_admin = http.get(host, 80, admin_check) end if resp_admin.status == 200 then table.insert(output_info.login, "Authentication successful (admin:admin)") end if next(output_info.login) ~= nil then return output_info, stdnse.format_output(true, output_info) end end local function vsonus_edge(host, port) local path = stdnse.get_script_args(SCRIPT_NAME .. ".path") or "/cgi/index.php" local response local output_info = {} local vsdwan = "" local urlp = path response = http.generic_request(host, port, "GET", path) if response.status == 301 or response.status == 302 then local url_parse_res = url.parse(response.header.location) urlp = url_parse_res.path stdnse.print_debug("Status code: " .. response.status) response = http.generic_request(host,port,"GET", urlp) end output_info = stdnse.output_table() if response == nil then return fail("Request failed") end local try_counter = 1 while try_counter < 6 and response.status ~= 200 do response = http.generic_request(host, port, "GET", urlp) try_counter = try_counter + 1 end if response.status == 200 then found, matches = http.response_contains(response, "/style/([.0-9]+)%-[0-9]+%_rel", false) if found == true then vsdwan = matches[1] else return nil end output_info.vsdwan_version = {} table.insert(output_info.vsdwan_version, "Sonus Edge Version: " .. vsdwan) end return output_info, stdnse.format_output(true, output_info) end local function vsonus_mgmt(host, port) local path = stdnse.get_script_args(SCRIPT_NAME .. ".path") or "/" local response local output_info = {} local vsdwan = "" local urlp = path response = http.generic_request(host, port, "GET", path) if response.status == 301 or response.status == 302 then local url_parse_res = url.parse(response.header.location) urlp = url_parse_res.path stdnse.print_debug("Status code: " .. response.status) response = http.generic_request(host,port,"GET", urlp) end output_info = stdnse.output_table() if response == nil then return fail("Request failed") end local try_counter = 1 while try_counter < 6 and (response.status ~= 503 or response.status ~= 200) do response = http.generic_request(host, port, "GET", urlp) try_counter = try_counter + 1 end if response.status == 503 or response.status == 200 then found, matches = http.response_contains(response, "EMA ([.0-9]+)", false) if found == true then vsdwan = matches[1] else return nil end output_info.vsdwan_version = {} table.insert(output_info.vsdwan_version, "Sonus Mgmt App Version: " .. vsdwan) end return output_info, stdnse.format_output(true, output_info) end local function vtalari(host, port) local path = stdnse.get_script_args(SCRIPT_NAME .. ".path") or "/" local response local output_info = {} local vsdwan = "" local urlp = path response = http.generic_request(host, port, "GET", path) if response.status == 301 or response.status == 302 then local url_parse_res = url.parse(response.header.location) urlp = url_parse_res.path stdnse.print_debug("Status code: " .. response.status) response = http.generic_request(host,port,"GET", urlp) end output_info = stdnse.output_table() if response == nil then return fail("Request failed") end local try_counter = 1 while try_counter < 6 and response.status ~= 200 do response = http.generic_request(host, port, "GET", urlp) try_counter = try_counter + 1 end if response.status == 200 then found, matches = http.response_contains(response, 'talari%.css%?([_.0-9A-Za-z]+)"', false) if found == true then vsdwan = matches[1] else return nil end output_info.vsdwan_version = {} table.insert(output_info.vsdwan_version, "Talari Version: " .. vsdwan) end return output_info, stdnse.format_output(true, output_info) end local function vversa_analytics(host, port) local path = stdnse.get_script_args(SCRIPT_NAME .. ".path") or "/versa/app/js/common/constants.js" local response local output_info = {} local vsdwan = "" local urlp = path response = http.generic_request(host, port, "GET", path) if response.status == 301 or response.status == 302 then local url_parse_res = url.parse(response.header.location) urlp = url_parse_res.path stdnse.print_debug("Status code: " .. response.status) response = http.generic_request(host,port,"GET", urlp) end output_info = stdnse.output_table() if response == nil then return fail("Request failed") end local try_counter = 1 while try_counter < 6 and response.status ~= 200 do response = http.generic_request(host, port, "GET", urlp) found, matches = http.response_contains(response, '0;url%=(.*)"%/%>') if found == true then local urltmp = url.parse(matches[1]) urlp = urltmp.path response = http.generic_request(host, port, "GET", urlp) try_counter = 1 end try_counter = try_counter + 1 end if response.status == 200 then found, matches = http.response_contains(response, "%/analytics%/([v.0-9]+)%/", false) if found == true then vsdwan = matches[1] else return nil end output_info.vsdwan_version = {} table.insert(output_info.vsdwan_version, "Versa Analytics Version: " .. vsdwan) end return output_info, stdnse.format_output(true, output_info) end local function vversa_flex(host, port) local path = stdnse.get_script_args(SCRIPT_NAME .. ".path") or "/scripts/main-layout/main-layout-controller.js" local response local output_info = {} local vsdwan = "" local urlp = path response = http.generic_request(host, port, "GET", path) if response.status == 301 or response.status == 302 then local url_parse_res = url.parse(response.header.location) urlp = url_parse_res.path stdnse.print_debug("Status code: " .. response.status) response = http.generic_request(host,port,"GET", urlp) end output_info = stdnse.output_table() if response == nil then return fail("Request failed") end local try_counter = 1 while try_counter < 6 and response.status ~= 200 do response = http.generic_request(host, port, "GET", urlp) try_counter = try_counter + 1 end if response.status == 200 then found, matches = http.response_contains(response, '"versa%-flexvnf%-([.0-9%-a-zA-Z]+)', false) if found == true then vsdwan = matches[1] else return nil end output_info.vsdwan_version = {} table.insert(output_info.vsdwan_version, "Versa Flex Version: " .. vsdwan) end return output_info, stdnse.format_output(true, output_info) end local function vvmware_nsx(host, port) local path = stdnse.get_script_args(SCRIPT_NAME .. ".path") or "/" local response local output_info = {} local vsdwan = "" local urlp = path response = http.generic_request(host, port, "GET", path) if response.status == 301 or response.status == 302 then local url_parse_res = url.parse(response.header.location) urlp = url_parse_res.path stdnse.print_debug("Status code: " .. response.status) response = http.generic_request(host,port,"GET", urlp) end output_info = stdnse.output_table() if response == nil then return fail("Request failed") end local try_counter = 1 while try_counter < 6 and response.status ~= 200 do response = http.generic_request(host, port, "GET", urlp) try_counter = try_counter + 1 end if response.status == 200 then found, matches = http.response_contains(response, "%/vco%-ui.([0-9.]+).", false) if found == true then vsdwan = matches[1] else return nil end output_info.vsdwan_version = {} table.insert(output_info.vsdwan_version, "VMware NSX Version: " .. vsdwan) end return output_info, stdnse.format_output(true, output_info) end local function fortinet(host, port) local resp_js_path, js_path, resp_js local conf_build, conf_model, conf_label local output_info = {} local version -- trigger 401 error to find path to js file with version resp_js_path = http.get(host, port, "/api") if not resp_js_path.body then return nil end -- search for js file that contains version js_path = string.match(resp_js_path.body:lower(), "