description = [[ Attempts to discover vulnerabilities by matching information from the version detection engine with databases such as CVE, ExploitDB and Scipvuldb. This script uses version information (-sV) to match product names with vulnerability databases stored in Nmap's NSE data directory. The databases are distributed separately, hence they need to be download manually before using the script. Optionally you may create empty placeholder files and execute the script update functionality to populate the databases (--script-args updatedb). The following databases are supported at the moment (in nselib/data/): * Scipvuldb (http://www.scip.ch/en/?vuldb) Vulnerability feed URL: http://www.scip.ch/vuldb/scipvuldb.csv * CVE (http://cve.mitre.org) Vulnerability feed URL: http://cve.mitre.org/data/downloads/allitems.csv * ExploitDB (http://www.exploit-db.com) Vulnerability feed URL: https://raw.githubusercontent.com/offensive-security/exploit-database/master/files.csv It is also possible to create and reference your own databases. This requires to create a database file with the following structure: ; Just execute vulscan like you would by refering to one of the pre- delivered databases. Feel free to share your own database and vulnerability connection with me, to add it to the official repository. Vulnerability detection of this script is only as good as Nmap version detection and the vulnerability database entries are. Some databases do not provide conclusive version information, which may lead to a lot of false-positives. REPORTING It is possible to use another pre-defined report structure with the script argument vulscanoutput. The supported output formats are: * details * listid * listlink * listtitle You may enforce your own report structure by using a format string as follows: * --script-args vulscanoutput='{link}\n{title}\n\n' * --script-args vulscanoutput='ID: {id} - Title: {title} ({matches})\n' * --script-args vulscanoutput='{id} | {product} | {version}\n' The supported elements in a dynamic report template are: * {id} ID of the vulnerability * {title} Title of the vulnerability * {matches} Count of matches * {product} Matched product string(s) * {version} Matched version string(s) * {link} Link to the vulnerability database entry * \n Newline * \t Tab Every default database comes with an url and a link, which is used during the scanning and might be accessed as {link} within the customized report template. To use custom database links, use the script argument 'vulscandblink': * --script-args "vulscandblink=http://example.org/{id}" Special credits go to Marc Ruef for creating the original vulscan script and maintaning the vulnerability database Scipvuldb. ]] --- -- @args vulscan.updatedb Updates the supported vulnerability databases. -- @args vulscan.db Sets the vulnerability database to use in a scan. -- @args vulscan.versiondetection Enables/disables version detection matching. -- @args vulscan.showall Show all possible matches (Prone to false positives). -- @args vulscan.interactive Enables interactive mode which allows users to manually -- override version strings. -- @args vulscan.output Sets the report's output format. -- -- @usage nmap --script vulscan --script-args vulscan.updatedb=1 <target> -- @usage nmap --script vulscan --script-args vulscan.db=<vulnerability_database> <target> -- @usage nmap --script vulscan --script-args vulscan.db=cve.csv <target> -- @usage nmap --script vulscan --script-args vulscan.versiondetection=0 <target> -- @usage nmap --script vulscan --script-args vulscan.showall=1 <target> -- @usage nmap --script vulscan --script-args vulscan.interactive=1 <target> -- @usage nmap --script vulscan --script-args vulscan.output=listid <target> -- @usage nmap --script vulscan --script-args vulscan.output='{link}\n{title}\n\n' <target> -- @usage nmap --script vulscan --script-args vulscan.dblink="http://example.org/{id}" <target> -- -- @output -- PORT STATE SERVICE REASON VERSION -- 25/tcp open smtp syn-ack Exim smtpd 4.69 -- | osvdb (22 findings): -- | [2440] qmailadmin autorespond Multiple Variable Remote Overflow -- | [3538] qmail Long SMTP Session DoS -- | [5850] qmail RCPT TO Command Remote Overflow DoS -- | [14176] MasqMail Piped Aliases Privilege Escalation --- author = {"Marc Ruef <marc.ruef-at-computec.ch>", "Jiayi Ye", "Paulino Calderon"} license = "Same as Nmap--See http://nmap.org/book/man-legal.html" categories = {"safe", "vuln"} local stdnse = require "stdnse" local http = require "http" local DATA_PATH = "nselib/data/" portrule = function(host, port) if nmap.registry.vulscan and (nmap.registry.vulscan.updatedb == true or nmap.registry.vulscan.nodb == true) then return false end if port.version.product ~= nil and port.version.product ~= "" then return true else stdnse.debug1("No version detection data available. Analysis not possible.") end end -- check if database exists before starting local function check_vuln_db(db) local filepath = nmap.fetchfile(DATA_PATH .. db) if filepath then return true end return false end --Writes string to file --Taken from: hostmap.nse -- @param filename Filename to write -- @param contents Content of file -- @return True if file was written successfully local function write_file (filepath, contents) local f, err = io.open(filepath, "w") if not f then return f, err end f:write(contents) f:close() return true end local function update_cve(url, filename) local filepath = nmap.fetchfile(DATA_PATH .. filename) if filepath == nil then return false, "Couldn't fetch " .. DATA_PATH .. filename end -- Replace spaces in the path with %20 url = string.gsub(url, " ", "%%20") local result = http.get_url(url) if(result['status'] ~= 200) then return false, "Status is not 200 for " .. url end if result['content-length'] == 0 then return false, "Content is empty for " .. url end local content = "" local regexp = "(CVE[^,]+),[^,]+,\"([^\"]+)\"," for line in string.gmatch(result.body,"[^\n]+") do local id, name = string.match(line, regexp) if id and name then content = content .. id .. ";" .. name .. "\n" end end return write_file(filepath, content) end local function update_exploit_db(url, filename) local filepath = nmap.fetchfile(DATA_PATH .. filename) if filepath == nil then return false, "Couldn't fetch " .. DATA_PATH .. filename end -- Replace spaces in the path with %20 url = string.gsub(url, " ", "%%20") local result = http.get_url(url) if(result['status'] ~= 200) then return false, "Status is not 200 for " .. url end if result['content-length'] == 0 then return false, "Content is empty for " .. url end local content = "" local regexp = "([^,]+),[^,]+,\"([^\"]+)\"," for line in string.gmatch(result.body,"[^\n]+") do local id, name = string.match(line, regexp) if id and name then content = content .. id .. ";" .. name .. "\n" end end return write_file(filepath, content) end local function update_scip_db(url, filename) local filepath = nmap.fetchfile(DATA_PATH .. filename) if filepath == nil then return false, "Couldn't fetch " .. DATA_PATH .. filename end -- Replace spaces in the path with %20 url = string.gsub(url, " ", "%%20") local result = http.get_url(url) if(result['status'] ~= 200) then return false, "Status is not 200 for " .. url end if result['content-length'] == 0 then return false, "Content is empty for " .. url end return write_file(filepath, result.body) end -- update vulnerability database local function update_vuln_db() local status, result = update_cve("http://cve.mitre.org/data/downloads/allitems.csv" , "cve.csv") if not status then stdnse.print_verbose("Failed to update MITRE CVE. " .. result) else stdnse.print_verbose("Updated MITRE CVE successfully.") end status, result = update_exploit_db("https://raw.githubusercontent.com/" .. "offensive-security/exploit-database/master/files.csv", "exploitdb.csv") if not status then stdnse.print_verbose("Failed to update Exploit-DB. " .. result) else stdnse.print_verbose("Updated Exploit-DB successfully.") end status, result = update_scip_db("http://www.scip.ch/vuldb/scipvuldb.csv", "scipvuldb.csv") if not status then stdnse.print_verbose("Failed to update scip VulDB. " .. result) else stdnse.print_verbose("Updated scip VulDB successfully.") end stdnse.debug1("Update finished") end -- We don't like unescaped things local function escape(s) s = string.gsub(s, "%%", "%%%%") return s end -- Parse the report output structure local function report_parsing(v, struct, link, match) local s = struct --database data (needs to be first) s = string.gsub(s, "{link}", escape(link)) --layout elements (needs to be second) s = string.gsub(s, "\\n", "\n") s = string.gsub(s, "\\t", "\t") --vulnerability data (needs to be third) -- match.id = escape(v.id) s = string.gsub(s, "{id}", escape(v.id)) match.title = escape(v.title) s = string.gsub(s, "{title}", escape(v.title)) match.link = string.gsub(escape(link), "{id}", escape(v.id)) if string.find(s, "{matches}") then match.matches = escape(v.matches) end s = string.gsub(s, "{matches}", escape(v.matches)) if string.find(s, "{product}") then match.matches = escape(v.product) end s = string.gsub(s, "{product}", escape(v.product)) if string.find(s, "{version}") then match.matches = escape(v.version) end s = string.gsub(s, "{version}", escape(v.version)) return s end -- Get the row of a CSV file local function extract_from_table(line, col, del) local val = stdnse.strsplit(del, line) if type(val[col]) == "string" then return val[col] end end -- Read a file local function read_from_file(file) local filepath = nmap.fetchfile(file) if filepath then local f, err, _ = io.open(filepath, "r") if not f then stdnse.debug1("vulscan: Failed to open file %s", file) end local line, ret = nil, {} while true do line = f:read() if not line then break end ret[#ret+1] = line end f:close() return ret else stdnse.debug1("vulscan: File %s not found", file) return nil end end -- Find the product matches in the vulnerability databases local function find_vulnerabilities(prod, ver, db, version_detection) local v = {} -- matching vulnerabilities local v_id -- id of vulnerability local v_title -- title of vulnerability local v_title_lower -- title of vulnerability in lowercase for speedup local v_found -- if a match could be found -- Load database local v_entries = read_from_file(DATA_PATH .. db) -- Clean useless dataparts (speeds up search and improves accuracy) prod = string.gsub(prod, " httpd", "") prod = string.gsub(prod, " smtpd", "") prod = string.gsub(prod, " ftpd", "") prod = string.gsub(prod, " smbd", "") if not(v_entries) then return v end local prod_words = stdnse.strsplit(" ", prod) stdnse.debug1("vulscan: Starting search of %s in %s (%d entries) ...", prod, db, #v_entries) -- Iterate through the vulnerabilities in the database for i=1, #v_entries, 1 do v_id = extract_from_table(v_entries[i], 1, ";") v_title = extract_from_table(v_entries[i], 2, ";") if type(v_title) == "string" then v_title_lower = string.lower(v_title) local isMatch = true for j=1, #prod_words, 1 do v_found = string.find(v_title_lower, escape(string.lower(prod_words[j])), 1) if v_found == nil then isMatch = false end end if isMatch == true then if #v == 0 then -- Initiate table v[1] = { id = v_id, title = v_title, product = prod, version = "", matches = 1 } elseif v[#v].id ~= v_id then -- Create new entry v[#v+1] = { id = v_id, title = v_title, product = prod, version = "", matches = 1 } else -- Add to current entry v[#v].product = v[#v].product .. " " .. prod v[#v].matches = v[#v].matches+1 end stdnse.debug2("vulscan: Match v_id %s -> v[%d] (%d match) (Prod: %s)", v_id, #v, v[#v].matches, prod) end -- Additional version matching if version_detection ~= "0" and ver ~= nil and ver ~= "" then --stdnse.debug1("Aditional version matching is set.") if v[#v] ~= nil and v[#v].id == v_id then for k=0, string.len(ver)-1, 1 do v_version = string.sub(ver, 1, string.len(ver)-k) v_found = string.find(string.lower(v_title), string.lower(" " .. v_version), 1) if type(v_found) == "number" then v[#v].version = v[#v].version .. v_version .. " " v[#v].matches = v[#v].matches+1 stdnse.debug2("vulscan: Match v_id %s -> v[%d] (%d match) (Version: %s)", v_id, #v, v[#v].matches, v_version) end end end end end end return v end -- Prepare the resulting matches local function prepare_result(v, struct, link, show_all, ids) local grace = 0 -- grace trigger local match_max = 0 -- counter for maximum matches local match_max_title = "" -- title of the maximum match local s = "" -- the output string -- Search the entries with the best matches if #v > 0 then -- Find maximum matches for i=1, #v, 1 do if v[i].matches > match_max then match_max = v[i].matches match_max_title = v[i].title end end stdnse.debug2("vulscan: Maximum matches of a finding are %d (%s)", match_max, match_max_title) if match_max > 0 then for matchpoints=match_max, 1, -1 do for i=1, #v, 1 do if v[i].matches == matchpoints then stdnse.debug2("vulscan: Setting up result id %d", i) local match = stdnse.output_table() s = s .. report_parsing(v[i], struct, link, match) ids[v[i].id] = match end end if show_all ~= "1" and s ~= "" then -- If the next iteration shall be approached (increases matches) if grace == 0 then stdnse.debug2("vulscan: Best matches found in 1st pass." .. "Going to use 2nd pass ...") grace = grace+1 elseif show_all ~= "1" then break end end end end end return s end action = function(host, port) local interactive_arg = stdnse.get_script_args(SCRIPT_NAME..".interactive") or nil local output_arg = stdnse.get_script_args(SCRIPT_NAME..".output") or nil local updatedb_arg = stdnse.get_script_args(SCRIPT_NAME..".updatedb") or nil local dblink_arg = stdnse.get_script_args(SCRIPT_NAME..".dblink") or nil local db_arg = stdnse.get_script_args(SCRIPT_NAME..".db") or nil local showall_arg = stdnse.get_script_args(SCRIPT_NAME..".showall") or nil local versiondetection_arg = stdnse.get_script_args(SCRIPT_NAME..".versiondetection") or 1 local mutex = nmap.mutex("vulscan") mutex "lock" if not nmap.registry.vulscan then nmap.registry.vulscan = {} nmap.registry.vulscan.updatedb = false nmap.registry.vulscan.nodb = false if updatedb_arg then stdnse.debug1("Updating databases...") nmap.registry.vulscan.updatedb = true update_vuln_db() return string.format("Vulnerability databases updated.") end end mutex "done" local prod = port.version.product -- product name local ver = port.version.version -- product version local struct = "[{id}] {title}\nURL:{link}\n" -- default report structure local db = {} -- vulnerability database local db_link = "" -- custom link for vulnerability databases local vul = {} -- details for the vulnerability local v_count = 0 -- counter for the vulnerabilities local s = "" -- the output string stdnse.debug1("vulscan: Found service %s", prod) -- Go into interactive mode if interactive_arg then stdnse.debug1("vulscan: Enabling interactive mode ...") stdnse.print_verbose(string.format("The scan has determined the following product:%s", prod)) stdnse.print_verbose("Press Enter to accept or define a new product name string.") local prod_override = io.stdin:read'*l' if string.len(prod_override) ~= 0 then prod = prod_override stdnse.print_verbose("New product name string: %s", prod) end end -- Read custom report structure if output_arg ~= nil then if output_arg == "details" then struct = "[{id}] {title}\nMatches: {matches}," .. "Prod: {product}, Ver: {version}\n{link}\n\n" elseif output_arg == "listid" then struct = "{id}\n" elseif output_arg == "listlink" then struct = "{link}\n" elseif output_arg == "listtitle" then struct = "{title}\n" else struct = output_arg end stdnse.debug1("vulscan: Custom output structure defined as %s", struct) end -- Read custom database link if dblink_arg ~= nil then db_link = dblink_arg stdnse.debug1("vulscan: Custom database link defined as %s", db_link) end -- Add your own database, if you want to include it in the multi db mode db[1] = {name="MITRE CVE", file="cve.csv", url="http://cve.mitre.org", link="http://cve.mitre.org/cgi-bin/cvename.cgi?name={id}"} db[2] = {name="Exploit-DB", file="exploitdb.csv", url="http://www.exploit-db.com", link="http://www.exploit-db.com/exploits/{id}"} db[3] = {name="scip VulDB", file="scipvuldb.csv", url="http://www.scip.ch/en/?vuldb", link="http://www.scip.ch/en/?vuldb.{id}"} local output = stdnse.output_table() if db_arg then stdnse.debug1("vulscan: Using single mode db:%s", db_arg) local dbstatus = check_vuln_db(db_arg) if dbstatus == false then stdnse.print_verbose("Operation failed. Could not read database '%s'", db_arg) return string.format("Could not find database '%s' in your nselib/data directory.", db_arg) end vul = find_vulnerabilities(prod, ver, db_arg, versiondetection_arg) local dbmatches = stdnse.output_table() if #vul > 0 then for i,v in ipairs(db) do if db_arg == v.file then db_link = v.link break end end s = s .. db_arg if db_link ~= "" then s = s .. " - " .. db_link end dbmatches.status = "Matches" local ids = stdnse.output_table() s = s .. ":\n" .. prepare_result(vul, struct, db_link, showall_arg, ids) .. "\n\n" dbmatches.IDs = ids else s = s .. "There were no matches. \n\n" dbmatches.status = "No matches" end output[db_arg] = dbmatches else stdnse.debug1("vulscan: Using multi db mode (%d databases) ...", #db) local gstatus = false local dbstatus = {} for i, v in ipairs(db) do dbstatus[i] = check_vuln_db(v.file) gstatus = dbstatus[i] or gstatus end if gstatus == false then mutex "lock" nmap.registry.vulscan.nodb = true mutex "done" stdnse.print_verbose("Failed: No database available.") end for i,v in ipairs(db) do vul = find_vulnerabilities(prod, ver, v.file) local dbmatches = stdnse.output_table() s = s .. v.name .. " - " .. v.url .. ":\n" if vul and #vul > 0 then v_count = v_count + #vul dbmatches.status = "Matches" local ids = stdnse.output_table() s = s .. prepare_result(vul, struct, v.link, showall_arg, ids) .. "\n" dbmatches.IDs = ids elseif dbstatus[i] == false then s = s .. "This database is not installed on the system.\n\n" dbmatches.status = "DB not installed" else s = s .. "There were no matches. \n\n" dbmatches.status = "No matches" end output[v.name] = dbmatches stdnse.debug1("vulscan: %d matches in %s", #vul, v.file) end stdnse.debug1("vulscan: %d matches in total", v_count) end if s then return output, s end end