diff --git a/scripts/vulners.nse b/scripts/vulners.nse new file mode 100644 index 000000000..284b2be99 --- /dev/null +++ b/scripts/vulners.nse @@ -0,0 +1,220 @@ +description = [[ +For each available CPE the script prints out known vulns (links to the correspondent info) and correspondent CVSS scores. + +Its work is pretty simple: +- work only when some software version is identified for an open port +- take all the known CPEs for that software (from the standard nmap -sV output) +- make a request to a remote server (vulners.com API) to learn whether any known vulns exist for that CPE + - if no info is found this way - try to get it using the software name alone +- print the obtained info out + +NB: +Since the size of the DB with all the vulns is more than 250GB there is no way to use a local db. +So we do make requests to a remote service. Still all the requests contain just two fields - the +software name and its version (or CPE), so one can still have the desired privacy. +]] + +--- +-- @usage +-- nmap -sV --script vulners [--script-args mincvss=] +-- +-- @output +-- +-- 53/tcp open domain ISC BIND DNS +-- | vulners: +-- | ISC BIND DNS: +-- | CVE-2012-1667 8.5 https://vulners.com/cve/CVE-2012-1667 +-- | CVE-2002-0651 7.5 https://vulners.com/cve/CVE-2002-0651 +-- | CVE-2002-0029 7.5 https://vulners.com/cve/CVE-2002-0029 +-- | CVE-2015-5986 7.1 https://vulners.com/cve/CVE-2015-5986 +-- | CVE-2010-3615 5.0 https://vulners.com/cve/CVE-2010-3615 +-- | CVE-2006-0987 5.0 https://vulners.com/cve/CVE-2006-0987 +-- | CVE-2014-3214 5.0 https://vulners.com/cve/CVE-2014-3214 +-- + +author = 'gmedian AT vulners DOT com' +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"vuln", "safe", "external"} + + +local http = require "http" +local json = require "json" +local string = require "string" +local table = require "table" + +local api_version="1.2" +local mincvss=nmap.registry.args.mincvss and tonumber(nmap.registry.args.mincvss) or 0.0 + + +portrule = function(host, port) + local vers=port.version + return vers ~= nil and vers.version ~= nil +end + + +--- +-- Return a string with all the found cve's and correspondent links +-- +-- @param vulns a table with the parsed json response from the vulners server +-- +function make_links(vulns) + local output_str="" + local is_exploit=false + local cvss_score="" + + -- NOTE[gmedian]: data.search is a "list" already, so just use table.sort with a custom compare function + -- However, for the future it might be wiser to create a copy rather than do it in-place + + local vulns_result = {} + for _, v in ipairs(vulns.data.search) do + table.insert(vulns_result, v) + end + + -- Sort the acquired vulns by the CVSS score + table.sort(vulns_result, function(a, b) + return a._source.cvss.score > b._source.cvss.score + end + ) + + for _, vuln in ipairs(vulns_result) do + -- Mark the exploits out + is_exploit = vuln._source.bulletinFamily:lower() == "exploit" + + -- Sometimes it might happen, so check the score availability + cvss_score = vuln._source.cvss and (type(vuln._source.cvss.score) == "number") and (vuln._source.cvss.score) or "" + + -- NOTE[gmedian]: exploits seem to have cvss == 0, so print them anyway + if is_exploit or (cvss_score ~= "" and mincvss <= tonumber(cvss_score)) then + output_str = string.format("%s\n\t%s", output_str, vuln._source.id .. "\t\t" .. cvss_score .. '\t\thttps://vulners.com/' .. vuln._source.type .. '/' .. vuln._source.id .. (is_exploit and '\t\t*EXPLOIT*' or '')) + end + end + + return output_str +end + + +--- +-- Issues the requests, receives json and parses it, calls make_links when successfull +-- +-- @param what string, future value for the software query argument +-- @param vers string, the version query argument +-- @param type string, the type query argument +-- +function get_results(what, vers, type) + local v_host="vulners.com" + local v_port=443 + local response, path + local status, error + local vulns + local option={header={}} + + option['header']['User-Agent'] = string.format('Vulners NMAP Plugin %s', api_version) + + path = '/api/v3/burp/software/' .. '?software=' .. what .. '&version=' .. vers .. '&type=' .. type + + response = http.get(v_host, v_port, path, option) + + status = response.status + if status == nil then + -- Something went really wrong out there + -- According to the NSE way we will die silently rather than spam user with error messages + return "" + elseif status ~= 200 then + -- Again just die silently + return "" + end + + status, vulns = json.parse(response.body) + + if status == true then + if vulns.result == "OK" then + return make_links(vulns) + end + end + + return "" +end + + +--- +-- Calls get_results for type="software" +-- +-- It is called from action when nothing is found for the available cpe's +-- +-- @param software string, the software name +-- @param version string, the software version +-- +function get_vulns_by_software(software, version) + return get_results(software, version, "software") +end + + +--- +-- Calls get_results for type="cpe" +-- +-- Takes the version number from the given cpe and tries to get the result. +-- If none found, changes the given cpe a bit in order to possibly separate version number from the patch version +-- And makes another attempt. +-- Having failed returns an empty string. +-- +-- @param cpe string, the given cpe +-- +function get_vulns_by_cpe(cpe) + local vers + local vers_regexp=":([%d%.%-%_]+)([^:]*)$" + local output_str="" + + -- TODO[gmedian]: add check for cpe:/a as we might be interested in software rather than in OS (cpe:/o) and hardware (cpe:/h) + -- TODO[gmedian]: work not with the LAST part but simply with the THIRD one (according to cpe doc it must be version) + + -- NOTE[gmedian]: take only the numeric part of the version + _, _, vers = cpe:find(vers_regexp) + + + if not vers then + return "" + end + + output_str = get_results(cpe, vers, "cpe") + + if output_str == "" then + local new_cpe + + new_cpe = cpe:gsub(vers_regexp, ":%1:%2") + output_str = get_results(new_cpe, vers, "cpe") + end + + return output_str +end + + +action = function(host, port) + local tab={} + local changed=false + local response + local output_str="" + + for i, cpe in ipairs(port.version.cpe) do + output_str = get_vulns_by_cpe(cpe, port.version) + if output_str ~= "" then + tab[cpe] = output_str + changed = true + end + end + + -- NOTE[gmedian]: issue request for type=software, but only when nothing is found so far + if not changed then + local vendor_version = port.version.product .. " " .. port.version.version + output_str = get_vulns_by_software(port.version.product, port.version.version) + if output_str ~= "" then + tab[vendor_version] = output_str + changed = true + end + end + + if (not changed) then + return + end + return tab +end +